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