Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:34:12.587Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches Β· 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

folding_ranges.rs

1193 lines Β· 44.2 KB Β· rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.