From bcf4c71fdbfefcb853fd3e64cb8d1ae3daf90821 Mon Sep 17 00:00:00 2001 From: David Alecrim <35930364+davidalecrim1@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:40:49 -0300 Subject: [PATCH] vim: Respect auto_indent setting in o/O commands (#53620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- crates/vim/src/normal.rs | 139 ++++++++++++++++++++++++++++++++------- 1 file changed, 114 insertions(+), 25 deletions(-) diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 8bdd4e7ef50feaefc77d5b3ccc9e7bb13ef6c674..1d0d0812e82899898a1fe12eb5a97ea6d85a401f 100644 --- a/crates/vim/src/normal.rs +++ b/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::(); - let start_of_line = Point::new(row, 0); - (start_of_line..start_of_line, indent + "\n") - }) - .collect::>(); - 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::(); - let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row))); - (end_of_line..end_of_line, "\n".to_string() + &indent) - }) - .collect::>(); + 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;