From 72c1d973ec582f161ee34641e8d60fb362359d45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Soares?=
<37777652+Dnreikronos@users.noreply.github.com>
Date: Tue, 24 Mar 2026 13:08:43 -0300
Subject: [PATCH] editor: Include closing delimiter on same line when folding
(#50090)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CLOSES: #50002
When using indent-based folding (the default, `document_folding_ranges =
Off`),
collapsed folds in brace-delimited languages displayed the closing
delimiter on
a separate line:
fn b() {⋯
}
This extends the fold range in `crease_for_buffer_row` to include the
trailing
newline and leading whitespace before a closing `}`, `)`, or `]`,
producing
the expected single-line display:
fn b() {⋯}
Whitespace-sensitive languages like Python are unaffected — their
terminating
lines don't start with a closing delimiter, so the existing behavior is
preserved.
Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Screenshots:
Before:
After:
Release Notes:
- Fixed indent-based code folding to display the closing delimiter (`}`,
`)`, `]`) on the same line as the fold placeholder instead of on a
separate line
([#50002](https://github.com/zed-industries/zed/issues/50002)).
---
crates/editor/src/display_map.rs | 64 ++++++++----
crates/editor/src/editor_tests.rs | 152 +++++++++++++++++++++++++++-
crates/editor/src/folding_ranges.rs | 4 +-
crates/language/src/language.rs | 40 ++++++++
4 files changed, 234 insertions(+), 26 deletions(-)
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()),