From 735eb4340dfae4001a72589aad1cd39642f4d1f9 Mon Sep 17 00:00:00 2001 From: Ryan Walker Date: Thu, 19 Mar 2026 08:14:31 -0600 Subject: [PATCH] editor: Fix folding for unindented multiline strings and comments (#50049) Closes #5057 ## Summary The indent-based code fold detection does not account for lines inside multiline strings or block comments that have less indentation than the surrounding code. This causes the fold scanner to think the enclosing block ends prematurely. For example, folding `fn main()` here would fail because the raw string content at indent level 0 gets treated as the end of the function body: ``` rust fn main() { let s = r#" unindented content "#; } ``` This PR checks whether a low-indent line falls inside a string or comment override region and skips it if so. This works across all languages that define `@string` or `@comment` overrides in their `overrides.scm`. ## Before https://github.com/user-attachments/assets/a08e6bf8-4f25-4211-8a46-8f6da7e49247 ## After https://github.com/user-attachments/assets/cd5b36db-6d4d-420b-9d60-79f9fad8638e ## Test Plan - Added `test_fold_with_unindented_multiline_raw_string` - Added `test_fold_with_unindented_multiline_block_comment` - All existing fold tests pass - Manually tested both Rust and Python examples Release Notes: - Fixed code folding incorrectly collapsing when multiline strings or block comments contained unindented content --- crates/editor/src/display_map.rs | 13 ++++ crates/editor/src/editor_tests.rs | 102 +++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index b11832faa3f9bb8294c6ea054a335292b1422b02..271e6b0afc56ba8c8a799027d14672d3497c46c6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -2320,6 +2320,19 @@ impl DisplaySnapshot { if !line_indent.is_line_blank() && line_indent.raw_len() <= start_line_indent.raw_len() { + if self + .buffer_snapshot() + .language_scope_at(Point::new(row, 0)) + .is_some_and(|scope| { + matches!( + scope.override_name(), + Some("string") | Some("comment") | Some("comment.inclusive") + ) + }) + { + continue; + } + let prev_row = row - 1; end = Some(Point::new( prev_row, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index fefebb67a6f655c5b81b013c1d447b713e72575d..d813dcc62c38d2c1bb5a121ed6821d0513f655d1 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -26,7 +26,7 @@ use language::{ BracketPairConfig, Capability::ReadWrite, DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig, - LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point, + LanguageConfigOverride, LanguageMatcher, LanguageName, LanguageQueries, Override, Point, language_settings::{ CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode, }, @@ -51,6 +51,7 @@ use settings::{ IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent, SettingsContent, SettingsStore, }; +use std::borrow::Cow; use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant}; use std::{ iter, @@ -1324,6 +1325,105 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_fold_with_unindented_multiline_raw_string(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + overrides: Some(Cow::from(indoc! {" + [ + (string_literal) + (raw_string_literal) + ] @string + [ + (line_comment) + (block_comment) + ] @comment.inclusive + "})), + ..Default::default() + }) + .expect("Could not parse queries"), + ); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + fn main() { + let s = r#\" + a + b + c + \"#; + }ˇ + "}); + + cx.update_editor(|editor, window, cx| { + editor.fold_at_level(&FoldAtLevel(1), window, cx); + assert_eq!( + editor.display_text(cx), + indoc! {" + fn main() {⋯ + } + "}, + ); + }); +} + +#[gpui::test] +async fn test_fold_with_unindented_multiline_block_comment(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + overrides: Some(Cow::from(indoc! {" + [ + (string_literal) + (raw_string_literal) + ] @string + [ + (line_comment) + (block_comment) + ] @comment.inclusive + "})), + ..Default::default() + }) + .expect("Could not parse queries"), + ); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + fn main() { + let x = 1; + /* + unindented comment line + */ + }ˇ + "}); + + cx.update_editor(|editor, window, cx| { + editor.fold_at_level(&FoldAtLevel(1), window, cx); + assert_eq!( + editor.display_text(cx), + indoc! {" + fn main() {⋯ + } + "}, + ); + }); +} + #[gpui::test] fn test_fold_at_level(cx: &mut TestAppContext) { init_test(cx, |_| {});