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

## 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
This commit is contained in:
David Alecrim authored and GitHub committed 2026-04-22 16:40:49 +00:00
1 parent d82b039e69
commit 58e4ec9c76
1 file changed
+114 -25
+114 -25
View File
@@ -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;