vim: Respect auto_indent setting in o/O commands (#53620)

David Alecrim created

## Summary

Closes #53570

- `o` and `O` in normal mode were unconditionally copying the current
line's indentation into the new line, ignoring the `auto_indent` setting
entirely
- When `auto_indent: "none"` is set, new lines created by `o`/`O` now
start at column 0 as expected
- When `auto_indent` is `preserve_indent` or `syntax_aware`, behavior is
unchanged

The fix reads `language_settings_at` for the relevant row and splits
edits into two paths: `editor.edit()` (no autoindent) for `None`, and
`editor.edit_with_autoindent()` for everything else — mirroring the
approach already used by the non-vim `Newline` action.

## Test plan

- Added `test_o_auto_indent_none`: verifies `o`/`O` produce column-0
lines with `auto_indent: "none"`, including edge cases (first line,
empty line)
- Added `test_o_preserve_indent`: verifies `o`/`O` copy the current
line's indentation with `auto_indent: "preserve_indent"` (regression
guard)
- Existing neovim-backed tests (`test_o`, `test_insert_line_above`,
`test_o_comment`) continue to pass

Release Notes:

- Fixed vim `o`/`O` commands ignoring the `auto_indent: "none"` setting,
causing new lines to inherit indentation instead of starting at column 0

Change summary

crates/vim/src/normal.rs | 139 ++++++++++++++++++++++++++++++++++-------
1 file changed, 114 insertions(+), 25 deletions(-)

Detailed changes

crates/vim/src/normal.rs 🔗

@@ -29,7 +29,7 @@ use editor::{Anchor, SelectionEffects};
 use editor::{Bias, ToPoint};
 use editor::{display_map::ToDisplayPoint, movement};
 use gpui::{Context, Window, actions};
-use language::{Point, SelectionGoal};
+use language::{AutoIndentMode, Point, SelectionGoal};
 use log::error;
 use multi_buffer::MultiBufferRow;
 
@@ -729,19 +729,35 @@ impl Vim {
                     .into_iter()
                     .map(|selection| selection.start.row)
                     .collect();
-                let edits = selection_start_rows
-                    .into_iter()
-                    .map(|row| {
-                        let indent = snapshot
-                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
-                            .chars()
-                            .collect::<String>();
 
-                        let start_of_line = Point::new(row, 0);
-                        (start_of_line..start_of_line, indent + "\n")
-                    })
-                    .collect::<Vec<_>>();
-                editor.edit_with_autoindent(edits, cx);
+                let mut auto_indent_edits = Vec::new();
+                let mut plain_edits = Vec::new();
+
+                for row in selection_start_rows {
+                    let auto_indent_mode = snapshot
+                        .language_settings_at(Point::new(row, 0), cx)
+                        .auto_indent;
+                    let indent = if auto_indent_mode == AutoIndentMode::None {
+                        String::new()
+                    } else {
+                        snapshot.indent_and_comment_for_line(MultiBufferRow(row), cx)
+                    };
+                    let start_of_line = Point::new(row, 0);
+                    let edit = (start_of_line..start_of_line, indent + "\n");
+                    if auto_indent_mode == AutoIndentMode::None {
+                        plain_edits.push(edit);
+                    } else {
+                        auto_indent_edits.push(edit);
+                    }
+                }
+
+                if !plain_edits.is_empty() {
+                    editor.edit(plain_edits, cx);
+                }
+                if !auto_indent_edits.is_empty() {
+                    editor.edit_with_autoindent(auto_indent_edits, cx);
+                }
+
                 editor.change_selections(Default::default(), window, cx, |s| {
                     s.move_with(&mut |map, selection| {
                         let previous_line = map.start_of_relative_buffer_row(selection.start, -1);
@@ -776,18 +792,28 @@ impl Vim {
                         }
                     })
                     .collect();
-                let edits = selection_end_rows
-                    .into_iter()
-                    .map(|row| {
-                        let indent = snapshot
-                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
-                            .chars()
-                            .collect::<String>();
 
-                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
-                        (end_of_line..end_of_line, "\n".to_string() + &indent)
-                    })
-                    .collect::<Vec<_>>();
+                let mut auto_indent_edits = Vec::new();
+                let mut plain_edits = Vec::new();
+
+                for row in selection_end_rows {
+                    let auto_indent_mode = snapshot
+                        .language_settings_at(Point::new(row, 0), cx)
+                        .auto_indent;
+                    let indent = if auto_indent_mode == AutoIndentMode::None {
+                        String::new()
+                    } else {
+                        snapshot.indent_and_comment_for_line(MultiBufferRow(row), cx)
+                    };
+                    let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
+                    let edit = (end_of_line..end_of_line, "\n".to_string() + &indent);
+                    if auto_indent_mode == AutoIndentMode::None {
+                        plain_edits.push(edit);
+                    } else {
+                        auto_indent_edits.push(edit);
+                    }
+                }
+
                 editor.change_selections(Default::default(), window, cx, |s| {
                     s.move_with(&mut |map, selection| {
                         let current_line = if !selection.is_empty() && selection.end.column() == 0 {
@@ -802,7 +828,13 @@ impl Vim {
                         selection.collapse_to(insert_point, SelectionGoal::None)
                     });
                 });
-                editor.edit_with_autoindent(edits, cx);
+
+                if !plain_edits.is_empty() {
+                    editor.edit(plain_edits, cx);
+                }
+                if !auto_indent_edits.is_empty() {
+                    editor.edit_with_autoindent(auto_indent_edits, cx);
+                }
             });
         });
     }
