folding_ranges.rs

   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}