diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 271e6b0afc56ba8c8a799027d14672d3497c46c6..c5018abe598e7a20eaccf7c20f68a5f973f71436 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -2269,6 +2269,29 @@ impl DisplaySnapshot { .unwrap_or(false) } + /// Returns the indent length of `row` if it starts with a closing bracket. + fn closing_bracket_indent_len(&self, row: u32) -> Option { + let snapshot = self.buffer_snapshot(); + let indent_len = self + .line_indent_for_buffer_row(MultiBufferRow(row)) + .raw_len(); + let content_start = Point::new(row, indent_len); + let line_text: String = snapshot + .chars_at(content_start) + .take_while(|ch| *ch != '\n') + .collect(); + + let scope = snapshot.language_scope_at(Point::new(row, 0))?; + if scope + .brackets() + .any(|(pair, _)| line_text.starts_with(&pair.end)) + { + return Some(indent_len); + } + + None + } + #[instrument(skip_all)] pub fn crease_for_buffer_row(&self, buffer_row: MultiBufferRow) -> Option> { let start = @@ -2313,7 +2336,7 @@ impl DisplaySnapshot { { let start_line_indent = self.line_indent_for_buffer_row(buffer_row); let max_point = self.buffer_snapshot().max_point(); - let mut end = None; + let mut closing_row = None; for row in (buffer_row.0 + 1)..=max_point.row { let line_indent = self.line_indent_for_buffer_row(MultiBufferRow(row)); @@ -2333,32 +2356,33 @@ impl DisplaySnapshot { continue; } - let prev_row = row - 1; - end = Some(Point::new( - prev_row, - self.buffer_snapshot().line_len(MultiBufferRow(prev_row)), - )); + closing_row = Some(row); break; } } - let mut row_before_line_breaks = end.unwrap_or(max_point); - while row_before_line_breaks.row > start.row - && self - .buffer_snapshot() - .is_line_blank(MultiBufferRow(row_before_line_breaks.row)) - { - row_before_line_breaks.row -= 1; - } + let last_non_blank_row = |from_row: u32| -> Point { + let mut row = from_row; + while row > start.row && self.buffer_snapshot().is_line_blank(MultiBufferRow(row)) { + row -= 1; + } + Point::new(row, self.buffer_snapshot().line_len(MultiBufferRow(row))) + }; - row_before_line_breaks = Point::new( - row_before_line_breaks.row, - self.buffer_snapshot() - .line_len(MultiBufferRow(row_before_line_breaks.row)), - ); + let end = if let Some(row) = closing_row { + if let Some(indent_len) = self.closing_bracket_indent_len(row) { + // Include newline and whitespace before closing delimiter, + // so it appears on the same display line as the fold placeholder + Point::new(row, indent_len) + } else { + last_non_blank_row(row - 1) + } + } else { + last_non_blank_row(max_point.row) + }; Some(Crease::Inline { - range: start..row_before_line_breaks, + range: start..end, placeholder: self.fold_placeholder.clone(), render_toggle: None, render_trailer: None, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5360757ffe42c5c8d85dd1e8632c7bca62f467a8..e8ae74c5a42b1021d8feae851ed8af4d67710a4c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -23,7 +23,7 @@ use gpui::{ }; use indoc::indoc; use language::{ - BracketPairConfig, + BracketPair, BracketPairConfig, Capability::ReadWrite, DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, LanguageQueries, Override, Point, @@ -1121,7 +1121,93 @@ fn test_cancel(cx: &mut TestAppContext) { } #[gpui::test] -fn test_fold_action(cx: &mut TestAppContext) { +async fn test_fold_action(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx)); + cx.set_state(indoc! {" + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() { + 2 + } + + fn c() { + 3 + } + }ˇ + "}); + + cx.update_editor(|editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0) + ]); + }); + editor.fold(&Fold, window, cx); + assert_eq!( + editor.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯} + + fn c() {⋯} + } + " + .unindent(), + ); + + editor.fold(&Fold, window, cx); + assert_eq!( + editor.display_text(cx), + " + impl Foo {⋯} + " + .unindent(), + ); + + editor.unfold_lines(&UnfoldLines, window, cx); + assert_eq!( + editor.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯} + + fn c() {⋯} + } + " + .unindent(), + ); + + editor.unfold_lines(&UnfoldLines, window, cx); + assert_eq!( + editor.display_text(cx), + editor.buffer.read(cx).read(cx).text() + ); + }); +} + +#[gpui::test] +fn test_fold_action_without_language(cx: &mut TestAppContext) { init_test(cx, |_| {}); let editor = cx.add_window(|window, cx| { @@ -1440,6 +1526,36 @@ async fn test_fold_with_unindented_multiline_raw_string(cx: &mut TestAppContext) }); } +#[gpui::test] +async fn test_fold_with_unindented_multiline_raw_string_includes_closing_bracket( + cx: &mut TestAppContext, +) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), 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, |_| {}); @@ -1489,6 +1605,35 @@ async fn test_fold_with_unindented_multiline_block_comment(cx: &mut TestAppConte }); } +#[gpui::test] +async fn test_fold_with_unindented_multiline_block_comment_includes_closing_bracket( + cx: &mut TestAppContext, +) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), 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, |_| {}); @@ -23761,8 +23906,7 @@ async fn test_indent_guide_with_folds(cx: &mut TestAppContext) { " fn main() { if a { - b(⋯ - ) + b(⋯) } else { e( f diff --git a/crates/editor/src/folding_ranges.rs b/crates/editor/src/folding_ranges.rs index 745fdcbe30a0aede4f364afd5c58958c74b3da79..c4113f0504430b2926bb3c7858226afd2ff7bb40 100644 --- a/crates/editor/src/folding_ranges.rs +++ b/crates/editor/src/folding_ranges.rs @@ -538,7 +538,7 @@ mod tests { snapshot.is_line_folded(MultiBufferRow(0)), "Indentation-based fold should work on the function" ); - assert_eq!(editor.display_text(cx), "fn main() {⋯\n}\n",); + assert_eq!(editor.display_text(cx), "fn main() {⋯}\n",); }); cx.update_editor(|editor, window, cx| { @@ -666,7 +666,7 @@ mod tests { snapshot.is_line_folded(MultiBufferRow(0)), "Indentation-based fold should work again after switching back" ); - assert_eq!(editor.display_text(cx), "fn main() {⋯\n}\n",); + assert_eq!(editor.display_text(cx), "fn main() {⋯}\n",); }); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 4e994a7e60f58b6e4ccd50c2cb0584f91bd351f2..bdee0d9dd68c96eab47278a01f3e475548c16336 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2801,6 +2801,46 @@ pub fn rust_lang() -> Arc { ..Default::default() }, line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + surround: false, + newline: true, + }, + BracketPair { + start: "[".into(), + end: "]".into(), + close: true, + surround: false, + newline: true, + }, + BracketPair { + start: "(".into(), + end: ")".into(), + close: true, + surround: false, + newline: true, + }, + BracketPair { + start: "<".into(), + end: ">".into(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "\"".into(), + end: "\"".into(), + close: true, + surround: false, + newline: false, + }, + ], + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()),