folding_ranges.rs

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