Skip to repository content1193 lines Β· 44.2 KB Β· rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:38:53.345Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches Β· 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
folding_ranges.rs
1use futures::future::join_all;
2use itertools::Itertools;
3use language::language_settings::LanguageSettings;
4use text::BufferId;
5use ui::{Context, Window};
6
7use crate::{Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT};
8
9impl Editor {
10 pub(super) fn refresh_folding_ranges(
11 &mut self,
12 for_buffer: Option<BufferId>,
13 _window: &Window,
14 cx: &mut Context<Self>,
15 ) {
16 if !self.lsp_data_enabled() || !self.use_document_folding_ranges {
17 return;
18 }
19 let Some(project) = self.project.as_ref().map(|p| p.downgrade()) else {
20 return;
21 };
22
23 let buffers_to_query = self
24 .visible_buffers(cx)
25 .into_iter()
26 .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
27 .chain(for_buffer.and_then(|id| self.buffer.read(cx).buffer(id)))
28 .filter(|buffer| {
29 let id = buffer.read(cx).remote_id();
30 (for_buffer.is_none_or(|target| target == id))
31 && self.registered_buffers.contains_key(&id)
32 && LanguageSettings::for_buffer(buffer.read(cx), cx)
33 .document_folding_ranges
34 .enabled()
35 })
36 .unique_by(|buffer| buffer.read(cx).remote_id())
37 .collect::<Vec<_>>();
38
39 self.refresh_folding_ranges_task = cx.spawn(async move |editor, cx| {
40 cx.background_executor()
41 .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
42 .await;
43
44 let Some(tasks) = editor
45 .update(cx, |_, cx| {
46 let project = project.upgrade()?;
47 Some(project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
48 buffers_to_query
49 .into_iter()
50 .map(|buffer| {
51 let buffer_id = buffer.read(cx).remote_id();
52 let task = lsp_store.fetch_folding_ranges(&buffer, cx);
53 async move { (buffer_id, task.await) }
54 })
55 .collect::<Vec<_>>()
56 }))
57 })
58 .ok()
59 .flatten()
60 else {
61 return;
62 };
63
64 let results = join_all(tasks).await;
65 if results.is_empty() {
66 return;
67 }
68
69 editor
70 .update(cx, |editor, cx| {
71 editor.display_map.update(cx, |display_map, cx| {
72 for (buffer_id, ranges) in results {
73 display_map.set_lsp_folding_ranges(buffer_id, ranges, cx);
74 }
75 });
76 cx.notify();
77 })
78 .ok();
79 });
80 }
81
82 pub fn document_folding_ranges_enabled(&self, cx: &ui::App) -> bool {
83 self.use_document_folding_ranges && self.display_map.read(cx).has_lsp_folding_ranges()
84 }
85
86 /// Removes LSP folding creases for buffers whose `lsp_folding_ranges`
87 /// setting has been turned off, and triggers a refresh so newly-enabled
88 /// buffers get their ranges fetched.
89 pub(super) fn clear_disabled_lsp_folding_ranges(
90 &mut self,
91 window: &mut Window,
92 cx: &mut Context<Self>,
93 ) {
94 if !self.use_document_folding_ranges {
95 return;
96 }
97
98 let buffers_to_clear = self
99 .buffer
100 .read(cx)
101 .all_buffers()
102 .into_iter()
103 .filter(|buffer| {
104 let buffer = buffer.read(cx);
105 !LanguageSettings::for_buffer(&buffer, cx)
106 .document_folding_ranges
107 .enabled()
108 })
109 .map(|buffer| buffer.read(cx).remote_id())
110 .collect::<Vec<_>>();
111
112 if !buffers_to_clear.is_empty() {
113 self.display_map.update(cx, |display_map, cx| {
114 for buffer_id in buffers_to_clear {
115 display_map.clear_lsp_folding_ranges(buffer_id, cx);
116 }
117 });
118 cx.notify();
119 }
120
121 self.refresh_folding_ranges(None, window, cx);
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use futures::StreamExt as _;
128 use gpui::TestAppContext;
129 use lsp::FoldingRange;
130 use multi_buffer::MultiBufferRow;
131 use pretty_assertions::assert_eq;
132 use settings::DocumentFoldingRanges;
133
134 use crate::{
135 editor_tests::{init_test, update_test_language_settings},
136 test::editor_lsp_test_context::EditorLspTestContext,
137 };
138
139 #[gpui::test]
140 async fn test_lsp_folding_ranges_populates_creases(cx: &mut TestAppContext) {
141 init_test(cx, |_| {});
142
143 update_test_language_settings(cx, &|settings| {
144 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On);
145 });
146
147 let mut cx = EditorLspTestContext::new_rust(
148 lsp::ServerCapabilities {
149 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
150 ..lsp::ServerCapabilities::default()
151 },
152 cx,
153 )
154 .await;
155
156 let mut folding_request = cx
157 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(
158 move |_, _, _| async move {
159 Ok(Some(vec![
160 FoldingRange {
161 start_line: 0,
162 start_character: Some(10),
163 end_line: 4,
164 end_character: Some(1),
165 kind: None,
166 collapsed_text: None,
167 },
168 FoldingRange {
169 start_line: 1,
170 start_character: Some(13),
171 end_line: 3,
172 end_character: Some(5),
173 kind: None,
174 collapsed_text: None,
175 },
176 FoldingRange {
177 start_line: 6,
178 start_character: Some(11),
179 end_line: 8,
180 end_character: Some(1),
181 kind: None,
182 collapsed_text: None,
183 },
184 ]))
185 },
186 );
187
188 cx.set_state(
189 "Λfn main() {\n if true {\n println!(\"hello\");\n }\n}\n\nfn other() {\n let x = 1;\n}\n",
190 );
191 assert!(folding_request.next().await.is_some());
192 cx.run_until_parked();
193
194 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
195 assert!(
196 editor.document_folding_ranges_enabled(cx),
197 "Expected LSP folding ranges to be populated"
198 );
199 });
200
201 cx.update_editor(|editor, _window, cx| {
202 let snapshot = editor.display_snapshot(cx);
203 assert!(
204 !snapshot.is_line_folded(MultiBufferRow(0)),
205 "Line 0 should not be folded before any fold action"
206 );
207 assert!(
208 !snapshot.is_line_folded(MultiBufferRow(6)),
209 "Line 6 should not be folded before any fold action"
210 );
211 });
212
213 cx.update_editor(|editor, window, cx| {
214 editor.fold_at(MultiBufferRow(0), window, cx);
215 });
216
217 cx.update_editor(|editor, _window, cx| {
218 let snapshot = editor.display_snapshot(cx);
219 assert!(
220 snapshot.is_line_folded(MultiBufferRow(0)),
221 "Line 0 should be folded after fold_at on an LSP crease"
222 );
223 assert_eq!(
224 editor.display_text(cx),
225 "fn main() β―\n\nfn other() {\n let x = 1;\n}\n",
226 );
227 });
228
229 cx.update_editor(|editor, window, cx| {
230 editor.fold_at(MultiBufferRow(6), window, cx);
231 });
232
233 cx.update_editor(|editor, _window, cx| {
234 let snapshot = editor.display_snapshot(cx);
235 assert!(
236 snapshot.is_line_folded(MultiBufferRow(6)),
237 "Line 6 should be folded after fold_at on the second LSP crease"
238 );
239 assert_eq!(editor.display_text(cx), "fn main() β―\n\nfn other() β―\n",);
240 });
241 }
242
243 #[gpui::test]
244 async fn test_lsp_folding_ranges_disabled_by_default(cx: &mut TestAppContext) {
245 init_test(cx, |_| {});
246
247 let mut cx = EditorLspTestContext::new_rust(
248 lsp::ServerCapabilities {
249 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
250 ..lsp::ServerCapabilities::default()
251 },
252 cx,
253 )
254 .await;
255
256 cx.set_state("Λfn main() {\n let x = 1;\n}\n");
257 cx.run_until_parked();
258
259 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
260 assert!(
261 !editor.document_folding_ranges_enabled(cx),
262 "LSP folding ranges should not be enabled by default"
263 );
264 });
265 }
266
267 #[gpui::test]
268 async fn test_lsp_folding_ranges_toggling_off_removes_creases(cx: &mut TestAppContext) {
269 init_test(cx, |_| {});
270
271 update_test_language_settings(cx, &|settings| {
272 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On);
273 });
274
275 let mut cx = EditorLspTestContext::new_rust(
276 lsp::ServerCapabilities {
277 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
278 ..lsp::ServerCapabilities::default()
279 },
280 cx,
281 )
282 .await;
283
284 let mut folding_request = cx
285 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(
286 move |_, _, _| async move {
287 Ok(Some(vec![FoldingRange {
288 start_line: 0,
289 start_character: Some(10),
290 end_line: 4,
291 end_character: Some(1),
292 kind: None,
293 collapsed_text: None,
294 }]))
295 },
296 );
297
298 cx.set_state("Λfn main() {\n if true {\n println!(\"hello\");\n }\n}\n");
299 assert!(folding_request.next().await.is_some());
300 cx.run_until_parked();
301
302 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
303 assert!(
304 editor.document_folding_ranges_enabled(cx),
305 "Expected LSP folding ranges to be active before toggling off"
306 );
307 });
308
309 cx.update_editor(|editor, window, cx| {
310 editor.fold_at(MultiBufferRow(0), window, cx);
311 });
312 cx.update_editor(|editor, _window, cx| {
313 let snapshot = editor.display_snapshot(cx);
314 assert!(
315 snapshot.is_line_folded(MultiBufferRow(0)),
316 "Line 0 should be folded via LSP crease before toggling off"
317 );
318 assert_eq!(editor.display_text(cx), "fn main() β―\n",);
319 });
320
321 update_test_language_settings(&mut cx.cx.cx, &|settings| {
322 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::Off);
323 });
324 cx.run_until_parked();
325
326 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
327 assert!(
328 !editor.document_folding_ranges_enabled(cx),
329 "LSP folding ranges should be cleared after toggling off"
330 );
331 });
332 }
333
334 #[gpui::test]
335 async fn test_lsp_folding_ranges_nested_folds(cx: &mut TestAppContext) {
336 init_test(cx, |_| {});
337
338 update_test_language_settings(cx, &|settings| {
339 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On);
340 });
341
342 let mut cx = EditorLspTestContext::new_rust(
343 lsp::ServerCapabilities {
344 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
345 ..lsp::ServerCapabilities::default()
346 },
347 cx,
348 )
349 .await;
350
351 let mut folding_request = cx
352 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(
353 move |_, _, _| async move {
354 Ok(Some(vec![
355 FoldingRange {
356 start_line: 0,
357 start_character: Some(10),
358 end_line: 7,
359 end_character: Some(1),
360 kind: None,
361 collapsed_text: None,
362 },
363 FoldingRange {
364 start_line: 1,
365 start_character: Some(12),
366 end_line: 3,
367 end_character: Some(5),
368 kind: None,
369 collapsed_text: None,
370 },
371 FoldingRange {
372 start_line: 4,
373 start_character: Some(13),
374 end_line: 6,
375 end_character: Some(5),
376 kind: None,
377 collapsed_text: None,
378 },
379 ]))
380 },
381 );
382
383 cx.set_state(
384 "Λfn main() {\n if true {\n a();\n }\n if false {\n b();\n }\n}\n",
385 );
386 assert!(folding_request.next().await.is_some());
387 cx.run_until_parked();
388
389 cx.update_editor(|editor, window, cx| {
390 editor.fold_at(MultiBufferRow(1), window, cx);
391 });
392 cx.update_editor(|editor, _window, cx| {
393 let snapshot = editor.display_snapshot(cx);
394 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
395 assert!(!snapshot.is_line_folded(MultiBufferRow(0)));
396 assert_eq!(
397 editor.display_text(cx),
398 "fn main() {\n if true β―\n if false {\n b();\n }\n}\n",
399 );
400 });
401
402 cx.update_editor(|editor, window, cx| {
403 editor.fold_at(MultiBufferRow(4), window, cx);
404 });
405 cx.update_editor(|editor, _window, cx| {
406 let snapshot = editor.display_snapshot(cx);
407 assert!(snapshot.is_line_folded(MultiBufferRow(4)));
408 assert_eq!(
409 editor.display_text(cx),
410 "fn main() {\n if true β―\n if false β―\n}\n",
411 );
412 });
413
414 cx.update_editor(|editor, window, cx| {
415 editor.fold_at(MultiBufferRow(0), window, cx);
416 });
417 cx.update_editor(|editor, _window, cx| {
418 let snapshot = editor.display_snapshot(cx);
419 assert!(snapshot.is_line_folded(MultiBufferRow(0)));
420 assert_eq!(editor.display_text(cx), "fn main() β―\n",);
421 });
422 }
423
424 #[gpui::test]
425 async fn test_lsp_folding_ranges_unsorted_from_server(cx: &mut TestAppContext) {
426 init_test(cx, |_| {});
427
428 update_test_language_settings(cx, &|settings| {
429 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On);
430 });
431
432 let mut cx = EditorLspTestContext::new_rust(
433 lsp::ServerCapabilities {
434 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
435 ..lsp::ServerCapabilities::default()
436 },
437 cx,
438 )
439 .await;
440
441 let mut folding_request = cx
442 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(
443 move |_, _, _| async move {
444 Ok(Some(vec![
445 FoldingRange {
446 start_line: 6,
447 start_character: Some(11),
448 end_line: 8,
449 end_character: Some(1),
450 kind: None,
451 collapsed_text: None,
452 },
453 FoldingRange {
454 start_line: 0,
455 start_character: Some(10),
456 end_line: 4,
457 end_character: Some(1),
458 kind: None,
459 collapsed_text: None,
460 },
461 FoldingRange {
462 start_line: 1,
463 start_character: Some(13),
464 end_line: 3,
465 end_character: Some(5),
466 kind: None,
467 collapsed_text: None,
468 },
469 ]))
470 },
471 );
472
473 cx.set_state(
474 "Λfn main() {\n if true {\n println!(\"hello\");\n }\n}\n\nfn other() {\n let x = 1;\n}\n",
475 );
476 assert!(folding_request.next().await.is_some());
477 cx.run_until_parked();
478
479 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
480 assert!(
481 editor.document_folding_ranges_enabled(cx),
482 "Expected LSP folding ranges to be populated despite unsorted server response"
483 );
484 });
485
486 cx.update_editor(|editor, window, cx| {
487 editor.fold_at(MultiBufferRow(0), window, cx);
488 });
489 cx.update_editor(|editor, _window, cx| {
490 assert_eq!(
491 editor.display_text(cx),
492 "fn main() β―\n\nfn other() {\n let x = 1;\n}\n",
493 );
494 });
495
496 cx.update_editor(|editor, window, cx| {
497 editor.fold_at(MultiBufferRow(6), window, cx);
498 });
499 cx.update_editor(|editor, _window, cx| {
500 assert_eq!(editor.display_text(cx), "fn main() β―\n\nfn other() β―\n",);
501 });
502 }
503
504 #[gpui::test]
505 async fn test_lsp_folding_ranges_switch_between_treesitter_and_lsp(cx: &mut TestAppContext) {
506 init_test(cx, |_| {});
507
508 let mut cx = EditorLspTestContext::new_rust(
509 lsp::ServerCapabilities {
510 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
511 ..lsp::ServerCapabilities::default()
512 },
513 cx,
514 )
515 .await;
516
517 let source =
518 "fn main() {\n let a = 1;\n let b = 2;\n let c = 3;\n let d = 4;\n}\n";
519 cx.set_state(&format!("Λ{source}"));
520 cx.run_until_parked();
521
522 // Phase 1: tree-sitter / indentation-based folding (LSP folding OFF by default).
523 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
524 assert!(
525 !editor.document_folding_ranges_enabled(cx),
526 "LSP folding ranges should be off by default"
527 );
528 });
529
530 cx.update_editor(|editor, window, cx| {
531 editor.fold_at(MultiBufferRow(0), window, cx);
532 });
533 cx.update_editor(|editor, _window, cx| {
534 let snapshot = editor.display_snapshot(cx);
535 assert!(
536 snapshot.is_line_folded(MultiBufferRow(0)),
537 "Indentation-based fold should work on the function"
538 );
539 assert_eq!(editor.display_text(cx), "fn main() {β―}\n",);
540 });
541
542 cx.update_editor(|editor, window, cx| {
543 editor.unfold_at(MultiBufferRow(0), window, cx);
544 });
545 cx.update_editor(|editor, _window, cx| {
546 assert!(
547 !editor
548 .display_snapshot(cx)
549 .is_line_folded(MultiBufferRow(0)),
550 "Function should be unfolded"
551 );
552 });
553
554 // Phase 2: switch to LSP folding with non-syntactic ("odd") ranges.
555 // The LSP returns two ranges that each cover a pair of let-bindings,
556 // which is not something tree-sitter / indentation folding would produce.
557 let mut folding_request = cx
558 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(
559 move |_, _, _| async move {
560 Ok(Some(vec![
561 FoldingRange {
562 start_line: 1,
563 start_character: Some(14),
564 end_line: 2,
565 end_character: Some(14),
566 kind: None,
567 collapsed_text: None,
568 },
569 FoldingRange {
570 start_line: 3,
571 start_character: Some(14),
572 end_line: 4,
573 end_character: Some(14),
574 kind: None,
575 collapsed_text: None,
576 },
577 ]))
578 },
579 );
580
581 update_test_language_settings(&mut cx.cx.cx, &|settings| {
582 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On);
583 });
584 assert!(folding_request.next().await.is_some());
585 cx.run_until_parked();
586
587 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
588 assert!(
589 editor.document_folding_ranges_enabled(cx),
590 "LSP folding ranges should now be active"
591 );
592 });
593
594 // The indentation fold at row 0 should no longer be available;
595 // only the LSP ranges exist.
596 cx.update_editor(|editor, window, cx| {
597 editor.fold_at(MultiBufferRow(0), window, cx);
598 });
599 cx.update_editor(|editor, _window, cx| {
600 assert!(
601 !editor
602 .display_snapshot(cx)
603 .is_line_folded(MultiBufferRow(0)),
604 "Row 0 has no LSP crease, so fold_at should be a no-op"
605 );
606 });
607
608 cx.update_editor(|editor, window, cx| {
609 editor.fold_at(MultiBufferRow(1), window, cx);
610 });
611 cx.update_editor(|editor, _window, cx| {
612 assert!(
613 editor
614 .display_snapshot(cx)
615 .is_line_folded(MultiBufferRow(1)),
616 "First odd LSP range should fold"
617 );
618 assert_eq!(
619 editor.display_text(cx),
620 "fn main() {\n let a = 1;β―\n let c = 3;\n let d = 4;\n}\n",
621 );
622 });
623
624 cx.update_editor(|editor, window, cx| {
625 editor.fold_at(MultiBufferRow(3), window, cx);
626 });
627 cx.update_editor(|editor, _window, cx| {
628 assert!(
629 editor
630 .display_snapshot(cx)
631 .is_line_folded(MultiBufferRow(3)),
632 "Second odd LSP range should fold"
633 );
634 assert_eq!(
635 editor.display_text(cx),
636 "fn main() {\n let a = 1;β―\n let c = 3;β―\n}\n",
637 );
638 });
639
640 cx.update_editor(|editor, window, cx| {
641 editor.unfold_at(MultiBufferRow(1), window, cx);
642 editor.unfold_at(MultiBufferRow(3), window, cx);
643 });
644
645 // Phase 3: switch back to tree-sitter by disabling LSP folding ranges.
646 update_test_language_settings(&mut cx.cx.cx, &|settings| {
647 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::Off);
648 });
649 cx.run_until_parked();
650
651 cx.editor.read_with(&cx.cx.cx, |editor, cx| {
652 assert!(
653 !editor.document_folding_ranges_enabled(cx),
654 "LSP folding ranges should be cleared after switching back"
655 );
656 });
657
658 cx.update_editor(|editor, window, cx| {
659 editor.fold_at(MultiBufferRow(0), window, cx);
660 });
661 cx.update_editor(|editor, _window, cx| {
662 let snapshot = editor.display_snapshot(cx);
663 assert!(
664 snapshot.is_line_folded(MultiBufferRow(0)),
665 "Indentation-based fold should work again after switching back"
666 );
667 assert_eq!(editor.display_text(cx), "fn main() {β―}\n",);
668 });
669 }
670
671 #[gpui::test]
672 async fn test_lsp_folding_ranges_collapsed_text(cx: &mut TestAppContext) {
673 init_test(cx, |_| {});
674
675 update_test_language_settings(cx, &|settings| {
676 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On);
677 });
678
679 let mut cx = EditorLspTestContext::new_rust(
680 lsp::ServerCapabilities {
681 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
682 ..lsp::ServerCapabilities::default()
683 },
684 cx,
685 )
686 .await;
687
688 let mut folding_request = cx
689 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(
690 move |_, _, _| async move {
691 Ok(Some(vec![
692 // main: custom collapsed text
693 FoldingRange {
694 start_line: 0,
695 start_character: Some(10),
696 end_line: 4,
697 end_character: Some(1),
698 kind: None,
699 collapsed_text: Some("{ fn body }".to_string()),
700 },
701 // other: collapsed text longer than the original folded content
702 FoldingRange {
703 start_line: 6,
704 start_character: Some(11),
705 end_line: 8,
706 end_character: Some(1),
707 kind: None,
708 collapsed_text: Some("{ this collapsed text is intentionally much longer than the original function body it replaces }".to_string()),
709 },
710 // emoji: collapsed text WITH emoji and multi-byte chars
711 FoldingRange {
712 start_line: 10,
713 start_character: Some(11),
714 end_line: 13,
715 end_character: Some(1),
716 kind: None,
717 collapsed_text: Some("{ π¦β¦cafΓ© }".to_string()),
718 },
719 // outer: collapsed text on the outer fn
720 FoldingRange {
721 start_line: 15,
722 start_character: Some(11),
723 end_line: 22,
724 end_character: Some(1),
725 kind: None,
726 collapsed_text: Some("{ outer⦠}".to_string()),
727 },
728 // inner_a: nested inside outer, with collapsed text
729 FoldingRange {
730 start_line: 16,
731 start_character: Some(17),
732 end_line: 18,
733 end_character: Some(5),
734 kind: None,
735 collapsed_text: Some("{ a }".to_string()),
736 },
737 // inner_b: nested inside outer, no collapsed text
738 FoldingRange {
739 start_line: 19,
740 start_character: Some(17),
741 end_line: 21,
742 end_character: Some(5),
743 kind: None,
744 collapsed_text: None,
745 },
746 // newline: collapsed text containing \n
747 FoldingRange {
748 start_line: 24,
749 start_character: Some(13),
750 end_line: 27,
751 end_character: Some(1),
752 kind: None,
753 collapsed_text: Some("{\n β¦\n}".to_string()),
754 },
755 ]))
756 },
757 );
758
759 cx.set_state(
760 &[
761 "Λfn main() {\n",
762 " if true {\n",
763 " println!(\"hello\");\n",
764 " }\n",
765 "}\n",
766 "\n",
767 "fn other() {\n",
768 " let x = 1;\n",
769 "}\n",
770 "\n",
771 "fn emoji() {\n",
772 " let a = \"π¦π₯\";\n",
773 " let b = \"cafΓ©\";\n",
774 "}\n",
775 "\n",
776 "fn outer() {\n",
777 " fn inner_a() {\n",
778 " let x = 1;\n",
779 " }\n",
780 " fn inner_b() {\n",
781 " let y = 2;\n",
782 " }\n",
783 "}\n",
784 "\n",
785 "fn newline() {\n",
786 " let a = 1;\n",
787 " let b = 2;\n",
788 "}\n",
789 ]
790 .concat(),
791 );
792 assert!(folding_request.next().await.is_some());
793 cx.run_until_parked();
794
795 let unfolded_text = [
796 "fn main() {\n",
797 " if true {\n",
798 " println!(\"hello\");\n",
799 " }\n",
800 "}\n",
801 "\n",
802 "fn other() {\n",
803 " let x = 1;\n",
804 "}\n",
805 "\n",
806 "fn emoji() {\n",
807 " let a = \"π¦π₯\";\n",
808 " let b = \"cafΓ©\";\n",
809 "}\n",
810 "\n",
811 "fn outer() {\n",
812 " fn inner_a() {\n",
813 " let x = 1;\n",
814 " }\n",
815 " fn inner_b() {\n",
816 " let y = 2;\n",
817 " }\n",
818 "}\n",
819 "\n",
820 "fn newline() {\n",
821 " let a = 1;\n",
822 " let b = 2;\n",
823 "}\n",
824 ]
825 .concat();
826
827 // Fold newline fn β collapsed text that itself contains \n
828 // (newlines are sanitized to spaces to keep folds single-line).
829 cx.update_editor(|editor, window, cx| {
830 editor.fold_at(MultiBufferRow(24), window, cx);
831 });
832 cx.update_editor(|editor, _window, cx| {
833 assert_eq!(
834 editor.display_text(cx),
835 [
836 "fn main() {\n",
837 " if true {\n",
838 " println!(\"hello\");\n",
839 " }\n",
840 "}\n",
841 "\n",
842 "fn other() {\n",
843 " let x = 1;\n",
844 "}\n",
845 "\n",
846 "fn emoji() {\n",
847 " let a = \"π¦π₯\";\n",
848 " let b = \"cafΓ©\";\n",
849 "}\n",
850 "\n",
851 "fn outer() {\n",
852 " fn inner_a() {\n",
853 " let x = 1;\n",
854 " }\n",
855 " fn inner_b() {\n",
856 " let y = 2;\n",
857 " }\n",
858 "}\n",
859 "\n",
860 "fn newline() { β¦ }\n",
861 ]
862 .concat(),
863 );
864 });
865
866 cx.update_editor(|editor, window, cx| {
867 editor.unfold_all(&crate::actions::UnfoldAll, window, cx);
868 });
869
870 // Fold main β custom collapsed text.
871 cx.update_editor(|editor, window, cx| {
872 editor.fold_at(MultiBufferRow(0), window, cx);
873 });
874 cx.update_editor(|editor, _window, cx| {
875 assert_eq!(
876 editor.display_text(cx),
877 [
878 "fn main() { fn body }\n",
879 "\n",
880 "fn other() {\n",
881 " let x = 1;\n",
882 "}\n",
883 "\n",
884 "fn emoji() {\n",
885 " let a = \"π¦π₯\";\n",
886 " let b = \"cafΓ©\";\n",
887 "}\n",
888 "\n",
889 "fn outer() {\n",
890 " fn inner_a() {\n",
891 " let x = 1;\n",
892 " }\n",
893 " fn inner_b() {\n",
894 " let y = 2;\n",
895 " }\n",
896 "}\n",
897 "\n",
898 "fn newline() {\n",
899 " let a = 1;\n",
900 " let b = 2;\n",
901 "}\n",
902 ]
903 .concat(),
904 );
905 });
906
907 // Fold emoji fn β multi-byte / emoji collapsed text (main still folded).
908 cx.update_editor(|editor, window, cx| {
909 editor.fold_at(MultiBufferRow(10), window, cx);
910 });
911 cx.update_editor(|editor, _window, cx| {
912 assert_eq!(
913 editor.display_text(cx),
914 [
915 "fn main() { fn body }\n",
916 "\n",
917 "fn other() {\n",
918 " let x = 1;\n",
919 "}\n",
920 "\n",
921 "fn emoji() { π¦β¦cafΓ© }\n",
922 "\n",
923 "fn outer() {\n",
924 " fn inner_a() {\n",
925 " let x = 1;\n",
926 " }\n",
927 " fn inner_b() {\n",
928 " let y = 2;\n",
929 " }\n",
930 "}\n",
931 "\n",
932 "fn newline() {\n",
933 " let a = 1;\n",
934 " let b = 2;\n",
935 "}\n",
936 ]
937 .concat(),
938 );
939 });
940
941 // Fold a nested range (inner_a) while outer is still unfolded.
942 cx.update_editor(|editor, window, cx| {
943 editor.fold_at(MultiBufferRow(16), window, cx);
944 });
945 cx.update_editor(|editor, _window, cx| {
946 assert_eq!(
947 editor.display_text(cx),
948 [
949 "fn main() { fn body }\n",
950 "\n",
951 "fn other() {\n",
952 " let x = 1;\n",
953 "}\n",
954 "\n",
955 "fn emoji() { π¦β¦cafΓ© }\n",
956 "\n",
957 "fn outer() {\n",
958 " fn inner_a() { a }\n",
959 " fn inner_b() {\n",
960 " let y = 2;\n",
961 " }\n",
962 "}\n",
963 "\n",
964 "fn newline() {\n",
965 " let a = 1;\n",
966 " let b = 2;\n",
967 "}\n",
968 ]
969 .concat(),
970 );
971 });
972
973 // Unfold everything to reset.
974 cx.update_editor(|editor, window, cx| {
975 editor.unfold_all(&crate::actions::UnfoldAll, window, cx);
976 });
977 cx.update_editor(|editor, _window, cx| {
978 assert_eq!(editor.display_text(cx), unfolded_text);
979 });
980
981 // Fold ALL at once and verify every fold.
982 cx.update_editor(|editor, window, cx| {
983 editor.fold_all(&crate::actions::FoldAll, window, cx);
984 });
985 cx.update_editor(|editor, _window, cx| {
986 assert_eq!(
987 editor.display_text(cx),
988 [
989 "fn main() { fn body }\n",
990 "\n",
991 "fn other() { this collapsed text is intentionally much longer than the original function body it replaces }\n",
992 "\n",
993 "fn emoji() { π¦β¦cafΓ© }\n",
994 "\n",
995 "fn outer() { outer⦠}\n",
996 "\n",
997 "fn newline() { β¦ }\n",
998 ]
999 .concat(),
1000 );
1001 });
1002
1003 // Unfold all again, then fold only the outer, which should swallow inner folds.
1004 cx.update_editor(|editor, window, cx| {
1005 editor.unfold_all(&crate::actions::UnfoldAll, window, cx);
1006 });
1007 cx.update_editor(|editor, window, cx| {
1008 editor.fold_at(MultiBufferRow(15), window, cx);
1009 });
1010 cx.update_editor(|editor, _window, cx| {
1011 assert_eq!(
1012 editor.display_text(cx),
1013 [
1014 "fn main() {\n",
1015 " if true {\n",
1016 " println!(\"hello\");\n",
1017 " }\n",
1018 "}\n",
1019 "\n",
1020 "fn other() {\n",
1021 " let x = 1;\n",
1022 "}\n",
1023 "\n",
1024 "fn emoji() {\n",
1025 " let a = \"π¦π₯\";\n",
1026 " let b = \"cafΓ©\";\n",
1027 "}\n",
1028 "\n",
1029 "fn outer() { outer⦠}\n",
1030 "\n",
1031 "fn newline() {\n",
1032 " let a = 1;\n",
1033 " let b = 2;\n",
1034 "}\n",
1035 ]
1036 .concat(),
1037 );
1038 });
1039
1040 // Unfold the outer, then fold both inners independently.
1041 cx.update_editor(|editor, window, cx| {
1042 editor.unfold_all(&crate::actions::UnfoldAll, window, cx);
1043 });
1044 cx.update_editor(|editor, window, cx| {
1045 editor.fold_at(MultiBufferRow(16), window, cx);
1046 editor.fold_at(MultiBufferRow(19), window, cx);
1047 });
1048 cx.update_editor(|editor, _window, cx| {
1049 assert_eq!(
1050 editor.display_text(cx),
1051 [
1052 "fn main() {\n",
1053 " if true {\n",
1054 " println!(\"hello\");\n",
1055 " }\n",
1056 "}\n",
1057 "\n",
1058 "fn other() {\n",
1059 " let x = 1;\n",
1060 "}\n",
1061 "\n",
1062 "fn emoji() {\n",
1063 " let a = \"π¦π₯\";\n",
1064 " let b = \"cafΓ©\";\n",
1065 "}\n",
1066 "\n",
1067 "fn outer() {\n",
1068 " fn inner_a() { a }\n",
1069 " fn inner_b() β―\n",
1070 "}\n",
1071 "\n",
1072 "fn newline() {\n",
1073 " let a = 1;\n",
1074 " let b = 2;\n",
1075 "}\n",
1076 ]
1077 .concat(),
1078 );
1079 });
1080 }
1081
1082 #[gpui::test]
1083 async fn test_lsp_folding_ranges_with_multibyte_characters(cx: &mut TestAppContext) {
1084 init_test(cx, |_| {});
1085
1086 update_test_language_settings(cx, &|settings| {
1087 settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On);
1088 });
1089
1090 let mut cx = EditorLspTestContext::new_rust(
1091 lsp::ServerCapabilities {
1092 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
1093 ..lsp::ServerCapabilities::default()
1094 },
1095 cx,
1096 )
1097 .await;
1098
1099 // β is 3 bytes in UTF-8 but 1 code unit in UTF-16.
1100 // LSP character offsets are UTF-16, so interpreting them as byte
1101 // offsets lands inside a multi-byte character and panics.
1102 let mut folding_request = cx
1103 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(
1104 move |_, _, _| async move {
1105 Ok(Some(vec![
1106 // Outer fold: start/end on ASCII-only lines (sanity check).
1107 FoldingRange {
1108 start_line: 0,
1109 start_character: Some(16),
1110 end_line: 8,
1111 end_character: Some(1),
1112 kind: None,
1113 collapsed_text: None,
1114 },
1115 // Inner fold whose start_character falls among multi-byte chars.
1116 // Line 1 is " //ββββββββββ"
1117 // UTF-16 offsets: 0-3=' ', 4='/', 5='/', 6-15='β'Γ10
1118 // Byte offsets: 0-3=' ', 4='/', 5='/', 6..35='β'Γ10 (3 bytes each)
1119 // start_character=8 (UTF-16) β after " //ββ", byte offset would be 12
1120 // but naively using 8 as byte offset hits inside the first 'β'.
1121 FoldingRange {
1122 start_line: 1,
1123 start_character: Some(8),
1124 end_line: 3,
1125 end_character: Some(5),
1126 kind: None,
1127 collapsed_text: None,
1128 },
1129 ]))
1130 },
1131 );
1132
1133 // Line 0: "fn multibyte() {" (16 UTF-16 units)
1134 // Line 1: " //ββββββββββ" (16 UTF-16 units, 36 bytes)
1135 // Line 2: " let y = 2;" (14 UTF-16 units)
1136 // Line 3: " //βββ|end" (13 UTF-16 units; '|' is just a visual marker)
1137 // Line 4: " if true {" (14 UTF-16 units)
1138 // Line 5: " let a = \"ββ\";" (22 UTF-16 units, 28 bytes)
1139 // Line 6: " }" (5 UTF-16 units)
1140 // Line 7: " let z = 3;" (14 UTF-16 units)
1141 // Line 8: "}" (1 UTF-16 unit)
1142 cx.set_state(
1143 &[
1144 "Λfn multibyte() {\n",
1145 " //ββββββββββ\n",
1146 " let y = 2;\n",
1147 " //βββ|end\n",
1148 " if true {\n",
1149 " let a = \"ββ\";\n",
1150 " }\n",
1151 " let z = 3;\n",
1152 "}\n",
1153 ]
1154 .concat(),
1155 );
1156 assert!(folding_request.next().await.is_some());
1157 cx.run_until_parked();
1158
1159 // Fold the inner range whose start_character lands among β chars.
1160 // Fold spans from line 1 char 8 (" //ββ" visible) to line 3 char 5
1161 // ("/βββ|end" visible after fold marker).
1162 cx.update_editor(|editor, window, cx| {
1163 editor.fold_at(MultiBufferRow(1), window, cx);
1164 });
1165 cx.update_editor(|editor, _window, cx| {
1166 assert_eq!(
1167 editor.display_text(cx),
1168 [
1169 "fn multibyte() {\n",
1170 " //βββ―/βββ|end\n",
1171 " if true {\n",
1172 " let a = \"ββ\";\n",
1173 " }\n",
1174 " let z = 3;\n",
1175 "}\n",
1176 ]
1177 .concat(),
1178 );
1179 });
1180
1181 // Unfold, then fold the outer range to make sure it works too.
1182 cx.update_editor(|editor, window, cx| {
1183 editor.unfold_all(&crate::actions::UnfoldAll, window, cx);
1184 });
1185 cx.update_editor(|editor, window, cx| {
1186 editor.fold_at(MultiBufferRow(0), window, cx);
1187 });
1188 cx.update_editor(|editor, _window, cx| {
1189 assert_eq!(editor.display_text(cx), "fn multibyte() {β―\n",);
1190 });
1191 }
1192}
1193