@@ -1140,6 +1172,7 @@ mod test {
         state::Mode::{self},
         test::{NeovimBackedTestContext, VimTestContext},
     };
+    use language;
 
     #[gpui::test]
     async fn test_h(cx: &mut gpui::TestAppContext) {
@@ -2099,6 +2132,62 @@ mod test {
         cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
     }
 
+    #[gpui::test]
+    async fn test_o_auto_indent_none(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings(cx, |s| {
+                s.project.all_languages.defaults.auto_indent = Some(language::AutoIndentMode::None);
+            });
+        });
+
+        // o: new line below starts at column 0 regardless of current indentation
+        cx.set_state("    let xˇ = 1;", Mode::Normal);
+        cx.simulate_keystrokes("o");
+        cx.assert_state("    let x = 1;\nˇ", Mode::Insert);
+
+        // O: new line above starts at column 0 regardless of current indentation
+        cx.set_state("    let xˇ = 1;", Mode::Normal);
+        cx.simulate_keystrokes("shift-o");
+        cx.assert_state("ˇ\n    let x = 1;", Mode::Insert);
+
+        // o on the first line: no crash and column 0
+        cx.set_state("ˇfoo", Mode::Normal);
+        cx.simulate_keystrokes("o");
+        cx.assert_state("foo\nˇ", Mode::Insert);
+
+        // O on the first line: no crash and column 0
+        cx.set_state("ˇfoo", Mode::Normal);
+        cx.simulate_keystrokes("shift-o");
+        cx.assert_state("ˇ\nfoo", Mode::Insert);
+
+        // o on an already-empty line: stays at column 0
+        cx.set_state("fooˇ\n\nbar", Mode::Normal);
+        cx.simulate_keystrokes("j o");
+        cx.assert_state("foo\n\nˇ\nbar", Mode::Insert);
+    }
+
+    #[gpui::test]
+    async fn test_o_preserve_indent(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings(cx, |s| {
+                s.project.all_languages.defaults.auto_indent =
+                    Some(language::AutoIndentMode::PreserveIndent);
+            });
+        });
+
+        // o: new line below copies current line's indentation
+        cx.set_state("    let xˇ = 1;", Mode::Normal);
+        cx.simulate_keystrokes("o");
+        cx.assert_state("    let x = 1;\n    ˇ", Mode::Insert);
+
+        // O: new line above copies current line's indentation
+        cx.set_state("    let xˇ = 1;", Mode::Normal);
+        cx.simulate_keystrokes("shift-o");
+        cx.assert_state("    ˇ\n    let x = 1;", Mode::Insert);
+    }
+
     #[gpui::test]
     async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;