fix: preserve Visual selections through command entry

This commit is contained in:
Valdemar Lorenzen
2026-10-16 13:59:25 +02:00
parent 0446ec86c7
commit d67b874b87
6 changed files with 93 additions and 18 deletions
+82 -13
View File
@@ -508,7 +508,11 @@ core_actions![
Action::RepeatLastEdit,
&[Mode::Normal, Mode::Visual],
),
("core.command", Action::Command, &[Mode::Normal]),
(
"core.command",
Action::Command,
&[Mode::Normal, Mode::Visual]
),
("core.search", Action::Search, &[Mode::Normal]),
("core.search_word", Action::SearchWord, &[Mode::Normal]),
(
@@ -1350,7 +1354,6 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
('s', SubstituteCharacter),
('u', Undo),
('U', Redo),
(':', Command),
('/', Search),
('*', SearchWord),
('p', PasteAfter),
@@ -1382,6 +1385,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
}
for mode in [Mode::Normal, Mode::Visual] {
for (key, action) in [
(':', Command),
('h', Left),
('j', Down),
('k', Up),
@@ -1670,6 +1674,7 @@ struct ViewState {
search_highlight: bool,
search_match: Option<SearchMatch>,
command_line: String,
command_from_visual: bool,
caret_reveal_revision: u64,
lsp_center: Cell<Option<(u64, u64, Cursor)>>,
viewport_scroll: Cell<(isize, isize)>,
@@ -1691,6 +1696,7 @@ impl ViewState {
search_highlight: false,
search_match: None,
command_line: String::new(),
command_from_visual: false,
caret_reveal_revision: 0,
lsp_center: Cell::new(None),
viewport_scroll: Cell::new((0, 0)),
@@ -1712,6 +1718,7 @@ impl ViewState {
search_highlight: self.search_highlight,
search_match: self.search_match,
command_line: self.command_line.clone(),
command_from_visual: self.command_from_visual,
caret_reveal_revision: self.caret_reveal_revision,
visual_motions: self.visual_motions.clone(),
..Self::empty()
@@ -7611,6 +7618,7 @@ impl Editor {
self.pane_close_picker_origin = None;
self.active.view.command_line = format!("{} ", entry.name);
self.palette = None;
self.active.view.command_from_visual = self.active.view.mode == Mode::Visual;
if self.diff.is_some() && !self.diff_interacting() {
self.diff_prompt_mode.get_or_insert(self.active.view.mode);
} else if self.active.view.mode == Mode::Insert {
@@ -8239,14 +8247,19 @@ impl Editor {
None
}
fn finish_diff_prompt(&mut self) {
fn finish_prompt(&mut self) {
if let Some(mode) = self.diff_prompt_mode.take() {
self.active.view.mode = mode;
self.active.view.command_line.clear();
}
if std::mem::take(&mut self.active.view.command_from_visual) {
self.active.view.mode = Mode::Visual;
self.active.view.command_line.clear();
}
}
fn open_action_prompt(&mut self, entry: &CoreAction) {
self.active.view.command_from_visual = self.active.view.mode == Mode::Visual;
if self.diff().is_some() && !self.diff_interacting() {
self.diff_prompt_mode.get_or_insert(self.active.view.mode);
self.palette = None;
@@ -8268,8 +8281,10 @@ impl Editor {
{
self.pane_close_picker_origin = None;
}
if action == Action::Normal && self.diff_prompt_mode.is_some() {
self.finish_diff_prompt();
if action == Action::Normal
&& (self.diff_prompt_mode.is_some() || self.active.view.command_from_visual)
{
self.finish_prompt();
self.count = None;
self.clear_sequence();
return None;
@@ -8544,6 +8559,8 @@ impl Editor {
Action::Redo => self.redo(),
Action::RepeatLastEdit => self.repeat_last_edit(),
Action::Command | Action::Search => {
self.active.view.command_from_visual =
action == Action::Command && self.active.view.mode == Mode::Visual;
if self.diff().is_some() && !self.diff_interacting() {
self.diff_prompt_mode.get_or_insert(self.active.view.mode);
}
@@ -8978,6 +8995,7 @@ impl Editor {
self.normalize_targets();
}
self.active.view.command_line.clear();
self.active.view.command_from_visual = false;
self.count = None;
self.clear_sequence();
self.palette = None;
@@ -9252,7 +9270,7 @@ impl Editor {
pub fn execute(&mut self, command: &str) -> Option<Invocation> {
self.pending_lsp_navigation = None;
self.finish_diff_prompt();
self.finish_prompt();
let invocation = self.execute_command(command);
self.leave_diff_on_navigation();
self.refresh_live_diff();
@@ -9278,7 +9296,9 @@ impl Editor {
self.active.view.command_line.clear();
} else {
self.prepare_command();
self.finish_command_mode();
if self.active.view.mode != Mode::Visual {
self.finish_command_mode();
}
}
let Some(feature) = self.owner(name) else {
self.set_message(format!("Unknown command: {name}"));
@@ -9302,6 +9322,7 @@ impl Editor {
}
fn finish_command_mode(&mut self) {
self.active.view.command_from_visual = false;
if self.active.view.mode == Mode::Insert {
self.finish_insert();
}
@@ -9324,7 +9345,7 @@ impl Editor {
| Action::ReviewSave
| Action::ReviewOpen
) {
self.finish_diff_prompt();
self.finish_prompt();
if let Err(error) = self.local_review_command(entry.action, args) {
self.set_message(error);
}
@@ -9408,7 +9429,7 @@ impl Editor {
if !matches!(entry.action, Action::PaneClose | Action::PaneForceClose) {
self.pane_close_picker_origin = None;
}
self.finish_diff_prompt();
self.finish_prompt();
if matches!(
entry.action,
Action::PaneSplitHorizontal
@@ -15928,9 +15949,53 @@ mod tests {
}
#[test]
fn local_review_visual_palette_anchors_both_sides_and_preserves_stale_ranges() {
fn visual_commands_preserve_directed_ranges_on_cancel_and_plugin_dispatch() {
for shape in ["vll", "vhh", "Vj"] {
let mut editor = editor();
editor
.apply(vec![Effect::Replace(
"abcdef\nsecond\nabcdef\nfourth".into(),
)])
.unwrap();
editor.register(plugin("inspect", &["inspect"])).unwrap();
set_carets(&mut editor, &[(0, 2), (2, 2)], 1);
type_text(&mut editor, shape);
let selections = editor.selections();
let carets = editor.carets();
let linewise = editor.active.view.linewise;
type_text(&mut editor, ":inspect");
assert_eq!(editor.mode(), Mode::Command);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.carets(), carets);
assert_eq!(editor.active.view.linewise, linewise);
editor
.bind("visual", ",", Some("core.command".into()))
.unwrap();
editor.handle(Input::Char(','));
type_text(&mut editor, "inspect");
editor
.handle(Input::Enter)
.expect("selection-aware plugin invocation");
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.carets(), carets);
assert_eq!(editor.active.view.linewise, linewise);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Normal);
}
}
#[test]
fn local_review_visual_commands_and_palette_preserve_both_side_ranges() {
use crate::review::Side;
for focus in [Focus::Before, Focus::After] {
for (focus, palette) in [
(Focus::Before, false),
(Focus::After, false),
(Focus::Before, true),
(Focus::After, true),
] {
let directory = Directory::new();
let root = directory.0.canonicalize().unwrap();
let mut editor = editor();
@@ -15943,8 +16008,12 @@ mod tests {
editor.diff.as_mut().unwrap().focus = focus;
editor.bind_diff_buffer();
type_text(&mut editor, "Vj");
editor.handle(Input::Ctrl('p'));
type_text(&mut editor, "diff.comment");
if palette {
editor.handle(Input::Ctrl('p'));
type_text(&mut editor, "diff.comment");
} else {
type_text(&mut editor, ":diff.comment");
}
editor.handle(Input::Enter);
let diff = editor.workspace_diff().unwrap();
let anchor = diff.local_review.document.comments[0].anchor.clone();