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