From ffd1083cc185ddd13760391dc81231c18236e912 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 27 Sep 2024 10:06:19 -0600 Subject: [PATCH] vim: Command selection fixes (#18424) Release Notes: - vim: Fixed cursor position after `:{range}yank`. - vim: Added `:fo[ld]`, `:foldo[pen]` and `:foldc[lose]` --- crates/vim/src/command.rs | 260 +++++++++++++++++++++++----------- crates/vim/src/indent.rs | 8 +- crates/vim/src/normal.rs | 6 +- crates/vim/src/normal/yank.rs | 32 ++++- crates/vim/src/visual.rs | 14 +- 5 files changed, 218 insertions(+), 102 deletions(-) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 49e739faadfeaf6fc008f429afbb11eddf068b90..605bc3a05e43c84e00bfb37a0606c4f7180b1289 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1,4 +1,9 @@ -use std::{iter::Peekable, ops::Range, str::Chars, sync::OnceLock}; +use std::{ + iter::Peekable, + ops::{Deref, Range}, + str::Chars, + sync::OnceLock, +}; use anyhow::{anyhow, Result}; use command_palette_hooks::CommandInterceptResult; @@ -21,7 +26,7 @@ use crate::{ JoinLines, }, state::Mode, - visual::{VisualDeleteLine, VisualYankLine}, + visual::VisualDeleteLine, Vim, }; @@ -30,38 +35,55 @@ pub struct GoToLine { range: CommandRange, } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct YankCommand { + range: CommandRange, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] pub struct WithRange { - is_count: bool, + restore_selection: bool, range: CommandRange, - action: Box, + action: WrappedAction, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct WithCount { + count: u32, + action: WrappedAction, } +#[derive(Debug)] +struct WrappedAction(Box); + actions!(vim, [VisualCommand, CountCommand]); -impl_actions!(vim, [GoToLine, WithRange]); +impl_actions!(vim, [GoToLine, YankCommand, WithRange, WithCount]); -impl<'de> Deserialize<'de> for WithRange { +impl<'de> Deserialize<'de> for WrappedAction { fn deserialize(_: D) -> Result where D: serde::Deserializer<'de>, { - Err(serde::de::Error::custom("Cannot deserialize WithRange")) + Err(serde::de::Error::custom("Cannot deserialize WrappedAction")) } } -impl PartialEq for WithRange { +impl PartialEq for WrappedAction { fn eq(&self, other: &Self) -> bool { - self.range == other.range && self.action.partial_eq(&*other.action) + self.0.partial_eq(&*other.0) } } -impl Clone for WithRange { +impl Clone for WrappedAction { fn clone(&self) -> Self { - Self { - is_count: self.is_count, - range: self.range.clone(), - action: self.action.boxed_clone(), - } + Self(self.0.boxed_clone()) + } +} + +impl Deref for WrappedAction { + type Target = dyn Action; + fn deref(&self) -> &dyn Action { + &*self.0 } } @@ -110,13 +132,33 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { vim.move_cursor(Motion::StartOfDocument, Some(buffer_row.0 as usize + 1), cx); }); - Vim::action(editor, cx, |vim, action: &WithRange, cx| { - if action.is_count { - for _ in 0..action.range.as_count() { - cx.dispatch_action(action.action.boxed_clone()) + Vim::action(editor, cx, |vim, action: &YankCommand, cx| { + vim.update_editor(cx, |vim, editor, cx| { + let snapshot = editor.snapshot(cx); + if let Ok(range) = action.range.buffer_range(vim, editor, cx) { + let end = if range.end < snapshot.max_buffer_row() { + Point::new(range.end.0 + 1, 0) + } else { + snapshot.buffer_snapshot.max_point() + }; + vim.copy_ranges( + editor, + true, + true, + vec![Point::new(range.start.0, 0)..end], + cx, + ) } - return; + }); + }); + + Vim::action(editor, cx, |_, action: &WithCount, cx| { + for _ in 0..action.count { + cx.dispatch_action(action.action.boxed_clone()) } + }); + + Vim::action(editor, cx, |vim, action: &WithRange, cx| { let result = vim.update_editor(cx, |vim, editor, cx| { action.range.buffer_range(vim, editor, cx) }); @@ -134,31 +176,51 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { } Some(Ok(result)) => result, }; - vim.update_editor(cx, |_, editor, cx| { - editor.change_selections(None, cx, |s| { - let end = Point::new(range.end.0, s.buffer().line_len(range.end)); - s.select_ranges([end..Point::new(range.start.0, 0)]); + + let previous_selections = vim + .update_editor(cx, |_, editor, cx| { + let selections = action + .restore_selection + .then(|| editor.selections.disjoint_anchor_ranges()); + editor.change_selections(None, cx, |s| { + let end = Point::new(range.end.0, s.buffer().line_len(range.end)); + s.select_ranges([end..Point::new(range.start.0, 0)]); + }); + selections }) - }); + .flatten(); cx.dispatch_action(action.action.boxed_clone()); cx.defer(move |vim, cx| { vim.update_editor(cx, |_, editor, cx| { editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(range.start.0, 0)..Point::new(range.start.0, 0)]); + if let Some(previous_selections) = previous_selections { + s.select_ranges(previous_selections); + } else { + s.select_ranges([ + Point::new(range.start.0, 0)..Point::new(range.start.0, 0) + ]); + } }) }); }); }); } -#[derive(Debug, Default)] +#[derive(Default)] struct VimCommand { prefix: &'static str, suffix: &'static str, action: Option>, action_name: Option<&'static str>, bang_action: Option>, - has_range: bool, + range: Option< + Box< + dyn Fn(Box, &CommandRange) -> Option> + + Send + + Sync + + 'static, + >, + >, has_count: bool, } @@ -187,16 +249,25 @@ impl VimCommand { self } - fn range(mut self) -> Self { - self.has_range = true; + fn range( + mut self, + f: impl Fn(Box, &CommandRange) -> Option> + Send + Sync + 'static, + ) -> Self { + self.range = Some(Box::new(f)); self } + fn count(mut self) -> Self { self.has_count = true; self } - fn parse(&self, mut query: &str, cx: &AppContext) -> Option> { + fn parse( + &self, + mut query: &str, + range: &Option, + cx: &AppContext, + ) -> Option> { let has_bang = query.ends_with('!'); if has_bang { query = &query[..query.len() - 1]; @@ -207,14 +278,20 @@ impl VimCommand { return None; } - if has_bang && self.bang_action.is_some() { - Some(self.bang_action.as_ref().unwrap().boxed_clone()) + let action = if has_bang && self.bang_action.is_some() { + self.bang_action.as_ref().unwrap().boxed_clone() } else if let Some(action) = self.action.as_ref() { - Some(action.boxed_clone()) + action.boxed_clone() } else if let Some(action_name) = self.action_name { - cx.build_action(action_name, None).log_err() + cx.build_action(action_name, None).log_err()? } else { - None + return None; + }; + + if let Some(range) = range { + self.range.as_ref().and_then(|f| f(action, range)) + } else { + Some(action) } } @@ -405,27 +482,17 @@ impl CommandRange { } } - pub fn as_count(&self) -> u32 { + pub fn as_count(&self) -> Option { if let CommandRange { start: Position::Line { row, offset: 0 }, end: None, } = &self { - *row + Some(*row) } else { - 0 + None } } - - pub fn is_count(&self) -> bool { - matches!( - &self, - CommandRange { - start: Position::Line { row: _, offset: 0 }, - end: None - } - ) - } } fn generate_commands(_: &AppContext) -> Vec { @@ -578,18 +645,32 @@ fn generate_commands(_: &AppContext) -> Vec { VimCommand::str(("cl", "ist"), "diagnostics::Deploy"), VimCommand::new(("cc", ""), editor::actions::Hover), VimCommand::new(("ll", ""), editor::actions::Hover), - VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).count(), - VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).count(), - VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).count(), - VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).count(), - VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).count(), - VimCommand::new(("j", "oin"), JoinLines).range(), - VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(), - VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(), - VimCommand::new(("d", "elete"), VisualDeleteLine).range(), - VimCommand::new(("y", "ank"), VisualYankLine).range(), - VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(), - VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(), + VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count), + VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count), + VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count), + VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count), + VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count), + VimCommand::new(("j", "oin"), JoinLines).range(select_range), + VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range), + VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines) + .bang(editor::actions::UnfoldRecursive) + .range(act_on_range), + VimCommand::new(("foldc", "lose"), editor::actions::Fold) + .bang(editor::actions::FoldRecursive) + .range(act_on_range), + VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(act_on_range), + VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range), + VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range), + VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| { + Some( + YankCommand { + range: range.clone(), + } + .boxed_clone(), + ) + }), + VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range), + VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range), VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"), VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"), VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"), @@ -620,6 +701,38 @@ fn commands(cx: &AppContext) -> &Vec { .0 } +fn act_on_range(action: Box, range: &CommandRange) -> Option> { + Some( + WithRange { + restore_selection: true, + range: range.clone(), + action: WrappedAction(action), + } + .boxed_clone(), + ) +} + +fn select_range(action: Box, range: &CommandRange) -> Option> { + Some( + WithRange { + restore_selection: false, + range: range.clone(), + action: WrappedAction(action), + } + .boxed_clone(), + ) +} + +fn wrap_count(action: Box, range: &CommandRange) -> Option> { + range.as_count().map(|count| { + WithCount { + count, + action: WrappedAction(action), + } + .boxed_clone() + }) +} + pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option { // NOTE: We also need to support passing arguments to commands like :w // (ideally with filename autocompletion). @@ -679,25 +792,12 @@ pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option) { vim.store_visual_marks(cx); vim.update_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { - let mut original_positions = vim.save_selection_starts(editor, cx); + let original_positions = vim.save_selection_starts(editor, cx); for _ in 0..count { editor.indent(&Default::default(), cx); } - vim.restore_selection_cursors(editor, cx, &mut original_positions); + vim.restore_selection_cursors(editor, cx, original_positions); }); }); if vim.mode.is_visual() { @@ -38,11 +38,11 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { vim.store_visual_marks(cx); vim.update_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { - let mut original_positions = vim.save_selection_starts(editor, cx); + let original_positions = vim.save_selection_starts(editor, cx); for _ in 0..count { editor.outdent(&Default::default(), cx); } - vim.restore_selection_cursors(editor, cx, &mut original_positions); + vim.restore_selection_cursors(editor, cx, original_positions); }); }); if vim.mode.is_visual() { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 10bf3c8e8d73b48e5b34a8b2913e23f590275e58..4a4927a2fc5bfd1e3ce0be8b41b3401a9010805f 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -395,9 +395,9 @@ impl Vim { self.store_visual_marks(cx); self.update_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { - let mut original_positions = vim.save_selection_starts(editor, cx); + let original_positions = vim.save_selection_starts(editor, cx); editor.toggle_comments(&Default::default(), cx); - vim.restore_selection_cursors(editor, cx, &mut original_positions); + vim.restore_selection_cursors(editor, cx, original_positions); }); }); if self.mode.is_visual() { @@ -467,7 +467,7 @@ impl Vim { &self, editor: &mut Editor, cx: &mut ViewContext, - positions: &mut HashMap, + mut positions: HashMap, ) { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 8271aa6cabc163aed3410a41fb37193243d6d464..c176cd6ca9cf3d09498814e741f104ffc42a0866 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{ops::Range, time::Duration}; use crate::{ motion::Motion, @@ -73,7 +73,18 @@ impl Vim { linewise: bool, cx: &mut ViewContext, ) { - self.copy_selections_content_internal(editor, linewise, true, cx); + self.copy_ranges( + editor, + linewise, + true, + editor + .selections + .all_adjusted(cx) + .iter() + .map(|s| s.range()) + .collect(), + cx, + ) } pub fn copy_selections_content( @@ -82,17 +93,28 @@ impl Vim { linewise: bool, cx: &mut ViewContext, ) { - self.copy_selections_content_internal(editor, linewise, false, cx); + self.copy_ranges( + editor, + linewise, + false, + editor + .selections + .all_adjusted(cx) + .iter() + .map(|s| s.range()) + .collect(), + cx, + ) } - fn copy_selections_content_internal( + pub(crate) fn copy_ranges( &mut self, editor: &mut Editor, linewise: bool, is_yank: bool, + selections: Vec>, cx: &mut ViewContext, ) { - let selections = editor.selections.all_adjusted(cx); let buffer = editor.buffer().read(cx).snapshot(cx); let mut text = String::new(); let mut clipboard_selections = Vec::with_capacity(selections.len()); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1503eaac1b6b436d6b4f22b11ac525d2f1a6434b..72474d3ae407810e616940cabe6d5ae78f555c0a 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -63,12 +63,7 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { vim.record_current_action(cx); vim.visual_delete(true, cx); }); - Vim::action(editor, cx, |vim, _: &VisualYank, cx| { - vim.visual_yank(false, cx) - }); - Vim::action(editor, cx, |vim, _: &VisualYankLine, cx| { - vim.visual_yank(true, cx) - }); + Vim::action(editor, cx, |vim, _: &VisualYank, cx| vim.visual_yank(cx)); Vim::action(editor, cx, Vim::select_next); Vim::action(editor, cx, Vim::select_previous); @@ -483,11 +478,10 @@ impl Vim { self.switch_mode(Mode::Normal, true, cx); } - pub fn visual_yank(&mut self, line_mode: bool, cx: &mut ViewContext) { + pub fn visual_yank(&mut self, cx: &mut ViewContext) { self.store_visual_marks(cx); self.update_editor(cx, |vim, editor, cx| { - let line_mode = line_mode || editor.selections.line_mode; - editor.selections.line_mode = line_mode; + let line_mode = editor.selections.line_mode; vim.yank_selections_content(editor, line_mode, cx); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -657,7 +651,7 @@ impl Vim { self.stop_recording(cx); self.visual_delete(false, cx) } - Some(Operator::Yank) => self.visual_yank(false, cx), + Some(Operator::Yank) => self.visual_yank(cx), _ => {} // Ignoring other operators } }