editor: Fix inconsistent relative indent when using tab with multi cursors (#29519)

Smit Barmase created

Do not insert hard/soft tabs for cursors at the suggested indent level
if any other cursor lies before the suggested indent level. This PR
brings us one step closer to fixing
https://github.com/zed-industries/zed/issues/26157.

Before:


https://github.com/user-attachments/assets/8fd5cde4-99f4-4363-9292-5da8dadab658

After:


https://github.com/user-attachments/assets/17c9f8ca-5842-452b-8665-7c7138d50162

Release Notes:

- Fixed an issue where using tab with multiple cursors would result in
inconsistent relative indentation across lines.

Change summary

crates/editor/src/editor.rs       | 18 +++++++++++++++
crates/editor/src/editor_tests.rs | 39 +++++++++++++++++++++++++++++---
2 files changed, 53 insertions(+), 4 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -8620,6 +8620,15 @@ impl Editor {
         let rows_iter = selections.iter().map(|s| s.head().row);
         let suggested_indents = snapshot.suggested_indents(rows_iter, cx);
 
+        let has_some_cursor_in_whitespace = selections
+            .iter()
+            .filter(|selection| selection.is_empty())
+            .any(|selection| {
+                let cursor = selection.head();
+                let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row));
+                cursor.column < current_indent.len
+            });
+
         let mut edits = Vec::new();
         let mut prev_edited_row = 0;
         let mut row_delta = 0;
@@ -8643,6 +8652,15 @@ impl Editor {
             if let Some(suggested_indent) =
                 suggested_indents.get(&MultiBufferRow(cursor.row)).copied()
             {
+                // If there exist any empty selection in the leading whitespace, then skip
+                // indent for selections at the boundary.
+                if has_some_cursor_in_whitespace
+                    && cursor.column == current_indent.len
+                    && current_indent.len == suggested_indent.len
+                {
+                    continue;
+                }
+
                 if cursor.column < suggested_indent.len
                     && cursor.column <= current_indent.len
                     && current_indent.len <= suggested_indent.len

crates/editor/src/editor_tests.rs 🔗

@@ -2870,9 +2870,26 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
     );
     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
 
-    // cursors that are already at the suggested indent level insert
-    // a soft tab. cursors that are to the left of the suggested indent
-    // auto-indent their line.
+    // when all cursors are to the left of the suggested indent, then auto-indent all.
+    cx.set_state(indoc! {"
+        const a: B = (
+            c(
+        ˇ
+        ˇ    )
+        );
+    "});
+    cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(
+                ˇ
+            ˇ)
+        );
+    "});
+
+    // cursors that are already at the suggested indent level do not move
+    // until other cursors that are to the left of the suggested indent
+    // auto-indent.
     cx.set_state(indoc! {"
         ˇ
         const a: B = (
@@ -2886,7 +2903,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
     "});
     cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
     cx.assert_editor_state(indoc! {"
-            ˇ
+        ˇ
         const a: B = (
             c(
                 d(
@@ -2896,6 +2913,20 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
             ˇ)
         );
     "});
+    // once all multi-cursors are at the suggested
+    // indent level, they all insert a soft tab together.
+    cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+    cx.assert_editor_state(indoc! {"
+            ˇ
+        const a: B = (
+            c(
+                d(
+                        ˇ
+                )
+                    ˇ
+                ˇ)
+        );
+    "});
 
     // handle auto-indent when there are multiple cursors on the same line
     cx.set_state(indoc! {"