From 2051578819cff18ae6c4f2e2191724a00e5f3abd Mon Sep 17 00:00:00 2001
From: Kurian Jojo <67583328+polyesterswing@users.noreply.github.com>
Date: Fri, 20 Mar 2026 01:12:30 +0530
Subject: [PATCH] editor: Fix rewrapping with an empty selection (#51742)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #43043
Rewrapping logic for when there was nothing selected was desynced from
the code that handles selections.
The desired wrapping can be achieved when you selected the markdown
paragraph and use the rewrap action.
This PR moves the logic that selects lines for the case where there is
no selection further up to reuse the existing rewrapping logic
- [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)
Release Notes:
- Fixed rewrapping paragraphs in markdown when you have nothing selected
---------
Co-authored-by: Tom Houlé
Co-authored-by: Antonio Scandurra
Co-authored-by: Anthony Eid
---
crates/editor/src/editor.rs | 116 +++++++++++++++++-------------
crates/editor/src/editor_tests.rs | 42 +++++++++++
2 files changed, 109 insertions(+), 49 deletions(-)
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 761f732a11f31ddd59922c364478f9912843891b..ba7870efcb43a36442060654118bd228c0c0055c 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -13392,16 +13392,6 @@ impl Editor {
// Split selections to respect paragraph, indent, and comment prefix boundaries.
let wrap_ranges = selections.into_iter().flat_map(|selection| {
- let mut non_blank_rows_iter = (selection.start.row..=selection.end.row)
- .filter(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- .peekable();
-
- let first_row = if let Some(&row) = non_blank_rows_iter.peek() {
- row
- } else {
- return Vec::new();
- };
-
let language_settings = buffer.language_settings_at(selection.head(), cx);
let language_scope = buffer.language_scope_at(selection.head());
@@ -13478,8 +13468,70 @@ impl Editor {
(indent, comment_prefix, rewrap_prefix)
};
+ let mut start_row = selection.start.row;
+ let mut end_row = selection.end.row;
+
+ if selection.is_empty() {
+ let cursor_row = selection.start.row;
+
+ let (mut indent_size, comment_prefix, _) = indent_and_prefix_for_row(cursor_row);
+ let line_prefix = match &comment_prefix {
+ Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => {
+ Some(prefix.as_str())
+ }
+ Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig {
+ prefix, ..
+ })) => Some(prefix.as_ref()),
+ Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig {
+ start: _,
+ end: _,
+ prefix,
+ tab_size,
+ })) => {
+ indent_size.len += tab_size;
+ Some(prefix.as_ref())
+ }
+ None => None,
+ };
+ let indent_prefix = indent_size.chars().collect::();
+ let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or(""));
+
+ 'expand_upwards: while start_row > 0 {
+ let prev_row = start_row - 1;
+ if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix)
+ && buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len()
+ && !buffer.is_line_blank(MultiBufferRow(prev_row))
+ {
+ start_row = prev_row;
+ } else {
+ break 'expand_upwards;
+ }
+ }
+
+ 'expand_downwards: while end_row < buffer.max_point().row {
+ let next_row = end_row + 1;
+ if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix)
+ && buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len()
+ && !buffer.is_line_blank(MultiBufferRow(next_row))
+ {
+ end_row = next_row;
+ } else {
+ break 'expand_downwards;
+ }
+ }
+ }
+
+ let mut non_blank_rows_iter = (start_row..=end_row)
+ .filter(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
+ .peekable();
+
+ let first_row = if let Some(&row) = non_blank_rows_iter.peek() {
+ row
+ } else {
+ return Vec::new();
+ };
+
let mut ranges = Vec::new();
- let from_empty_selection = selection.is_empty();
let mut current_range_start = first_row;
let mut prev_row = first_row;
@@ -13510,7 +13562,6 @@ impl Editor {
current_range_indent,
current_range_comment_delimiters.clone(),
current_range_rewrap_prefix.clone(),
- from_empty_selection,
));
current_range_start = row;
current_range_indent = row_indent;
@@ -13527,7 +13578,6 @@ impl Editor {
current_range_indent,
current_range_comment_delimiters,
current_range_rewrap_prefix,
- from_empty_selection,
));
ranges
@@ -13536,17 +13586,11 @@ impl Editor {
let mut edits = Vec::new();
let mut rewrapped_row_ranges = Vec::>::new();
- for (
- language_settings,
- wrap_range,
- mut indent_size,
- comment_prefix,
- rewrap_prefix,
- from_empty_selection,
- ) in wrap_ranges
+ for (language_settings, wrap_range, mut indent_size, comment_prefix, rewrap_prefix) in
+ wrap_ranges
{
- let mut start_row = wrap_range.start.row;
- let mut end_row = wrap_range.end.row;
+ let start_row = wrap_range.start.row;
+ let end_row = wrap_range.end.row;
// Skip selections that overlap with a range that has already been rewrapped.
let selection_range = start_row..end_row;
@@ -13593,32 +13637,6 @@ impl Editor {
continue;
}
- if from_empty_selection {
- 'expand_upwards: while start_row > 0 {
- let prev_row = start_row - 1;
- if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix)
- && buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len()
- && !buffer.is_line_blank(MultiBufferRow(prev_row))
- {
- start_row = prev_row;
- } else {
- break 'expand_upwards;
- }
- }
-
- 'expand_downwards: while end_row < buffer.max_point().row {
- let next_row = end_row + 1;
- if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix)
- && buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len()
- && !buffer.is_line_blank(MultiBufferRow(next_row))
- {
- end_row = next_row;
- } else {
- break 'expand_downwards;
- }
- }
- }
-
let start = Point::new(start_row, 0);
let start_offset = ToOffset::to_offset(&start, &buffer);
let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row)));
diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs
index 4359b49552b6e3a51dfb288131efeab46d8874ed..5fd5d2ed75cb1a5c8940f8b8765304e0641aabb2 100644
--- a/crates/editor/src/editor_tests.rs
+++ b/crates/editor/src/editor_tests.rs
@@ -7462,6 +7462,48 @@ async fn test_rewrap(cx: &mut TestAppContext) {
also very long and should not merge
with the numbered item.ˇ»
"},
+ markdown_language.clone(),
+ &mut cx,
+ );
+
+ // Test that empty selection rewrap on a numbered list item does not merge adjacent items
+ assert_rewrap(
+ indoc! {"
+ 1. This is the first numbered list item that is very long and needs to be wrapped properly.
+ 2. ˇThis is the second numbered list item that is also very long and needs to be wrapped.
+ 3. This is the third numbered list item, shorter.
+ "},
+ indoc! {"
+ 1. This is the first numbered list item
+ that is very long and needs to be
+ wrapped properly.
+ 2. ˇThis is the second numbered list item
+ that is also very long and needs to
+ be wrapped.
+ 3. This is the third numbered list item,
+ shorter.
+ "},
+ markdown_language.clone(),
+ &mut cx,
+ );
+
+ // Test that empty selection rewrap on a bullet list item does not merge adjacent items
+ assert_rewrap(
+ indoc! {"
+ - This is the first bullet item that is very long and needs wrapping properly here.
+ - ˇThis is the second bullet item that is also very long and needs to be wrapped.
+ - This is the third bullet item, shorter.
+ "},
+ indoc! {"
+ - This is the first bullet item that is
+ very long and needs wrapping properly
+ here.
+ - ˇThis is the second bullet item that is
+ also very long and needs to be
+ wrapped.
+ - This is the third bullet item,
+ shorter.
+ "},
markdown_language,
&mut cx,
);