diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index fc13d2927c..dd2e6a132a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -72,6 +72,10 @@ "v": [ "vim::SwitchMode", "Visual" + ], + "V": [ + "vim::SwitchMode", + "VisualLine" ] } }, @@ -112,6 +116,14 @@ "x": "vim::VisualDelete" } }, + { + "context": "Editor && vim_mode == visual_line", + "bindings": { + "c": "vim::VisualLineChange", + "d": "vim::VisualLineDelete", + "x": "vim::VisualLineDelete" + } + }, { "context": "Editor && vim_mode == insert", "bindings": { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e5a80e44f4..a4761ddb06 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1319,7 +1319,11 @@ impl Editor { ) { if self.focused && self.leader_replica_id.is_none() { self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections(&self.selections.disjoint_anchors(), cx) + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + cx, + ) }); } @@ -5599,7 +5603,11 @@ impl View for Editor { self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); if self.leader_replica_id.is_none() { - buffer.set_active_selections(&self.selections.disjoint_anchors(), cx); + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + cx, + ); } }); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 355d1f4433..9893f94292 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -345,12 +345,13 @@ impl EditorElement { scroll_top, scroll_left, bounds, + false, cx, ); } let mut cursors = SmallVec::<[Cursor; 32]>::new(); - for (replica_id, selections) in &layout.selections { + for ((replica_id, line_mode), selections) in &layout.selections { let selection_style = style.replica_selection_style(*replica_id); let corner_radius = 0.15 * layout.line_height; @@ -367,6 +368,7 @@ impl EditorElement { scroll_top, scroll_left, bounds, + *line_mode, cx, ); @@ -483,6 +485,7 @@ impl EditorElement { scroll_top: f32, scroll_left: f32, bounds: RectF, + line_mode: bool, cx: &mut PaintContext, ) { if range.start != range.end { @@ -503,14 +506,14 @@ impl EditorElement { .map(|row| { let line_layout = &layout.line_layouts[(row - start_row) as usize]; HighlightedRangeLine { - start_x: if row == range.start.row() { + start_x: if row == range.start.row() && !line_mode { content_origin.x() + line_layout.x_for_index(range.start.column() as usize) - scroll_left } else { content_origin.x() - scroll_left }, - end_x: if row == range.end.row() { + end_x: if row == range.end.row() && !line_mode { content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left @@ -934,7 +937,7 @@ impl Element for EditorElement { ); let mut remote_selections = HashMap::default(); - for (replica_id, selection) in display_map + for (replica_id, line_mode, selection) in display_map .buffer_snapshot .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone())) { @@ -944,7 +947,7 @@ impl Element for EditorElement { } remote_selections - .entry(replica_id) + .entry((replica_id, line_mode)) .or_insert(Vec::new()) .push(crate::Selection { id: selection.id, @@ -978,7 +981,7 @@ impl Element for EditorElement { let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx)); selections.push(( - local_replica_id, + (local_replica_id, view.selections.line_mode), local_selections .into_iter() .map(|selection| crate::Selection { @@ -1237,7 +1240,7 @@ pub struct LayoutState { em_width: f32, em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, - selections: Vec<(ReplicaId, Vec>)>, + selections: Vec<((ReplicaId, bool), Vec>)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0d8cbf1c6b..47337aa9a2 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -103,7 +103,11 @@ impl FollowableItem for Editor { } else { self.buffer.update(cx, |buffer, cx| { if self.focused { - buffer.set_active_selections(&self.selections.disjoint_anchors(), cx); + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + cx, + ); } }); } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 0b8824be80..6dd1b0685b 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -509,6 +509,7 @@ impl MultiBuffer { pub fn set_active_selections( &mut self, selections: &[Selection], + line_mode: bool, cx: &mut ModelContext, ) { let mut selections_by_buffer: HashMap>> = @@ -573,7 +574,7 @@ impl MultiBuffer { } Some(selection) })); - buffer.set_active_selections(merged_selections, cx); + buffer.set_active_selections(merged_selections, line_mode, cx); }); } } @@ -2397,7 +2398,7 @@ impl MultiBufferSnapshot { pub fn remote_selections_in_range<'a>( &'a self, range: &'a Range, - ) -> impl 'a + Iterator)> { + ) -> impl 'a + Iterator)> { let mut cursor = self.excerpts.cursor::>(); cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &()); cursor @@ -2414,7 +2415,7 @@ impl MultiBufferSnapshot { excerpt .buffer .remote_selections_in_range(query_range) - .flat_map(move |(replica_id, selections)| { + .flat_map(move |(replica_id, line_mode, selections)| { selections.map(move |selection| { let mut start = Anchor { buffer_id: Some(excerpt.buffer_id), @@ -2435,6 +2436,7 @@ impl MultiBufferSnapshot { ( replica_id, + line_mode, Selection { id: selection.id, start, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 07fa2cd9b5..7d9ac8ed40 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -27,6 +27,7 @@ pub struct SelectionsCollection { display_map: ModelHandle, buffer: ModelHandle, pub next_selection_id: usize, + pub line_mode: bool, disjoint: Arc<[Selection]>, pending: Option, } @@ -37,6 +38,7 @@ impl SelectionsCollection { display_map, buffer, next_selection_id: 1, + line_mode: true, disjoint: Arc::from([]), pending: Some(PendingSelection { selection: Selection { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 647481367a..ccb3d382ea 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -83,6 +83,7 @@ pub struct BufferSnapshot { #[derive(Clone, Debug)] struct SelectionSet { + line_mode: bool, selections: Arc<[Selection]>, lamport_timestamp: clock::Lamport, } @@ -129,6 +130,7 @@ pub enum Operation { UpdateSelections { selections: Arc<[Selection]>, lamport_timestamp: clock::Lamport, + line_mode: bool, }, UpdateCompletionTriggers { triggers: Vec, @@ -343,6 +345,7 @@ impl Buffer { this.remote_selections.insert( selection_set.replica_id as ReplicaId, SelectionSet { + line_mode: selection_set.line_mode, selections: proto::deserialize_selections(selection_set.selections), lamport_timestamp, }, @@ -385,6 +388,7 @@ impl Buffer { replica_id: *replica_id as u32, selections: proto::serialize_selections(&set.selections), lamport_timestamp: set.lamport_timestamp.value, + line_mode: set.line_mode, }) .collect(), diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()), @@ -1030,6 +1034,7 @@ impl Buffer { pub fn set_active_selections( &mut self, selections: Arc<[Selection]>, + line_mode: bool, cx: &mut ModelContext, ) { let lamport_timestamp = self.text.lamport_clock.tick(); @@ -1038,11 +1043,13 @@ impl Buffer { SelectionSet { selections: selections.clone(), lamport_timestamp, + line_mode, }, ); self.send_operation( Operation::UpdateSelections { selections, + line_mode, lamport_timestamp, }, cx, @@ -1050,7 +1057,7 @@ impl Buffer { } pub fn remove_active_selections(&mut self, cx: &mut ModelContext) { - self.set_active_selections(Arc::from([]), cx); + self.set_active_selections(Arc::from([]), false, cx); } pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option @@ -1287,6 +1294,7 @@ impl Buffer { Operation::UpdateSelections { selections, lamport_timestamp, + line_mode, } => { if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) { if set.lamport_timestamp > lamport_timestamp { @@ -1299,6 +1307,7 @@ impl Buffer { SelectionSet { selections, lamport_timestamp, + line_mode, }, ); self.text.lamport_clock.observe(lamport_timestamp); @@ -1890,8 +1899,14 @@ impl BufferSnapshot { pub fn remote_selections_in_range<'a>( &'a self, range: Range, - ) -> impl 'a + Iterator>)> - { + ) -> impl 'a + + Iterator< + Item = ( + ReplicaId, + bool, + impl 'a + Iterator>, + ), + > { self.remote_selections .iter() .filter(|(replica_id, set)| { @@ -1909,7 +1924,11 @@ impl BufferSnapshot { Ok(ix) | Err(ix) => ix, }; - (*replica_id, set.selections[start_ix..end_ix].iter()) + ( + *replica_id, + set.line_mode, + set.selections[start_ix..end_ix].iter(), + ) }) } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 312b192cb9..d0a10df5a8 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -43,11 +43,13 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation { }), Operation::UpdateSelections { selections, + line_mode, lamport_timestamp, } => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections { replica_id: lamport_timestamp.replica_id as u32, lamport_timestamp: lamport_timestamp.value, selections: serialize_selections(selections), + line_mode: *line_mode, }), Operation::UpdateDiagnostics { diagnostics, @@ -217,6 +219,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { value: message.lamport_timestamp, }, selections: Arc::from(selections), + line_mode: message.line_mode, } } proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 527c13bfe8..3bc9f4b9dc 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -828,7 +828,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { selections ); active_selections.insert(replica_id, selections.clone()); - buffer.set_active_selections(selections, cx); + buffer.set_active_selections(selections, false, cx); }); mutation_count -= 1; } @@ -984,7 +984,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { let buffer = buffer.read(cx).snapshot(); let actual_remote_selections = buffer .remote_selections_in_range(Anchor::MIN..Anchor::MAX) - .map(|(replica_id, selections)| (replica_id, selections.collect::>())) + .map(|(replica_id, _, selections)| (replica_id, selections.collect::>())) .collect::>(); let expected_remote_selections = active_selections .iter() diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 0fee451c0d..91935f2be6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -779,6 +779,7 @@ message SelectionSet { uint32 replica_id = 1; repeated Selection selections = 2; uint32 lamport_timestamp = 3; + bool line_mode = 4; } message Selection { @@ -854,6 +855,7 @@ message Operation { uint32 replica_id = 1; uint32 lamport_timestamp = 2; repeated Selection selections = 3; + bool line_mode = 4; } message UpdateCompletionTriggers { diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index f9dfc588e1..f3b6115c84 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -18,22 +18,29 @@ fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppCont } fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { - Vim::update(cx, |state, cx| { - state.active_editor = Some(editor.downgrade()); + Vim::update(cx, |vim, cx| { + vim.active_editor = Some(editor.downgrade()); + vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| { + if let editor::Event::SelectionsChanged { local: true } = event { + let newest_empty = !editor.read(cx).selections.newest::(cx).is_empty(); + editor_local_selections_changed(newest_empty, cx); + } + })); + if editor.read(cx).mode() != EditorMode::Full { - state.switch_mode(Mode::Insert, cx); + vim.switch_mode(Mode::Insert, cx); } }); } fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { - Vim::update(cx, |state, cx| { - if let Some(previous_editor) = state.active_editor.clone() { + Vim::update(cx, |vim, cx| { + if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { - state.active_editor = None; + vim.active_editor = None; } } - state.sync_editor_options(cx); + vim.sync_editor_options(cx); }) } @@ -47,3 +54,11 @@ fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppC } }); } + +fn editor_local_selections_changed(newest_empty: bool, cx: &mut MutableAppContext) { + Vim::update(cx, |vim, cx| { + if vim.state.mode == Mode::Normal && !newest_empty { + vim.switch_mode(Mode::Visual, cx) + } + }) +} diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a38d10c8f8..8ab485b58c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -112,6 +112,7 @@ fn motion(motion: Motion, cx: &mut MutableAppContext) { match Vim::read(cx).state.mode { Mode::Normal => normal_motion(motion, cx), Mode::Visual => visual_motion(motion, cx), + Mode::VisualLine => visual_motion(motion, cx), Mode::Insert => { // Shouldn't execute a motion in insert mode. Ignoring } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index b4d5cbe9c7..31c2336f5e 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -7,6 +7,7 @@ pub enum Mode { Normal, Insert, Visual, + VisualLine, } impl Default for Mode { @@ -36,8 +37,7 @@ pub struct VimState { impl VimState { pub fn cursor_shape(&self) -> CursorShape { match self.mode { - Mode::Normal => CursorShape::Block, - Mode::Visual => CursorShape::Block, + Mode::Normal | Mode::Visual | Mode::VisualLine => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -53,6 +53,7 @@ impl VimState { match self.mode { Mode::Normal => "normal", Mode::Visual => "visual", + Mode::VisualLine => "visual_line", Mode::Insert => "insert", } .to_string(), diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index f0731edd49..115ef9ea38 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -10,7 +10,7 @@ mod visual; use collections::HashMap; use editor::{CursorShape, Editor}; -use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle}; +use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle}; use serde::Deserialize; use settings::Settings; @@ -51,6 +51,7 @@ pub fn init(cx: &mut MutableAppContext) { pub struct Vim { editors: HashMap>, active_editor: Option>, + selection_subscription: Option, enabled: bool, state: VimState, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 4d32d38c30..da9bc04cb1 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -4,11 +4,21 @@ use workspace::Workspace; use crate::{motion::Motion, state::Mode, Vim}; -actions!(vim, [VisualDelete, VisualChange]); +actions!( + vim, + [ + VisualDelete, + VisualChange, + VisualLineDelete, + VisualLineChange + ] +); pub fn init(cx: &mut MutableAppContext) { cx.add_action(change); + cx.add_action(change_line); cx.add_action(delete); + cx.add_action(delete_line); } pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { @@ -58,6 +68,22 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.move_with(|map, selection| { + selection.start = map.prev_line_boundary(selection.start.to_point(map)).1; + selection.end = map.next_line_boundary(selection.end.to_point(map)).1; + }); + }); + editor.insert("", cx); + }); + vim.switch_mode(Mode::Insert, cx); + }); +} + pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.switch_mode(Mode::Normal, cx); @@ -88,6 +114,43 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.switch_mode(Mode::Normal, cx); + vim.update_active_editor(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.move_with(|map, selection| { + selection.start = map.prev_line_boundary(selection.start.to_point(map)).1; + + if selection.end.row() < map.max_point().row() { + *selection.end.row_mut() += 1; + *selection.end.column_mut() = 0; + // Don't reset the end here + return; + } else if selection.start.row() > 0 { + *selection.start.row_mut() -= 1; + *selection.start.column_mut() = map.line_len(selection.start.row()); + } + + selection.end = map.next_line_boundary(selection.end.to_point(map)).1; + }); + }); + editor.insert("", cx); + + // Fixup cursor position after the deletion + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.move_with(|map, selection| { + let mut cursor = selection.head(); + cursor = map.clip_point(cursor, Bias::Left); + selection.collapse_to(cursor, selection.goal) + }); + }); + }); + }); +} + #[cfg(test)] mod test { use indoc::indoc;