From d7d17b21487dfccda90d9d9f23e194f8d6cef616 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 18 May 2022 11:10:24 -0700 Subject: [PATCH 01/14] WIP line mode operations --- assets/keymaps/vim.json | 12 ++++ crates/editor/src/editor.rs | 12 +++- crates/editor/src/element.rs | 17 +++--- crates/editor/src/items.rs | 6 +- crates/editor/src/multi_buffer.rs | 8 ++- crates/editor/src/selections_collection.rs | 2 + crates/language/src/buffer.rs | 27 +++++++-- crates/language/src/proto.rs | 3 + crates/language/src/tests.rs | 4 +- crates/rpc/proto/zed.proto | 2 + crates/vim/src/editor_events.rs | 29 +++++++--- crates/vim/src/motion.rs | 1 + crates/vim/src/state.rs | 5 +- crates/vim/src/vim.rs | 3 +- crates/vim/src/visual.rs | 65 +++++++++++++++++++++- 15 files changed, 166 insertions(+), 30 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index fc13d2927c856c4125fcd0f227adbc044abd0b09..dd2e6a132a58ab4ac532e7bf899d2036a3274800 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 e5a80e44f4014127cb5cb76090bfd04a1092e65b..a4761ddb060d42eee902b06909152f2cb8b26594 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 355d1f44337c9c855625ecc0255e51b57ee5db4b..9893f942924c70f4f5232ddfe138096f409ff701 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 0d8cbf1c6b47d66a19853fc3334b444b499fb895..47337aa9a2e6769b32b6486788a9e726e533d80b 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 0b8824be80f95f4b6bdd00610a7493ee29a01a58..6dd1b0685b675a96780e8017becfb5bcac240c3c 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 07fa2cd9b501d4e5964ddf776be12bc22a7bb87f..7d9ac8ed4025478192c4ab1e98ab6f19dff3caa3 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 647481367affe62e38e417accd868f8f59f1bd30..ccb3d382ea89c865cee61d942f475f496c68ed1a 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 312b192cb9f1b907bff763f3a2b31d7f82522d0c..d0a10df5a845149a6e69617511964abed857ecad 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 527c13bfe866b35f20cbcdb295aa648eb8d8eaaa..3bc9f4b9dcfcaea660688f03f658f2579f3b342f 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 0fee451c0d6a64df089592bc50a0ce9172e05ef7..91935f2be61f219f7f76390637eca65973b5ed2d 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 f9dfc588e12a0c9536c174ec3096d879f1ebf4b4..f3b6115c841d0fda9c6474c445d14248468b0768 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 a38d10c8f8be4fc1d701ef12480afd4db0ab54e8..8ab485b58c68789587f66deaf06c70251f850ae0 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 b4d5cbe9c7e2f19b7e611ed6849d6bbae54cddbe..31c2336f5e4721ce156bb71600719566fd0cb10e 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 f0731edd49b5cd3bfa14fa34f12899ae64920a99..115ef9ea38d1a8fb5b0737fee62404576a497bce 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 4d32d38c300467274f54565add78ab836ca9fd56..da9bc04cb1e4bd0fb51d58871130e05665923d0b 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; From f8f316cc64b9f2b7cbfe225c92fe7713c6020f84 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 18 May 2022 17:41:26 -0700 Subject: [PATCH 02/14] Working change and delete in line mode --- assets/keymaps/vim.json | 2 +- crates/editor/src/element.rs | 2 +- crates/editor/src/selections_collection.rs | 2 +- crates/vim/src/editor_events.rs | 2 +- crates/vim/src/state.rs | 4 ++++ crates/vim/src/vim.rs | 10 ++++++++++ crates/vim/src/visual.rs | 12 +++++++++--- 7 files changed, 27 insertions(+), 7 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index dd2e6a132a58ab4ac532e7bf899d2036a3274800..e5fdf44d3ea5238cade39d72107807854393fb9f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -73,7 +73,7 @@ "vim::SwitchMode", "Visual" ], - "V": [ + "shift-V": [ "vim::SwitchMode", "VisualLine" ] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9893f942924c70f4f5232ddfe138096f409ff701..a794ac7edd9725d84c5fed4ca3aede5062f66fd7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -488,7 +488,7 @@ impl EditorElement { line_mode: bool, cx: &mut PaintContext, ) { - if range.start != range.end { + if range.start != range.end || line_mode { let row_range = if range.end.column() == 0 { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) } else { diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 7d9ac8ed4025478192c4ab1e98ab6f19dff3caa3..dfed550777247fdf1ea73e6d69c82411b88cff64 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -38,7 +38,7 @@ impl SelectionsCollection { display_map, buffer, next_selection_id: 1, - line_mode: true, + line_mode: false, disjoint: Arc::from([]), pending: Some(PendingSelection { selection: Selection { diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index f3b6115c841d0fda9c6474c445d14248468b0768..1d477313846033fe2eed1d34a215a06c8e87fd68 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -22,7 +22,7 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont 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(); + let newest_empty = editor.read(cx).selections.newest::(cx).is_empty(); editor_local_selections_changed(newest_empty, cx); } })); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 31c2336f5e4721ce156bb71600719566fd0cb10e..a5ae5448fb73f8bda9692858ceff215ea460e95d 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -46,6 +46,10 @@ impl VimState { !matches!(self.mode, Mode::Insert) } + pub fn empty_selections_only(&self) -> bool { + self.mode != Mode::Visual && self.mode != Mode::VisualLine + } + pub fn keymap_context_layer(&self) -> Context { let mut context = Context::default(); context.map.insert( diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 115ef9ea38d1a8fb5b0737fee62404576a497bce..115536e6a5ad014f57bec88b50d2549241b49b7a 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -128,14 +128,24 @@ impl Vim { editor.set_cursor_shape(cursor_shape, cx); editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx); editor.set_input_enabled(!state.vim_controlled()); + editor.selections.line_mode = state.mode == Mode::VisualLine; let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer); } else { editor.set_cursor_shape(CursorShape::Bar, cx); editor.set_clip_at_line_ends(false, cx); editor.set_input_enabled(true); + editor.selections.line_mode = false; editor.remove_keymap_context_layer::(); } + + if state.empty_selections_only() { + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + selection.collapse_to(selection.head(), selection.goal) + }); + }) + } }); } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index da9bc04cb1e4bd0fb51d58871130e05665923d0b..0a7517bfb8078687a8b4a4db08582e967cdc15a7 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,3 +1,4 @@ +use collections::HashMap; use editor::{Autoscroll, Bias}; use gpui::{actions, MutableAppContext, ViewContext}; use workspace::Workspace; @@ -68,7 +69,7 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) { +pub fn change_line(_: &mut Workspace, _: &VisualLineChange, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -114,13 +115,14 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { +pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, 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); + let mut original_columns: HashMap<_, _> = Default::default(); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.move_with(|map, selection| { + original_columns.insert(selection.id, selection.head().column()); selection.start = map.prev_line_boundary(selection.start.to_point(map)).1; if selection.end.row() < map.max_point().row() { @@ -143,11 +145,15 @@ pub fn delete_line(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext Date: Thu, 19 May 2022 10:25:06 -0700 Subject: [PATCH 03/14] WIP copy on delete --- crates/editor/src/editor.rs | 6 +++--- crates/vim/src/normal/change.rs | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a4761ddb060d42eee902b06909152f2cb8b26594..219fcba22b7c23e4a3f25f2d13557b4466d4386f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -837,9 +837,9 @@ struct ActiveDiagnosticGroup { } #[derive(Serialize, Deserialize)] -struct ClipboardSelection { - len: usize, - is_entire_line: bool, +pub struct ClipboardSelection { + pub len: usize, + pub is_entire_line: bool, } #[derive(Debug)] diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 8124f8a2006c974d8dace98648d7128c8d881aa2..0636b4b1ef7fb1fb5002a4d9d5f71fdcdcdae9ff 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,6 +1,6 @@ use crate::{motion::Motion, state::Mode, Vim}; -use editor::{char_kind, movement, Autoscroll}; -use gpui::{impl_actions, MutableAppContext, ViewContext}; +use editor::{char_kind, movement, Autoscroll, ClipboardSelection}; +use gpui::{impl_actions, ClipboardItem, MutableAppContext, ViewContext}; use serde::Deserialize; use workspace::Workspace; @@ -22,12 +22,26 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); + let mut text = String::new(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let mut clipboard_selections = Vec::with_capacity(editor.selections.count()); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.move_with(|map, selection| { motion.expand_selection(map, selection, false); + let mut len = 0; + let range = selection.start.to_point(map)..selection.end.to_point(map); + for chunk in buffer.text_for_range(range) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line: motion.linewise(), + }); }); }); editor.insert(&"", cx); + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); }); }); vim.switch_mode(Mode::Insert, cx) From 082036161fd3815c831ceedfd28ba15b0ed6eb9f Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 19 May 2022 17:42:30 -0700 Subject: [PATCH 04/14] Enable copy and paste in vim mode --- assets/keymaps/vim.json | 3 +- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 2 +- crates/text/src/selection.rs | 5 ++ crates/vim/src/normal.rs | 124 +++++++++++++++++++++++++++++++- crates/vim/src/normal/change.rs | 22 ++---- crates/vim/src/normal/delete.rs | 3 +- crates/vim/src/utils.rs | 26 +++++++ crates/vim/src/vim.rs | 14 ++-- crates/vim/src/visual.rs | 16 +++-- 10 files changed, 183 insertions(+), 34 deletions(-) create mode 100644 crates/vim/src/utils.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index e5fdf44d3ea5238cade39d72107807854393fb9f..00e7fdba2c1e93f30979e786dda9364b192db180 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -76,7 +76,8 @@ "shift-V": [ "vim::SwitchMode", "VisualLine" - ] + ], + "p": "vim::Paste" } }, { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 219fcba22b7c23e4a3f25f2d13557b4466d4386f..d80b03da9e655a957d7855345cec0718a77550f7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3,7 +3,7 @@ mod element; pub mod items; pub mod movement; mod multi_buffer; -mod selections_collection; +pub mod selections_collection; #[cfg(test)] mod test; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a794ac7edd9725d84c5fed4ca3aede5062f66fd7..3ef169a2e0248004f888ca9c886e88c5e706a78c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -489,7 +489,7 @@ impl EditorElement { cx: &mut PaintContext, ) { if range.start != range.end || line_mode { - let row_range = if range.end.column() == 0 { + let row_range = if range.end.column() == 0 && !line_mode { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) } else { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 8dcc3fc7f18974eef49fa2b96297e57977b82ab1..fd8d57ca9f81a597a309483d3355cdcb9aa79afc 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -1,6 +1,7 @@ use crate::Anchor; use crate::{rope::TextDimension, BufferSnapshot}; use std::cmp::Ordering; +use std::ops::Range; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum SelectionGoal { @@ -83,6 +84,10 @@ impl Selection { self.goal = new_goal; self.reversed = false; } + + pub fn range(&self) -> Range { + self.start..self.end + } } impl Selection { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2a391676fa8e0b8a5796be153386f96a054b66bd..d9b5d470e768f0b8e2810fb1bcad20c2034fdf88 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,6 +1,8 @@ mod change; mod delete; +use std::borrow::Cow; + use crate::{ motion::Motion, state::{Mode, Operator}, @@ -8,9 +10,9 @@ use crate::{ }; use change::init as change_init; use collections::HashSet; -use editor::{Autoscroll, Bias, DisplayPoint}; +use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::SelectionGoal; +use language::{Point, SelectionGoal}; use workspace::Workspace; use self::{change::change_over, delete::delete_over}; @@ -27,6 +29,8 @@ actions!( DeleteRight, ChangeToEndOfLine, DeleteToEndOfLine, + Paste, + Yank, ] ); @@ -56,6 +60,7 @@ pub fn init(cx: &mut MutableAppContext) { delete_over(vim, Motion::EndOfLine, cx); }) }); + cx.add_action(paste); change_init(cx); } @@ -187,6 +192,98 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); } +fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + if let Some(item) = cx.as_mut().read_from_clipboard() { + let mut clipboard_text = Cow::Borrowed(item.text()); + if let Some(mut clipboard_selections) = + item.metadata::>() + { + let (display_map, selections) = editor.selections.all_display(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + if clipboard_selections.len() != selections.len() { + let mut newline_separated_text = String::new(); + let mut clipboard_selections = + clipboard_selections.drain(..).peekable(); + let mut ix = 0; + while let Some(clipboard_selection) = clipboard_selections.next() { + newline_separated_text + .push_str(&clipboard_text[ix..ix + clipboard_selection.len]); + ix += clipboard_selection.len; + if clipboard_selections.peek().is_some() { + newline_separated_text.push('\n'); + } + } + clipboard_text = Cow::Owned(newline_separated_text); + } + + let mut new_selections = Vec::new(); + editor.buffer().update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let mut start_offset = 0; + let mut edits = Vec::new(); + for (ix, selection) in selections.iter().enumerate() { + let to_insert; + let linewise; + if let Some(clipboard_selection) = clipboard_selections.get(ix) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + linewise = clipboard_selection.is_entire_line; + start_offset = end_offset; + } else { + to_insert = clipboard_text.as_str(); + linewise = all_selections_were_entire_line; + } + + // If the clipboard text was copied linewise, and the current selection + // is empty, then paste the text after this line and move the selection + // to the start of the pasted text + let range = if selection.is_empty() && linewise { + let (point, _) = display_map + .next_line_boundary(selection.start.to_point(&display_map)); + + if !to_insert.starts_with('\n') { + // Add newline before pasted text so that it shows up + edits.push((point..point, "\n")); + } + // Drop selection at the start of the next line + let selection_point = Point::new(point.row + 1, 0); + new_selections.push(selection.map(|_| selection_point.clone())); + point..point + } else { + let range = selection.map(|p| p.to_point(&display_map)).range(); + new_selections.push(selection.map(|_| range.start.clone())); + range + }; + + if linewise && to_insert.ends_with('\n') { + edits.push(( + range, + &to_insert[0..to_insert.len().saturating_sub(1)], + )) + } else { + edits.push((range, to_insert)); + } + } + drop(snapshot); + buffer.edit_with_autoindent(edits, cx); + }); + + editor.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.select(new_selections) + }); + } else { + editor.insert(&clipboard_text, cx); + } + } + }); + }); + }); +} + #[cfg(test)] mod test { use indoc::indoc; @@ -1026,4 +1123,27 @@ mod test { brown fox"}, ); } + + #[gpui::test] + async fn test_p(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + + cx.simulate_keystrokes(["d", "d"]); + cx.assert_editor_state(indoc! {" + The quick brown + the la|zy dog"}); + + cx.simulate_keystroke("p"); + cx.assert_editor_state(indoc! {" + The quick brown + the lazy dog + |fox jumps over"}); + } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 0636b4b1ef7fb1fb5002a4d9d5f71fdcdcdae9ff..7f417fd31ed3167097e6eeeff52f14fc9024b690 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,6 +1,6 @@ -use crate::{motion::Motion, state::Mode, Vim}; -use editor::{char_kind, movement, Autoscroll, ClipboardSelection}; -use gpui::{impl_actions, ClipboardItem, MutableAppContext, ViewContext}; +use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; +use editor::{char_kind, movement, Autoscroll}; +use gpui::{impl_actions, MutableAppContext, ViewContext}; use serde::Deserialize; use workspace::Workspace; @@ -22,26 +22,13 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); - let mut text = String::new(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let mut clipboard_selections = Vec::with_capacity(editor.selections.count()); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.move_with(|map, selection| { motion.expand_selection(map, selection, false); - let mut len = 0; - let range = selection.start.to_point(map)..selection.end.to_point(map); - for chunk in buffer.text_for_range(range) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line: motion.linewise(), - }); }); }); + copy_selections_content(editor, motion.linewise(), cx); editor.insert(&"", cx); - cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); }); }); vim.switch_mode(Mode::Insert, cx) @@ -79,6 +66,7 @@ fn change_word( }); }); }); + copy_selections_content(editor, false, cx); editor.insert(&"", cx); }); }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index b44f0a1f34890142904463e530d7a4e76cd81d5f..cea607e9f3dbba956dc6436d0965ccc87bdd1021 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,4 +1,4 @@ -use crate::{motion::Motion, Vim}; +use crate::{motion::Motion, utils::copy_selections_content, Vim}; use collections::HashMap; use editor::{Autoscroll, Bias}; use gpui::MutableAppContext; @@ -15,6 +15,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { original_columns.insert(selection.id, original_head.column()); }); }); + copy_selections_content(editor, motion.linewise(), cx); editor.insert(&"", cx); // Fixup cursor position after the deletion diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..1cd5f1360860ba087101ab04206cadd3424301ba --- /dev/null +++ b/crates/vim/src/utils.rs @@ -0,0 +1,26 @@ +use editor::{ClipboardSelection, Editor}; +use gpui::{ClipboardItem, MutableAppContext}; +use language::Point; + +pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) { + let selections = editor.selections.all::(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); + let mut text = String::new(); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + for selection in selections.iter() { + let initial_len = text.len(); + let start = selection.start; + let end = selection.end; + for chunk in buffer.text_for_range(start..end) { + text.push_str(chunk); + } + clipboard_selections.push(ClipboardSelection { + len: text.len() - initial_len, + is_entire_line: linewise, + }); + } + } + + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 115536e6a5ad014f57bec88b50d2549241b49b7a..00ef98987415e7acfeb3b5c92996025bc3d7c142 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -6,6 +6,7 @@ mod insert; mod motion; mod normal; mod state; +mod utils; mod visual; use collections::HashMap; @@ -140,11 +141,14 @@ impl Vim { } if state.empty_selections_only() { - editor.change_selections(None, cx, |s| { - s.move_with(|_, selection| { - selection.collapse_to(selection.head(), selection.goal) - }); - }) + // Defer so that access to global settings object doesn't panic + cx.defer(|editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + selection.collapse_to(selection.head(), selection.goal) + }); + }) + }); } }); } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 0a7517bfb8078687a8b4a4db08582e967cdc15a7..480c7e07b6652baf3c9d2c033bad208f8051f34a 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -3,7 +3,7 @@ use editor::{Autoscroll, Bias}; use gpui::{actions, MutableAppContext, ViewContext}; use workspace::Workspace; -use crate::{motion::Motion, state::Mode, Vim}; +use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; actions!( vim, @@ -41,7 +41,7 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { // Head was at the end of the selection, and now is at the start. We need to move the end // forward by one if possible in order to compensate for this change. *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Left); + selection.end = map.clip_point(selection.end, Bias::Right); } }); }); @@ -63,6 +63,7 @@ pub fn change(_: &mut Workspace, _: &VisualChange, 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| { if !selection.reversed { - // Head was at the end of the selection, and now is at the start. We need to move the end - // forward by one if possible in order to compensate for this change. + // Head is at the end of the selection. Adjust the end position to + // to include the character under the cursor. *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Left); + selection.end = map.clip_point(selection.end, Bias::Right); } }); }); + copy_selections_content(editor, false, cx); editor.insert("", cx); // Fixup cursor position after the deletion @@ -112,6 +114,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext Date: Thu, 19 May 2022 18:20:41 -0700 Subject: [PATCH 05/14] Add visual line mode operator tests --- crates/vim/src/vim_test_context.rs | 8 +- crates/vim/src/visual.rs | 158 +++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index f9080e554cb23638b44b5401d0c1ca762b078900..a319d6dbeade6f6a32c0382ff29b20c808ab115b 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -1,4 +1,4 @@ -use std::ops::{Deref, Range}; +use std::ops::{Deref, DerefMut, Range}; use collections::BTreeMap; use itertools::{Either, Itertools}; @@ -404,3 +404,9 @@ impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> { &self.cx } } + +impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 480c7e07b6652baf3c9d2c033bad208f8051f34a..c3416da471eec1f4ae486353b90bef9a093afae8 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -249,6 +249,13 @@ mod test { The |ver the lazy dog"}, ); + // Test pasting code copied on delete + cx.simulate_keystrokes(["j", "p"]); + cx.assert_editor_state(indoc! {" + The ver + the lazy d|quick brown + fox jumps oog"}); + cx.assert( indoc! {" The quick brown @@ -299,6 +306,77 @@ mod test { ); } + #[gpui::test] + async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-V", "x"]); + cx.assert( + indoc! {" + The qu|ick brown + fox jumps over + the lazy dog"}, + indoc! {" + fox ju|mps over + the lazy dog"}, + ); + // Test pasting code copied on delete + cx.simulate_keystroke("p"); + cx.assert_editor_state(indoc! {" + fox jumps over + |The quick brown + the lazy dog"}); + + cx.assert( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + indoc! {" + The quick brown + the la|zy dog"}, + ); + cx.assert( + indoc! {" + The quick brown + fox jumps over + the la|zy dog"}, + indoc! {" + The quick brown + fox ju|mps over"}, + ); + let mut cx = cx.binding(["shift-V", "j", "x"]); + cx.assert( + indoc! {" + The qu|ick brown + fox jumps over + the lazy dog"}, + "the la|zy dog", + ); + // Test pasting code copied on delete + cx.simulate_keystroke("p"); + cx.assert_editor_state(indoc! {" + the lazy dog + |The quick brown + fox jumps over"}); + + cx.assert( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + "The qu|ick brown", + ); + cx.assert( + indoc! {" + The quick brown + fox jumps over + the la|zy dog"}, + indoc! {" + The quick brown + fox ju|mps over"}, + ); + } + #[gpui::test] async fn test_visual_change(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; @@ -363,4 +441,84 @@ mod test { the lazy dog"}, ); } + + #[gpui::test] + async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert); + cx.assert( + indoc! {" + The qu|ick brown + fox jumps over + the lazy dog"}, + indoc! {" + | + fox jumps over + the lazy dog"}, + ); + // Test pasting code copied on change + cx.simulate_keystrokes(["escape", "j", "p"]); + cx.assert_editor_state(indoc! {" + + fox jumps over + |The quick brown + the lazy dog"}); + + cx.assert( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + indoc! {" + The quick brown + | + the lazy dog"}, + ); + cx.assert( + indoc! {" + The quick brown + fox jumps over + the la|zy dog"}, + indoc! {" + The quick brown + fox jumps over + |"}, + ); + let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert); + cx.assert( + indoc! {" + The qu|ick brown + fox jumps over + the lazy dog"}, + indoc! {" + | + the lazy dog"}, + ); + // Test pasting code copied on delete + cx.simulate_keystrokes(["escape", "j", "p"]); + cx.assert_editor_state(indoc! {" + + the lazy dog + |The quick brown + fox jumps over"}); + cx.assert( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + indoc! {" + The quick brown + |"}, + ); + cx.assert( + indoc! {" + The quick brown + fox jumps over + the la|zy dog"}, + indoc! {" + The quick brown + fox jumps over + |"}, + ); + } } From 61f0daa5c5215082d8bbba4e50a0f19711f4f231 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Mon, 23 May 2022 09:23:25 -0700 Subject: [PATCH 06/14] Visual line mode handles soft wraps --- assets/keymaps/vim.json | 23 +++- crates/editor/src/display_map.rs | 12 ++ crates/editor/src/editor.rs | 21 ++-- crates/editor/src/element.rs | 82 +++++++----- crates/editor/src/selections_collection.rs | 14 +++ crates/gpui/src/app.rs | 50 ++++---- crates/vim/src/motion.rs | 2 + crates/vim/src/normal.rs | 8 +- crates/vim/src/normal/yank.rs | 26 ++++ crates/vim/src/state.rs | 2 + crates/vim/src/vim.rs | 19 ++- crates/vim/src/vim_test_context.rs | 8 ++ crates/vim/src/visual.rs | 139 +++++++++++++++++++-- crates/zed/src/main.rs | 4 +- 14 files changed, 314 insertions(+), 96 deletions(-) create mode 100644 crates/vim/src/normal/yank.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 00e7fdba2c1e93f30979e786dda9364b192db180..f1dca985ab687aec250c3624e79ee1515c96c8e5 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -9,7 +9,7 @@ } ], "h": "vim::Left", - "backspace": "vim::Left", + "backspace": "editor::Backspace", // "vim::Left", "j": "vim::Down", "k": "vim::Up", "l": "vim::Right", @@ -57,6 +57,10 @@ "Delete" ], "shift-D": "vim::DeleteToEndOfLine", + "y": [ + "vim::PushOperator", + "Yank" + ], "i": [ "vim::SwitchMode", "Insert" @@ -77,7 +81,10 @@ "vim::SwitchMode", "VisualLine" ], - "p": "vim::Paste" + "p": "vim::Paste", + "u": "editor::Undo", + "ctrl-r": "editor::Redo", + "ctrl-o": "pane::GoBack" } }, { @@ -109,12 +116,19 @@ "d": "vim::CurrentLine" } }, + { + "context": "Editor && vim_operator == y", + "bindings": { + "y": "vim::CurrentLine" + } + }, { "context": "Editor && vim_mode == visual", "bindings": { "c": "vim::VisualChange", "d": "vim::VisualDelete", - "x": "vim::VisualDelete" + "x": "vim::VisualDelete", + "y": "vim::VisualYank" } }, { @@ -122,7 +136,8 @@ "bindings": { "c": "vim::VisualLineChange", "d": "vim::VisualLineDelete", - "x": "vim::VisualLineDelete" + "x": "vim::VisualLineDelete", + "y": "vim::VisualLineYank" } }, { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 3de44e031537a0627606faaecf1c7ceb2615c125..f76d23a187958a057f085a8b328169a99d3b508d 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -279,6 +279,18 @@ impl DisplaySnapshot { } } + pub fn expand_to_line(&self, mut range: Range) -> Range { + (range.start, _) = self.prev_line_boundary(range.start); + (range.end, _) = self.next_line_boundary(range.end); + + if range.is_empty() && range.start.row > 0 { + range.start.row -= 1; + range.start.column = self.buffer_snapshot.line_len(range.start.row); + } + + range + } + fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { let fold_point = self.folds_snapshot.to_fold_point(point, bias); let tab_point = self.tabs_snapshot.to_tab_point(fold_point); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d80b03da9e655a957d7855345cec0718a77550f7..f46fa831412603e91280f46e8fb5c5df8ea873d1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1860,7 +1860,7 @@ impl Editor { pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { let text: Arc = text.into(); self.transact(cx, |this, cx| { - let old_selections = this.selections.all::(cx); + let old_selections = this.selections.all_adjusted(cx); let selection_anchors = this.buffer.update(cx, |buffer, cx| { let anchors = { let snapshot = buffer.read(cx); @@ -2750,7 +2750,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(cx); for selection in &mut selections { - if selection.is_empty() { + if selection.is_empty() && !self.selections.line_mode { let old_head = selection.head(); let mut new_head = movement::left(&display_map, old_head.to_display_point(&display_map)) @@ -2783,8 +2783,9 @@ impl Editor { pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { self.transact(cx, |this, cx| { this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - if selection.is_empty() { + if selection.is_empty() && !line_mode { let cursor = movement::right(map, selection.head()); selection.set_head(cursor, SelectionGoal::None); } @@ -2807,7 +2808,7 @@ impl Editor { return; } - let mut selections = self.selections.all::(cx); + let mut selections = self.selections.all_adjusted(cx); if selections.iter().all(|s| s.is_empty()) { self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { @@ -3347,7 +3348,7 @@ impl Editor { { let max_point = buffer.max_point(); for selection in &mut selections { - let is_entire_line = selection.is_empty(); + let is_entire_line = selection.is_empty() || self.selections.line_mode; if is_entire_line { selection.start = Point::new(selection.start.row, 0); selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); @@ -3378,16 +3379,17 @@ impl Editor { let selections = self.selections.all::(cx); let buffer = self.buffer.read(cx).read(cx); let mut text = String::new(); + let mut clipboard_selections = Vec::with_capacity(selections.len()); { let max_point = buffer.max_point(); for selection in selections.iter() { let mut start = selection.start; let mut end = selection.end; - let is_entire_line = selection.is_empty(); + let is_entire_line = selection.is_empty() || self.selections.line_mode; if is_entire_line { start = Point::new(start.row, 0); - end = cmp::min(max_point, Point::new(start.row + 1, 0)); + end = cmp::min(max_point, Point::new(end.row + 1, 0)); } let mut len = 0; for chunk in buffer.text_for_range(start..end) { @@ -3453,7 +3455,7 @@ impl Editor { let line_start = selection.start - column; line_start..line_start } else { - selection.start..selection.end + selection.range() }; edits.push((range, to_insert)); @@ -3670,8 +3672,9 @@ impl Editor { ) { self.transact(cx, |this, cx| { this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - if selection.is_empty() { + if selection.is_empty() && !line_mode { let cursor = movement::previous_word_start(map, selection.head()); selection.set_head(cursor, SelectionGoal::None); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ef169a2e0248004f888ca9c886e88c5e706a78c..8c0791517dc2d814f3ca557ec5b1388a9656b86c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3,7 +3,10 @@ use super::{ Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN, }; -use crate::{display_map::TransformBlock, EditorStyle}; +use crate::{ + display_map::{DisplaySnapshot, TransformBlock}, + EditorStyle, +}; use clock::ReplicaId; use collections::{BTreeMap, HashMap}; use gpui::{ @@ -22,7 +25,7 @@ use gpui::{ MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; -use language::{Bias, DiagnosticSeverity}; +use language::{Bias, DiagnosticSeverity, Selection}; use settings::Settings; use smallvec::SmallVec; use std::{ @@ -32,6 +35,35 @@ use std::{ ops::Range, }; +struct SelectionLayout { + head: DisplayPoint, + range: Range, +} + +impl SelectionLayout { + fn from( + selection: Selection, + line_mode: bool, + map: &DisplaySnapshot, + ) -> Self { + if line_mode { + let selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); + let point_range = map.expand_to_line(selection.range()); + Self { + head: selection.head().to_display_point(map), + range: point_range.start.to_display_point(map) + ..point_range.end.to_display_point(map), + } + } else { + let selection = selection.map(|p| p.to_display_point(map)); + Self { + head: selection.head(), + range: selection.range(), + } + } + } +} + pub struct EditorElement { view: WeakViewHandle, style: EditorStyle, @@ -345,19 +377,18 @@ impl EditorElement { scroll_top, scroll_left, bounds, - false, cx, ); } let mut cursors = SmallVec::<[Cursor; 32]>::new(); - for ((replica_id, line_mode), selections) in &layout.selections { + for (replica_id, selections) in &layout.selections { let selection_style = style.replica_selection_style(*replica_id); let corner_radius = 0.15 * layout.line_height; for selection in selections { self.paint_highlighted_range( - selection.start..selection.end, + selection.range.clone(), start_row, end_row, selection_style.selection, @@ -368,12 +399,11 @@ impl EditorElement { scroll_top, scroll_left, bounds, - *line_mode, cx, ); if view.show_local_cursors() || *replica_id != local_replica_id { - let cursor_position = selection.head(); + let cursor_position = selection.head; if (start_row..end_row).contains(&cursor_position.row()) { let cursor_row_layout = &layout.line_layouts[(cursor_position.row() - start_row) as usize]; @@ -485,11 +515,10 @@ impl EditorElement { scroll_top: f32, scroll_left: f32, bounds: RectF, - line_mode: bool, cx: &mut PaintContext, ) { - if range.start != range.end || line_mode { - let row_range = if range.end.column() == 0 && !line_mode { + if range.start != range.end { + let row_range = if range.end.column() == 0 { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) } else { cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) @@ -506,14 +535,14 @@ impl EditorElement { .map(|row| { let line_layout = &layout.line_layouts[(row - start_row) as usize]; HighlightedRangeLine { - start_x: if row == range.start.row() && !line_mode { + start_x: if row == range.start.row() { 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() && !line_mode { + end_x: if row == range.end.row() { content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left @@ -921,7 +950,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections = Vec::new(); + let mut selections: Vec<(ReplicaId, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; let mut highlighted_ranges = Vec::new(); @@ -945,17 +974,10 @@ impl Element for EditorElement { if Some(replica_id) == view.leader_replica_id { continue; } - remote_selections - .entry((replica_id, line_mode)) + .entry(replica_id) .or_insert(Vec::new()) - .push(crate::Selection { - id: selection.id, - goal: selection.goal, - reversed: selection.reversed, - start: selection.start.to_display_point(&display_map), - end: selection.end.to_display_point(&display_map), - }); + .push(SelectionLayout::from(selection, line_mode, &display_map)); } selections.extend(remote_selections); @@ -981,15 +1003,15 @@ impl Element for EditorElement { let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx)); selections.push(( - (local_replica_id, view.selections.line_mode), + local_replica_id, local_selections .into_iter() - .map(|selection| crate::Selection { - id: selection.id, - goal: selection.goal, - reversed: selection.reversed, - start: selection.start.to_display_point(&display_map), - end: selection.end.to_display_point(&display_map), + .map(|selection| { + SelectionLayout::from( + selection, + view.selections.line_mode, + &display_map, + ) }) .collect(), )); @@ -1240,7 +1262,7 @@ pub struct LayoutState { em_width: f32, em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, - selections: Vec<((ReplicaId, bool), Vec>)>, + selections: Vec<(ReplicaId, Vec)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index dfed550777247fdf1ea73e6d69c82411b88cff64..db6571cee1f3c3d884912f4e269360daa5070bde 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -128,6 +128,20 @@ impl SelectionsCollection { .collect() } + // Returns all of the selections, adjusted to take into account the selection line_mode + pub fn all_adjusted(&self, cx: &mut MutableAppContext) -> Vec> { + let mut selections = self.all::(cx); + if self.line_mode { + let map = self.display_map(cx); + for selection in &mut selections { + let new_range = map.expand_to_line(selection.range()); + selection.start = new_range.start; + selection.end = new_range.end; + } + } + selections + } + pub fn disjoint_in_range<'a, D>( &self, range: Range, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index eb4b9650a67dbc0568f754abb72322df659cc06b..93e5f8279dc6b3f53a990f0fdb6b25dd5903d6a6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -755,7 +755,7 @@ type SubscriptionCallback = Box b type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; type FocusObservationCallback = Box bool>; -type GlobalObservationCallback = Box; +type GlobalObservationCallback = Box; type ReleaseObservationCallback = Box; type DeserializeActionCallback = fn(json: &str) -> anyhow::Result>; @@ -1263,7 +1263,7 @@ impl MutableAppContext { pub fn observe_global(&mut self, mut observe: F) -> Subscription where G: Any, - F: 'static + FnMut(&G, &mut MutableAppContext), + F: 'static + FnMut(&mut MutableAppContext), { let type_id = TypeId::of::(); let id = post_inc(&mut self.next_subscription_id); @@ -1274,11 +1274,8 @@ impl MutableAppContext { .or_default() .insert( id, - Some( - Box::new(move |global: &dyn Any, cx: &mut MutableAppContext| { - observe(global.downcast_ref().unwrap(), cx) - }) as GlobalObservationCallback, - ), + Some(Box::new(move |cx: &mut MutableAppContext| observe(cx)) + as GlobalObservationCallback), ); Subscription::GlobalObservation { @@ -2261,27 +2258,24 @@ impl MutableAppContext { fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) { let callbacks = self.global_observations.lock().remove(&observed_type_id); if let Some(callbacks) = callbacks { - if let Some(global) = self.cx.globals.remove(&observed_type_id) { - for (id, callback) in callbacks { - if let Some(mut callback) = callback { - callback(global.as_ref(), self); - match self - .global_observations - .lock() - .entry(observed_type_id) - .or_default() - .entry(id) - { - collections::btree_map::Entry::Vacant(entry) => { - entry.insert(Some(callback)); - } - collections::btree_map::Entry::Occupied(entry) => { - entry.remove(); - } + for (id, callback) in callbacks { + if let Some(mut callback) = callback { + callback(self); + match self + .global_observations + .lock() + .entry(observed_type_id) + .or_default() + .entry(id) + { + collections::btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + collections::btree_map::Entry::Occupied(entry) => { + entry.remove(); } } } - self.cx.globals.insert(observed_type_id, global); } } } @@ -5599,7 +5593,7 @@ mod tests { let observation_count = Rc::new(RefCell::new(0)); let subscription = cx.observe_global::({ let observation_count = observation_count.clone(); - move |_, _| { + move |_| { *observation_count.borrow_mut() += 1; } }); @@ -5629,7 +5623,7 @@ mod tests { let observation_count = Rc::new(RefCell::new(0)); cx.observe_global::({ let observation_count = observation_count.clone(); - move |_, _| { + move |_| { *observation_count.borrow_mut() += 1; } }) @@ -6003,7 +5997,7 @@ mod tests { *subscription.borrow_mut() = Some(cx.observe_global::<(), _>({ let observation_count = observation_count.clone(); let subscription = subscription.clone(); - move |_, _| { + move |_| { subscription.borrow_mut().take(); *observation_count.borrow_mut() += 1; } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 8ab485b58c68789587f66deaf06c70251f850ae0..16533f89f1b6a4481661f841266b0c6108849dca 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -193,11 +193,13 @@ impl Motion { if selection.end.row() < map.max_point().row() { *selection.end.row_mut() += 1; *selection.end.column_mut() = 0; + selection.end = map.clip_point(selection.end, Bias::Right); // 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.start = map.clip_point(selection.start, Bias::Left); } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index d9b5d470e768f0b8e2810fb1bcad20c2034fdf88..0d68b29bf9409fd851dde06678f713ecc497349d 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,5 +1,6 @@ mod change; mod delete; +mod yank; use std::borrow::Cow; @@ -15,7 +16,7 @@ use gpui::{actions, MutableAppContext, ViewContext}; use language::{Point, SelectionGoal}; use workspace::Workspace; -use self::{change::change_over, delete::delete_over}; +use self::{change::change_over, delete::delete_over, yank::yank_over}; actions!( vim, @@ -69,11 +70,12 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { match vim.state.operator_stack.pop() { None => move_cursor(vim, motion, cx), - Some(Operator::Change) => change_over(vim, motion, cx), - Some(Operator::Delete) => delete_over(vim, motion, cx), Some(Operator::Namespace(_)) => { // Can't do anything for a namespace operator. Ignoring } + Some(Operator::Change) => change_over(vim, motion, cx), + Some(Operator::Delete) => delete_over(vim, motion, cx), + Some(Operator::Yank) => yank_over(vim, motion, cx), } vim.clear_operator(cx); }); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs new file mode 100644 index 0000000000000000000000000000000000000000..17a9e47d3d84b8a491c8dd837c7e8f975a422c74 --- /dev/null +++ b/crates/vim/src/normal/yank.rs @@ -0,0 +1,26 @@ +use crate::{motion::Motion, utils::copy_selections_content, Vim}; +use collections::HashMap; +use gpui::MutableAppContext; + +pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { + vim.update_active_editor(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + let mut original_positions: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let original_position = (selection.head(), selection.goal); + motion.expand_selection(map, selection, true); + original_positions.insert(selection.id, original_position); + }); + }); + copy_selections_content(editor, motion.linewise(), cx); + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + let (head, goal) = original_positions.remove(&selection.id).unwrap(); + selection.collapse_to(head, goal); + }); + }); + }); + }); +} diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index a5ae5448fb73f8bda9692858ceff215ea460e95d..c4c6d4850b751910691007f958aa9ae43a4dc314 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -26,6 +26,7 @@ pub enum Operator { Namespace(Namespace), Change, Delete, + Yank, } #[derive(Default)] @@ -80,6 +81,7 @@ impl Operator { Operator::Namespace(Namespace::G) => "g", Operator::Change => "c", Operator::Delete => "d", + Operator::Yank => "y", } .to_owned(); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 00ef98987415e7acfeb3b5c92996025bc3d7c142..5ee3f3d38bb11c8d53bb27e2785e42a439c39dbf 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -42,8 +42,10 @@ pub fn init(cx: &mut MutableAppContext) { }, ); - cx.observe_global::(|settings, cx| { - Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx)) + cx.observe_global::(|cx| { + Vim::update(cx, |state, cx| { + state.set_enabled(cx.global::().vim_mode, cx) + }) }) .detach(); } @@ -141,14 +143,11 @@ impl Vim { } if state.empty_selections_only() { - // Defer so that access to global settings object doesn't panic - cx.defer(|editor, cx| { - editor.change_selections(None, cx, |s| { - s.move_with(|_, selection| { - selection.collapse_to(selection.head(), selection.goal) - }); - }) - }); + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + selection.collapse_to(selection.head(), selection.goal) + }); + }) } }); } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index a319d6dbeade6f6a32c0382ff29b20c808ab115b..b6120848a3fc089b3d87e96bda531b4b2cbc9c78 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -337,6 +337,14 @@ impl<'a> VimTestContext<'a> { let mode = self.mode(); VimBindingTestContext::new(keystrokes, mode, mode, self) } + + pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) { + self.cx.update(|cx| { + let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned()); + let expected_content = expected_content.map(|content| content.to_owned()); + assert_eq!(actual_content, expected_content); + }) + } } impl<'a> Deref for VimTestContext<'a> { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index c3416da471eec1f4ae486353b90bef9a093afae8..17a4272117b4f1ad2f72b74888a0adbfab900942 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -9,9 +9,11 @@ actions!( vim, [ VisualDelete, - VisualChange, VisualLineDelete, - VisualLineChange + VisualChange, + VisualLineChange, + VisualYank, + VisualLineYank, ] ); @@ -20,6 +22,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(change_line); cx.add_action(delete); cx.add_action(delete_line); + cx.add_action(yank); + cx.add_action(yank_line); } pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { @@ -56,8 +60,8 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext 0 { *selection.start.row_mut() -= 1; *selection.start.column_mut() = map.line_len(selection.start.row()); + selection.start = map.clip_point(selection.start, Bias::Left); } selection.end = map.next_line_boundary(selection.end.to_point(map)).1; @@ -161,6 +164,38 @@ pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext }); } +pub fn yank(_: &mut Workspace, _: &VisualYank, 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| { + if !selection.reversed { + // Head is at the end of the selection. Adjust the end position to + // to include the character under the cursor. + *selection.end.column_mut() = selection.end.column() + 1; + selection.end = map.clip_point(selection.end, Bias::Left); + } + }); + }); + copy_selections_content(editor, false, cx); + }); + vim.switch_mode(Mode::Normal, cx); + }); +} + +pub fn yank_line(_: &mut Workspace, _: &VisualLineYank, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + let adjusted = editor.selections.all_adjusted(cx); + editor.change_selections(None, cx, |s| s.select(adjusted)); + copy_selections_content(editor, true, cx); + }); + vim.switch_mode(Mode::Normal, cx); + }); +} + #[cfg(test)] mod test { use indoc::indoc; @@ -521,4 +556,88 @@ mod test { |"}, ); } + + #[gpui::test] + async fn test_visual_yank(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["v", "w", "y"]); + cx.assert("The quick |brown", "The quick |brown"); + cx.assert_clipboard_content(Some("brown")); + let mut cx = cx.binding(["v", "w", "j", "y"]); + cx.assert( + indoc! {" + The |quick brown + fox jumps over + the lazy dog"}, + indoc! {" + The |quick brown + fox jumps over + the lazy dog"}, + ); + cx.assert_clipboard_content(Some(indoc! {" + quick brown + fox jumps ov"})); + cx.assert( + indoc! {" + The quick brown + fox jumps over + the |lazy dog"}, + indoc! {" + The quick brown + fox jumps over + the |lazy dog"}, + ); + cx.assert_clipboard_content(Some("lazy d")); + cx.assert( + indoc! {" + The quick brown + fox jumps |over + the lazy dog"}, + indoc! {" + The quick brown + fox jumps |over + the lazy dog"}, + ); + cx.assert_clipboard_content(Some(indoc! {" + over + t"})); + let mut cx = cx.binding(["v", "b", "k", "y"]); + cx.assert( + indoc! {" + The |quick brown + fox jumps over + the lazy dog"}, + indoc! {" + The |quick brown + fox jumps over + the lazy dog"}, + ); + cx.assert_clipboard_content(Some("The q")); + cx.assert( + indoc! {" + The quick brown + fox jumps over + the |lazy dog"}, + indoc! {" + The quick brown + fox jumps over + the |lazy dog"}, + ); + cx.assert_clipboard_content(Some(indoc! {" + fox jumps over + the l"})); + cx.assert( + indoc! {" + The quick brown + fox jumps |over + the lazy dog"}, + indoc! {" + The quick brown + fox jumps |over + the lazy dog"}, + ); + cx.assert_clipboard_content(Some(indoc! {" + quick brown + fox jumps o"})); + } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3e21e454f2f9f08f93fec6886938e98eb03d8647..40ef2a84ab1ea17391709db07a5ae7aee92d42a4 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -179,8 +179,8 @@ fn main() { cx.observe_global::({ let languages = languages.clone(); - move |settings, _| { - languages.set_theme(&settings.theme.editor.syntax); + move |cx| { + languages.set_theme(&cx.global::().theme.editor.syntax); } }) .detach(); From 11569a869a72f786a9798c53266e28c05c79f824 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Mon, 23 May 2022 11:04:26 -0700 Subject: [PATCH 07/14] in progress working on aborting operators on unhandled editor input --- crates/vim/src/normal.rs | 6 +++--- crates/vim/src/state.rs | 7 +++++++ crates/vim/src/vim.rs | 15 +++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 0d68b29bf9409fd851dde06678f713ecc497349d..48c4ad339a7f741a17127c1e2962d9c6f26b3cf6 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1131,9 +1131,9 @@ mod test { let mut cx = VimTestContext::new(cx, true).await; cx.set_state( indoc! {" - The quick brown - fox ju|mps over - the lazy dog"}, + The quick brown + fox ju|mps over + the lazy dog"}, Mode::Normal, ); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index c4c6d4850b751910691007f958aa9ae43a4dc314..e0ecbc33ad191b699a2d075e7b8dfea629b627ef 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -47,6 +47,13 @@ impl VimState { !matches!(self.mode, Mode::Insert) } + pub fn clip_at_line_end(&self) -> bool { + match self.mode { + Mode::Insert | Mode::Visual | Mode::VisualLine => false, + _ => true, + } + } + pub fn empty_selections_only(&self) -> bool { self.mode != Mode::Visual && self.mode != Mode::VisualLine } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 5ee3f3d38bb11c8d53bb27e2785e42a439c39dbf..c5ab4118f7bfad23f3af708190c64e00a8ea0301 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -10,7 +10,7 @@ mod utils; mod visual; use collections::HashMap; -use editor::{CursorShape, Editor}; +use editor::{CursorShape, Editor, Input}; use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle}; use serde::Deserialize; @@ -41,6 +41,13 @@ pub fn init(cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| vim.push_operator(operator, cx)) }, ); + cx.add_action(|_: &mut Editor, _: &Input, cx| { + if Vim::read(cx).active_operator().is_some() { + cx.defer(|_, cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx))) + } else { + cx.propagate_action() + } + }); cx.observe_global::(|cx| { Vim::update(cx, |state, cx| { @@ -105,7 +112,7 @@ impl Vim { self.sync_editor_options(cx); } - fn active_operator(&mut self) -> Option { + fn active_operator(&self) -> Option { self.state.operator_stack.last().copied() } @@ -122,14 +129,14 @@ impl Vim { fn sync_editor_options(&self, cx: &mut MutableAppContext) { let state = &self.state; - let cursor_shape = state.cursor_shape(); + for editor in self.editors.values() { if let Some(editor) = editor.upgrade(cx) { editor.update(cx, |editor, cx| { if self.enabled { editor.set_cursor_shape(cursor_shape, cx); - editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx); + editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); editor.set_input_enabled(!state.vim_controlled()); editor.selections.line_mode = state.mode == Mode::VisualLine; let context_layer = state.keymap_context_layer(); From e93c49f4f02b3edaddae6a6a4cc0ac433f242357 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 24 May 2022 13:35:57 -0700 Subject: [PATCH 08/14] Unify visual line_mode and non line_mode operators --- assets/keymaps/vim.json | 23 ++- crates/editor/src/display_map.rs | 21 ++- crates/editor/src/editor.rs | 24 +-- crates/editor/src/selections_collection.rs | 7 + crates/vim/src/editor_events.rs | 2 +- crates/vim/src/motion.rs | 3 +- crates/vim/src/normal.rs | 10 +- crates/vim/src/state.rs | 12 +- crates/vim/src/utils.rs | 3 +- crates/vim/src/vim.rs | 28 ++-- crates/vim/src/vim_test_context.rs | 21 ++- crates/vim/src/visual.rs | 167 ++++++++------------- 12 files changed, 142 insertions(+), 179 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index f1dca985ab687aec250c3624e79ee1515c96c8e5..c1e5d7db8c576ca388653c0662179046000a260d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -9,7 +9,7 @@ } ], "h": "vim::Left", - "backspace": "editor::Backspace", // "vim::Left", + "backspace": "vim::Left", "j": "vim::Down", "k": "vim::Up", "l": "vim::Right", @@ -75,11 +75,19 @@ "shift-O": "vim::InsertLineAbove", "v": [ "vim::SwitchMode", - "Visual" + { + "Visual": { + "line": false + } + } ], "shift-V": [ "vim::SwitchMode", - "VisualLine" + { + "Visual": { + "line": true + } + } ], "p": "vim::Paste", "u": "editor::Undo", @@ -131,15 +139,6 @@ "y": "vim::VisualYank" } }, - { - "context": "Editor && vim_mode == visual_line", - "bindings": { - "c": "vim::VisualLineChange", - "d": "vim::VisualLineDelete", - "x": "vim::VisualLineDelete", - "y": "vim::VisualLineYank" - } - }, { "context": "Editor && vim_mode == insert", "bindings": { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index f76d23a187958a057f085a8b328169a99d3b508d..4378db540700dfd142551347832e10668b6f11e7 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -279,16 +279,21 @@ impl DisplaySnapshot { } } - pub fn expand_to_line(&self, mut range: Range) -> Range { - (range.start, _) = self.prev_line_boundary(range.start); - (range.end, _) = self.next_line_boundary(range.end); - - if range.is_empty() && range.start.row > 0 { - range.start.row -= 1; - range.start.column = self.buffer_snapshot.line_len(range.start.row); + pub fn expand_to_line(&self, range: Range) -> Range { + let mut new_start = self.prev_line_boundary(range.start).0; + let mut new_end = self.next_line_boundary(range.end).0; + + if new_start.row == range.start.row && new_end.row == range.end.row { + if new_end.row < self.buffer_snapshot.max_point().row { + new_end.row += 1; + new_end.column = 0; + } else if new_start.row > 0 { + new_start.row -= 1; + new_start.column = self.buffer_snapshot.line_len(new_start.row); + } } - range + new_start..new_end } fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f46fa831412603e91280f46e8fb5c5df8ea873d1..37080789d4a813465a84fd0d528ebd22b88a89ab 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1866,7 +1866,10 @@ impl Editor { let snapshot = buffer.read(cx); old_selections .iter() - .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) + .map(|s| { + let anchor = snapshot.anchor_after(s.end); + s.map(|_| anchor.clone()) + }) .collect::>() }; buffer.edit_with_autoindent( @@ -1878,25 +1881,8 @@ impl Editor { anchors }); - let selections = { - let snapshot = this.buffer.read(cx).read(cx); - selection_anchors - .into_iter() - .map(|(id, goal, position)| { - let position = position.to_offset(&snapshot); - Selection { - id, - start: position, - end: position, - goal, - reversed: false, - } - }) - .collect() - }; - this.change_selections(Some(Autoscroll::Fit), cx, |s| { - s.select(selections); + s.select_anchors(selection_anchors); }) }); } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index db6571cee1f3c3d884912f4e269360daa5070bde..b77e55c5cf0bb65697b05e99a56a0f78cd8aa674 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -22,6 +22,13 @@ pub struct PendingSelection { pub mode: SelectMode, } +#[derive(Clone)] +pub enum LineMode { + None, + WithNewline, + WithoutNewline, +} + #[derive(Clone)] pub struct SelectionsCollection { display_map: ModelHandle, diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 1d477313846033fe2eed1d34a215a06c8e87fd68..092d369058270e8e12a1b3da03630cdc7cf8495f 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -58,7 +58,7 @@ 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) + vim.switch_mode(Mode::Visual { line: false }, cx) } }) } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 16533f89f1b6a4481661f841266b0c6108849dca..221898c056220246cfd59d56b50f23639ce5953e 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -111,8 +111,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::Visual { .. } => visual_motion(motion, cx), Mode::Insert => { // Shouldn't execute a motion in insert mode. Ignoring } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 48c4ad339a7f741a17127c1e2962d9c6f26b3cf6..26336838816bb17e56e548f9ab38a82c5eb75a0b 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -777,14 +777,8 @@ mod test { | The quick"}, ); - cx.assert( - indoc! {" - | - The quick"}, - indoc! {" - | - The quick"}, - ); + // Indoc disallows trailing whitspace. + cx.assert(" | \nThe quick", " | \nThe quick"); } #[gpui::test] diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index e0ecbc33ad191b699a2d075e7b8dfea629b627ef..a08b8bd2d2103126a8d8c521c9737f9d2c1fe316 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -6,8 +6,7 @@ use serde::Deserialize; pub enum Mode { Normal, Insert, - Visual, - VisualLine, + Visual { line: bool }, } impl Default for Mode { @@ -38,7 +37,7 @@ pub struct VimState { impl VimState { pub fn cursor_shape(&self) -> CursorShape { match self.mode { - Mode::Normal | Mode::Visual | Mode::VisualLine => CursorShape::Block, + Mode::Normal | Mode::Visual { .. } => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -49,13 +48,13 @@ impl VimState { pub fn clip_at_line_end(&self) -> bool { match self.mode { - Mode::Insert | Mode::Visual | Mode::VisualLine => false, + Mode::Insert | Mode::Visual { .. } => false, _ => true, } } pub fn empty_selections_only(&self) -> bool { - self.mode != Mode::Visual && self.mode != Mode::VisualLine + !matches!(self.mode, Mode::Visual { .. }) } pub fn keymap_context_layer(&self) -> Context { @@ -64,8 +63,7 @@ impl VimState { "vim_mode".to_string(), match self.mode { Mode::Normal => "normal", - Mode::Visual => "visual", - Mode::VisualLine => "visual_line", + Mode::Visual { .. } => "visual", Mode::Insert => "insert", } .to_string(), diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index 1cd5f1360860ba087101ab04206cadd3424301ba..cb6a736c6344d0c91cfdb7b5b22458ac0e9fed2e 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,9 +1,8 @@ use editor::{ClipboardSelection, Editor}; use gpui::{ClipboardItem, MutableAppContext}; -use language::Point; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) { - let selections = editor.selections.all::(cx); + 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/vim.rs b/crates/vim/src/vim.rs index c5ab4118f7bfad23f3af708190c64e00a8ea0301..89647b56e29f3e83c8174309e731850f9656f961 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -10,7 +10,7 @@ mod utils; mod visual; use collections::HashMap; -use editor::{CursorShape, Editor, Input}; +use editor::{Bias, CursorShape, Editor, Input}; use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle}; use serde::Deserialize; @@ -43,7 +43,8 @@ pub fn init(cx: &mut MutableAppContext) { ); cx.add_action(|_: &mut Editor, _: &Input, cx| { if Vim::read(cx).active_operator().is_some() { - cx.defer(|_, cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx))) + // Defer without updating editor + MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx))) } else { cx.propagate_action() } @@ -138,7 +139,8 @@ impl Vim { editor.set_cursor_shape(cursor_shape, cx); editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); editor.set_input_enabled(!state.vim_controlled()); - editor.selections.line_mode = state.mode == Mode::VisualLine; + editor.selections.line_mode = + matches!(state.mode, Mode::Visual { line: true }); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer); } else { @@ -149,13 +151,17 @@ impl Vim { editor.remove_keymap_context_layer::(); } - if state.empty_selections_only() { - editor.change_selections(None, cx, |s| { - s.move_with(|_, selection| { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + selection.set_head( + map.clip_point(selection.head(), Bias::Left), + selection.goal, + ); + if state.empty_selections_only() { selection.collapse_to(selection.head(), selection.goal) - }); - }) - } + } + }); + }) }); } } @@ -190,9 +196,9 @@ mod test { assert_eq!(cx.mode(), Mode::Normal); cx.simulate_keystrokes(["h", "h", "h", "l"]); assert_eq!(cx.editor_text(), "hjkl".to_owned()); - cx.assert_editor_state("hj|kl"); + cx.assert_editor_state("h|jkl"); cx.simulate_keystrokes(["i", "T", "e", "s", "t"]); - cx.assert_editor_state("hjTest|kl"); + cx.assert_editor_state("hTest|jkl"); // Disabling and enabling resets to normal mode assert_eq!(cx.mode(), Mode::Insert); diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index b6120848a3fc089b3d87e96bda531b4b2cbc9c78..b4a93e158c30f3c5bec7a2e2508792c9586c165b 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -12,7 +12,7 @@ use util::{ set_eq, test::{marked_text, marked_text_ranges_by, SetEqError}, }; -use workspace::{AppState, WorkspaceHandle}; +use workspace::{pane, AppState, WorkspaceHandle}; use crate::{state::Operator, *}; @@ -26,6 +26,7 @@ impl<'a> VimTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> { cx.update(|cx| { editor::init(cx); + pane::init(cx); crate::init(cx); settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap(); @@ -269,9 +270,12 @@ impl<'a> VimTestContext<'a> { panic!( indoc! {" Editor has extra selection - Extra Selection Location: {} - Asserted selections: {} - Actual selections: {}"}, + Extra Selection Location: + {} + Asserted selections: + {} + Actual selections: + {}"}, location_text, asserted_selections, actual_selections, ); } @@ -279,9 +283,12 @@ impl<'a> VimTestContext<'a> { panic!( indoc! {" Editor is missing empty selection - Missing Selection Location: {} - Asserted selections: {} - Actual selections: {}"}, + Missing Selection Location: + {} + Asserted selections: + {} + Actual selections: + {}"}, location_text, asserted_selections, actual_selections, ); } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 17a4272117b4f1ad2f72b74888a0adbfab900942..665b468b733a1c53963318353236affe0d0fc66c 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,29 +1,17 @@ use collections::HashMap; -use editor::{Autoscroll, Bias}; +use editor::{display_map::ToDisplayPoint, Autoscroll, Bias}; use gpui::{actions, MutableAppContext, ViewContext}; +use language::SelectionGoal; use workspace::Workspace; use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; -actions!( - vim, - [ - VisualDelete, - VisualLineDelete, - VisualChange, - VisualLineChange, - VisualYank, - VisualLineYank, - ] -); +actions!(vim, [VisualDelete, VisualChange, VisualYank,]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(change); - cx.add_action(change_line); cx.add_action(delete); - cx.add_action(delete_line); cx.add_action(yank); - cx.add_action(yank_line); } pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { @@ -32,7 +20,6 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.move_with(|map, selection| { let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal); - let new_head = map.clip_at_line_end(new_head); let was_reversed = selection.reversed; selection.set_head(new_head, goal); @@ -57,7 +44,12 @@ 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); - - let adjusted = editor.selections.all_adjusted(cx); - editor.change_selections(None, cx, |s| s.select(adjusted)); - copy_selections_content(editor, true, cx); - editor.insert("", cx); - }); - vim.switch_mode(Mode::Insert, cx); - }); -} + if line_mode { + let range = selection.map(|p| p.to_point(map)).range(); + let expanded_range = map.expand_to_line(range); + // If we are at the last line, the anchor needs to be after the newline so that + // it is on a line of its own. Otherwise, the anchor may be after the newline + let anchor = if expanded_range.end == map.buffer_snapshot.max_point() { + map.buffer_snapshot.anchor_after(expanded_range.end) + } else { + map.buffer_snapshot.anchor_before(expanded_range.start) + }; -pub fn delete(_: &mut Workspace, _: &VisualDelete, 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| { - if !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); + edits.push((expanded_range, "\n")); + new_selections.push(selection.map(|_| anchor.clone())); + } else { + let range = selection.map(|p| p.to_point(map)).range(); + let anchor = map.buffer_snapshot.anchor_after(range.end); + edits.push((range, "")); + new_selections.push(selection.map(|_| anchor.clone())); } }); }); - copy_selections_content(editor, false, cx); - editor.insert("", cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); + copy_selections_content(editor, editor.selections.line_mode, cx); + editor.edit_with_autoindent(edits, 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) - }); + s.select_anchors(new_selections); }); }); - vim.switch_mode(Mode::Normal, cx); + vim.switch_mode(Mode::Insert, cx); }); } -pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext) { +pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_columns: HashMap<_, _> = Default::default(); + let line_mode = editor.selections.line_mode; editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.move_with(|map, selection| { - original_columns.insert(selection.id, selection.head().column()); - 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; + if line_mode { + original_columns + .insert(selection.id, selection.head().to_point(&map).column); + } else if !selection.reversed { + // Head is at the end of the selection. Adjust the end position to + // to include the character under the cursor. + *selection.end.column_mut() = selection.end.column() + 1; selection.end = map.clip_point(selection.end, Bias::Right); - // 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.start = map.clip_point(selection.start, Bias::Left); } - - selection.end = map.next_line_boundary(selection.end.to_point(map)).1; }); }); - copy_selections_content(editor, true, cx); + copy_selections_content(editor, line_mode, cx); 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(); + let mut cursor = selection.head().to_point(map); + if let Some(column) = original_columns.get(&selection.id) { - *cursor.column_mut() = *column + cursor.column = *column } - cursor = map.clip_point(cursor, Bias::Left); + let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); selection.collapse_to(cursor, selection.goal) }); }); @@ -168,9 +133,10 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, 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| { + let line_mode = editor.selections.line_mode; + editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { - if !selection.reversed { + if !line_mode && !selection.reversed { // Head is at the end of the selection. Adjust the end position to // to include the character under the cursor. *selection.end.column_mut() = selection.end.column() + 1; @@ -178,19 +144,12 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) } }); }); - copy_selections_content(editor, false, cx); - }); - vim.switch_mode(Mode::Normal, cx); - }); -} - -pub fn yank_line(_: &mut Workspace, _: &VisualLineYank, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let adjusted = editor.selections.all_adjusted(cx); - editor.change_selections(None, cx, |s| s.select(adjusted)); - copy_selections_content(editor, true, cx); + copy_selections_content(editor, line_mode, cx); + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + selection.collapse_to(selection.start, SelectionGoal::None) + }); + }); }); vim.switch_mode(Mode::Normal, cx); }); @@ -205,7 +164,9 @@ mod test { #[gpui::test] async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual); + let mut cx = cx + .binding(["v", "w", "j"]) + .mode_after(Mode::Visual { line: false }); cx.assert( indoc! {" The |quick brown @@ -236,7 +197,9 @@ mod test { fox jumps [over }the lazy dog"}, ); - let mut cx = cx.binding(["v", "b", "k"]).mode_after(Mode::Visual); + let mut cx = cx + .binding(["v", "b", "k"]) + .mode_after(Mode::Visual { line: false }); cx.assert( indoc! {" The |quick brown @@ -576,7 +539,7 @@ mod test { ); cx.assert_clipboard_content(Some(indoc! {" quick brown - fox jumps ov"})); + fox jumps o"})); cx.assert( indoc! {" The quick brown @@ -608,7 +571,7 @@ mod test { fox jumps over the lazy dog"}, indoc! {" - The |quick brown + |The quick brown fox jumps over the lazy dog"}, ); @@ -620,8 +583,8 @@ mod test { the |lazy dog"}, indoc! {" The quick brown - fox jumps over - the |lazy dog"}, + |fox jumps over + the lazy dog"}, ); cx.assert_clipboard_content(Some(indoc! {" fox jumps over @@ -632,8 +595,8 @@ mod test { fox jumps |over the lazy dog"}, indoc! {" - The quick brown - fox jumps |over + The |quick brown + fox jumps over the lazy dog"}, ); cx.assert_clipboard_content(Some(indoc! {" From 98f9575653c4fad6ece47e85813d966e9bda199f Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 25 May 2022 10:22:49 -0700 Subject: [PATCH 09/14] WIP --- crates/editor/src/editor.rs | 304 ++++++++++++---------------- crates/editor/src/test.rs | 312 ++++++++++++++++++++++++++++- crates/vim/src/vim_test_context.rs | 263 +++--------------------- 3 files changed, 457 insertions(+), 422 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 37080789d4a813465a84fd0d528ebd22b88a89ab..bb181a6ea1d2c374261a033288345f2da8ff722c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5,8 +5,8 @@ pub mod movement; mod multi_buffer; pub mod selections_collection; -#[cfg(test)] -mod test; +#[cfg(any(test, feature = "test-support"))] +pub mod test; use aho_corasick::AhoCorasick; use anyhow::Result; @@ -6017,7 +6017,9 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { - use crate::test::{assert_text_with_selections, select_ranges}; + use crate::test::{ + assert_text_with_selections, build_editor, select_ranges, EditorTestContext, + }; use super::*; use gpui::{ @@ -7289,117 +7291,62 @@ mod tests { } #[gpui::test] - fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { - cx.set_global(Settings::test(cx)); - let buffer = MultiBuffer::build_simple( - indoc! {" - one two - three - four"}, - cx, - ); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); - - view.update(cx, |view, cx| { - // two selections on the same line - select_ranges( - view, - indoc! {" - [one] [two] - three - four"}, - cx, - ); - - // indent from mid-tabstop to full tabstop - view.tab(&Tab, cx); - assert_text_with_selections( - view, - indoc! {" - [one] [two] - three - four"}, - cx, - ); - - // outdent from 1 tabstop to 0 tabstops - view.tab_prev(&TabPrev, cx); - assert_text_with_selections( - view, - indoc! {" - [one] [two] - three - four"}, - cx, - ); - - // select across line ending - select_ranges( - view, - indoc! {" - one two - t[hree - ] four"}, - cx, - ); + async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; - // indent and outdent affect only the preceding line - view.tab(&Tab, cx); - assert_text_with_selections( - view, - indoc! {" - one two - t[hree - ] four"}, - cx, - ); - view.tab_prev(&TabPrev, cx); - assert_text_with_selections( - view, - indoc! {" - one two - t[hree - ] four"}, - cx, - ); - - // Ensure that indenting/outdenting works when the cursor is at column 0. - select_ranges( - view, - indoc! {" - one two - []three - four"}, - cx, - ); - view.tab(&Tab, cx); - assert_text_with_selections( - view, - indoc! {" - one two - []three - four"}, - cx, - ); + cx.set_state(indoc! {" + [one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + [one} [two} + three + four"}); - select_ranges( - view, - indoc! {" - one two - [] three - four"}, - cx, - ); - view.tab_prev(&TabPrev, cx); - assert_text_with_selections( - view, - indoc! {" - one two - []three - four"}, - cx, - ); - }); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + [one} [two} + three + four"}); + + // select across line ending + cx.set_state(indoc! {" + one two + t[hree + } four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + t[hree + } four"}); + + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t[hree + } four"}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + |three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + |three + four"}); + + cx.set_state(indoc! {" + one two + | three + four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + |three + four"}); } #[gpui::test] @@ -7508,73 +7455,74 @@ mod tests { } #[gpui::test] - fn test_backspace(cx: &mut gpui::MutableAppContext) { - cx.set_global(Settings::test(cx)); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(MultiBuffer::build_simple("", cx), cx) - }); - - view.update(cx, |view, cx| { - view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx); - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - // an empty selection - the preceding character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ]) - }); - view.backspace(&Backspace, cx); - assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n"); - - view.set_text(" one\n two\n three\n four", cx); - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - // cursors at the the end of leading indent - last indent is deleted - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8), - // cursors inside leading indent - overlapping indent deletions are coalesced - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6), - // cursor at the beginning of a line - preceding newline is deleted - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - // selection inside leading indent - only the selected character is deleted - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3), - ]) - }); - view.backspace(&Backspace, cx); - assert_eq!(view.text(cx), "one\n two\n three four"); - }); + async fn test_backspace(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + // Basic backspace + cx.set_state(indoc! {" + on|e two three + fou[r} five six + seven {eight nine + ]ten"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + o|e two three + fou| five six + seven |ten"}); + + // Test backspace inside and around indents + cx.set_state(indoc! {" + zero + |one + |two + | | | three + | | four"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + zero + |one + |two + | three| four"}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The |quick |brown + fox jumps over + the lazy dog + |The qu[ick b}rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + | + fox jumps over + the lazy dog|"}); } #[gpui::test] - fn test_delete(cx: &mut gpui::MutableAppContext) { - cx.set_global(Settings::test(cx)); - let buffer = - MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - // an empty selection - the following character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ]) - }); - view.delete(&Delete, cx); - }); - - assert_eq!( - buffer.read(cx).read(cx).text(), - "on two three\nfou five six\nseven ten\n" - ); + async fn test_delete(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + on|e two three + fou[r} five six + seven {eight nine + ]ten"}); + cx.update_editor(|e, cx| e.delete(&Delete, cx)); + cx.assert_editor_state(indoc! {" + on| two three + fou| five six + seven |ten"}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The |quick |brown + fox {jum]ps over| + the lazy dog + |The qu[ick b}rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + | + the lazy dog|"}); } #[gpui::test] @@ -9795,10 +9743,6 @@ mod tests { point..point } - fn build_editor(buffer: ModelHandle, cx: &mut ViewContext) -> Editor { - Editor::new(EditorMode::Full, buffer, None, None, None, cx) - } - fn assert_selection_ranges( marked_text: &str, selection_marker_pairs: Vec<(char, char)>, diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index cb064be5459c3abd7af6ae9c9ed56aca06758acd..a8bcc94ee29a7d2829f8e5734a3e4d6be679650b 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,9 +1,20 @@ -use gpui::ViewContext; -use util::test::{marked_text, marked_text_ranges}; +use std::ops::{Deref, DerefMut, Range}; + +use indoc::indoc; + +use collections::BTreeMap; +use gpui::{keymap::Keystroke, ModelHandle, ViewContext, ViewHandle}; +use itertools::{Either, Itertools}; +use language::Selection; +use settings::Settings; +use util::{ + set_eq, + test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError}, +}; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, - DisplayPoint, Editor, MultiBuffer, + Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, }; #[cfg(test)] @@ -56,3 +67,298 @@ pub fn assert_text_with_selections( assert_eq!(editor.text(cx), unmarked_text); assert_eq!(editor.selections.ranges(cx), text_ranges); } + +pub(crate) fn build_editor( + buffer: ModelHandle, + cx: &mut ViewContext, +) -> Editor { + Editor::new(EditorMode::Full, buffer, None, None, None, cx) +} + +pub struct EditorTestContext<'a> { + pub cx: &'a mut gpui::TestAppContext, + pub window_id: usize, + pub editor: ViewHandle, +} + +impl<'a> EditorTestContext<'a> { + pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { + let (window_id, editor) = cx.update(|cx| { + cx.set_global(Settings::test(cx)); + crate::init(cx); + + let (window_id, editor) = cx.add_window(Default::default(), |cx| { + build_editor(MultiBuffer::build_simple("", cx), cx) + }); + + editor.update(cx, |_, cx| cx.focus_self()); + + (window_id, editor) + }); + + Self { + cx, + window_id, + editor, + } + } + + pub fn update_editor(&mut self, update: F) -> T + where + F: FnOnce(&mut Editor, &mut ViewContext) -> T, + { + self.editor.update(self.cx, update) + } + + pub fn editor_text(&mut self) -> String { + self.editor + .update(self.cx, |editor, cx| editor.snapshot(cx).text()) + } + + pub fn simulate_keystroke(&mut self, keystroke_text: &str) { + let keystroke = Keystroke::parse(keystroke_text).unwrap(); + let input = if keystroke.modified() { + None + } else { + Some(keystroke.key.clone()) + }; + self.cx + .dispatch_keystroke(self.window_id, keystroke, input, false); + } + + pub fn simulate_keystrokes(&mut self, keystroke_texts: [&str; COUNT]) { + for keystroke_text in keystroke_texts.into_iter() { + self.simulate_keystroke(keystroke_text); + } + } + + // Sets the editor state via a marked string. + // `|` characters represent empty selections + // `[` to `}` represents a non empty selection with the head at `}` + // `{` to `]` represents a non empty selection with the head at `{` + pub fn set_state(&mut self, text: &str) { + self.editor.update(self.cx, |editor, cx| { + let (text_with_ranges, empty_selections) = marked_text(&text); + let (unmarked_text, mut selection_ranges) = + marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]); + editor.set_text(unmarked_text, cx); + + let mut selections: Vec> = empty_selections + .into_iter() + .map(|offset| offset..offset) + .collect(); + selections.extend(selection_ranges.remove(&('{', ']')).unwrap_or_default()); + selections.extend(selection_ranges.remove(&('[', '}')).unwrap_or_default()); + + editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections)); + }) + } + + // Asserts the editor state via a marked string. + // `|` characters represent empty selections + // `[` to `}` represents a non empty selection with the head at `}` + // `{` to `]` represents a non empty selection with the head at `{` + pub fn assert_editor_state(&mut self, text: &str) { + let (text_with_ranges, expected_empty_selections) = marked_text(&text); + let (unmarked_text, mut selection_ranges) = + marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]); + let editor_text = self.editor_text(); + assert_eq!( + editor_text, unmarked_text, + "Unmarked text doesn't match editor text" + ); + + let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default(); + let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default(); + + self.assert_selections( + expected_empty_selections, + expected_reverse_selections, + expected_forward_selections, + Some(text.to_string()), + ) + } + + pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { + let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) = + expected_selections.into_iter().partition_map(|selection| { + if selection.is_empty() { + Either::Left(selection.head()) + } else { + Either::Right(selection) + } + }); + + let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) = + expected_non_empty_selections + .into_iter() + .partition_map(|selection| { + let range = selection.start..selection.end; + if selection.reversed { + Either::Left(range) + } else { + Either::Right(range) + } + }); + + self.assert_selections( + expected_empty_selections, + expected_reverse_selections, + expected_forward_selections, + None, + ) + } + + fn assert_selections( + &mut self, + expected_empty_selections: Vec, + expected_reverse_selections: Vec>, + expected_forward_selections: Vec>, + asserted_text: Option, + ) { + let (empty_selections, reverse_selections, forward_selections) = + self.editor.read_with(self.cx, |editor, cx| { + let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor + .selections + .all::(cx) + .into_iter() + .partition_map(|selection| { + if selection.is_empty() { + Either::Left(selection.head()) + } else { + Either::Right(selection) + } + }); + + let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) = + non_empty_selections.into_iter().partition_map(|selection| { + let range = selection.start..selection.end; + if selection.reversed { + Either::Left(range) + } else { + Either::Right(range) + } + }); + (empty_selections, reverse_selections, forward_selections) + }); + + let asserted_selections = asserted_text.unwrap_or_else(|| { + self.insert_markers( + &expected_empty_selections, + &expected_reverse_selections, + &expected_forward_selections, + ) + }); + let actual_selections = + self.insert_markers(&empty_selections, &reverse_selections, &forward_selections); + + let unmarked_text = self.editor_text(); + let all_eq: Result<(), SetEqError> = + set_eq!(expected_empty_selections, empty_selections) + .map_err(|err| { + err.map(|missing| { + let mut error_text = unmarked_text.clone(); + error_text.insert(missing, '|'); + error_text + }) + }) + .and_then(|_| { + set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| { + err.map(|missing| { + let mut error_text = unmarked_text.clone(); + error_text.insert(missing.start, '{'); + error_text.insert(missing.end, ']'); + error_text + }) + }) + }) + .and_then(|_| { + set_eq!(expected_forward_selections, forward_selections).map_err(|err| { + err.map(|missing| { + let mut error_text = unmarked_text.clone(); + error_text.insert(missing.start, '['); + error_text.insert(missing.end, '}'); + error_text + }) + }) + }); + + match all_eq { + Err(SetEqError::LeftMissing(location_text)) => { + panic!( + indoc! {" + Editor has extra selection + Extra Selection Location: + {} + Asserted selections: + {} + Actual selections: + {}"}, + location_text, asserted_selections, actual_selections, + ); + } + Err(SetEqError::RightMissing(location_text)) => { + panic!( + indoc! {" + Editor is missing empty selection + Missing Selection Location: + {} + Asserted selections: + {} + Actual selections: + {}"}, + location_text, asserted_selections, actual_selections, + ); + } + _ => {} + } + } + + fn insert_markers( + &mut self, + empty_selections: &Vec, + reverse_selections: &Vec>, + forward_selections: &Vec>, + ) -> String { + let mut editor_text_with_selections = self.editor_text(); + let mut selection_marks = BTreeMap::new(); + for offset in empty_selections { + selection_marks.insert(offset, '|'); + } + for range in reverse_selections { + selection_marks.insert(&range.start, '{'); + selection_marks.insert(&range.end, ']'); + } + for range in forward_selections { + selection_marks.insert(&range.start, '['); + selection_marks.insert(&range.end, '}'); + } + for (offset, mark) in selection_marks.into_iter().rev() { + editor_text_with_selections.insert(*offset, mark); + } + + editor_text_with_selections + } + + pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) { + self.cx.update(|cx| { + let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned()); + let expected_content = expected_content.map(|content| content.to_owned()); + assert_eq!(actual_content, expected_content); + }) + } +} + +impl<'a> Deref for EditorTestContext<'a> { + type Target = gpui::TestAppContext; + + fn deref(&self) -> &Self::Target { + self.cx + } +} + +impl<'a> DerefMut for EditorTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index b4a93e158c30f3c5bec7a2e2508792c9586c165b..52d4778b383c1bc97c37df2f7b9162f7d3d9fdb8 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -1,25 +1,14 @@ -use std::ops::{Deref, DerefMut, Range}; +use std::ops::{Deref, DerefMut}; -use collections::BTreeMap; -use itertools::{Either, Itertools}; - -use editor::{display_map::ToDisplayPoint, Autoscroll}; -use gpui::{json::json, keymap::Keystroke, ViewHandle}; -use indoc::indoc; -use language::Selection; +use editor::test::EditorTestContext; +use gpui::json::json; use project::Project; -use util::{ - set_eq, - test::{marked_text, marked_text_ranges_by, SetEqError}, -}; use workspace::{pane, AppState, WorkspaceHandle}; use crate::{state::Operator, *}; pub struct VimTestContext<'a> { - cx: &'a mut gpui::TestAppContext, - window_id: usize, - editor: ViewHandle, + cx: EditorTestContext<'a>, } impl<'a> VimTestContext<'a> { @@ -70,9 +59,11 @@ impl<'a> VimTestContext<'a> { editor.update(cx, |_, cx| cx.focus_self()); Self { - cx, - window_id, - editor, + cx: EditorTestContext { + cx, + window_id, + editor, + }, } } @@ -101,225 +92,13 @@ impl<'a> VimTestContext<'a> { .read(|cx| cx.global::().state.operator_stack.last().copied()) } - pub fn editor_text(&mut self) -> String { - self.editor - .update(self.cx, |editor, cx| editor.snapshot(cx).text()) - } - - pub fn simulate_keystroke(&mut self, keystroke_text: &str) { - let keystroke = Keystroke::parse(keystroke_text).unwrap(); - let input = if keystroke.modified() { - None - } else { - Some(keystroke.key.clone()) - }; - self.cx - .dispatch_keystroke(self.window_id, keystroke, input, false); - } - - pub fn simulate_keystrokes(&mut self, keystroke_texts: [&str; COUNT]) { - for keystroke_text in keystroke_texts.into_iter() { - self.simulate_keystroke(keystroke_text); - } - } - pub fn set_state(&mut self, text: &str, mode: Mode) { - self.cx - .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))); - self.editor.update(self.cx, |editor, cx| { - let (unmarked_text, markers) = marked_text(&text); - editor.set_text(unmarked_text, cx); - let cursor_offset = markers[0]; - editor.change_selections(Some(Autoscroll::Fit), cx, |s| { - s.replace_cursors_with(|map| vec![cursor_offset.to_display_point(map)]) - }); - }) - } - - // Asserts the editor state via a marked string. - // `|` characters represent empty selections - // `[` to `}` represents a non empty selection with the head at `}` - // `{` to `]` represents a non empty selection with the head at `{` - pub fn assert_editor_state(&mut self, text: &str) { - let (text_with_ranges, expected_empty_selections) = marked_text(&text); - let (unmarked_text, mut selection_ranges) = - marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]); - let editor_text = self.editor_text(); - assert_eq!( - editor_text, unmarked_text, - "Unmarked text doesn't match editor text" - ); - - let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default(); - let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default(); - - self.assert_selections( - expected_empty_selections, - expected_reverse_selections, - expected_forward_selections, - Some(text.to_string()), - ) - } - - pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { - let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) = - expected_selections.into_iter().partition_map(|selection| { - if selection.is_empty() { - Either::Left(selection.head()) - } else { - Either::Right(selection) - } - }); - - let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) = - expected_non_empty_selections - .into_iter() - .partition_map(|selection| { - let range = selection.start..selection.end; - if selection.reversed { - Either::Left(range) - } else { - Either::Right(range) - } - }); - - self.assert_selections( - expected_empty_selections, - expected_reverse_selections, - expected_forward_selections, - None, - ) - } - - fn assert_selections( - &mut self, - expected_empty_selections: Vec, - expected_reverse_selections: Vec>, - expected_forward_selections: Vec>, - asserted_text: Option, - ) { - let (empty_selections, reverse_selections, forward_selections) = - self.editor.read_with(self.cx, |editor, cx| { - let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor - .selections - .all::(cx) - .into_iter() - .partition_map(|selection| { - if selection.is_empty() { - Either::Left(selection.head()) - } else { - Either::Right(selection) - } - }); - - let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) = - non_empty_selections.into_iter().partition_map(|selection| { - let range = selection.start..selection.end; - if selection.reversed { - Either::Left(range) - } else { - Either::Right(range) - } - }); - (empty_selections, reverse_selections, forward_selections) - }); - - let asserted_selections = asserted_text.unwrap_or_else(|| { - self.insert_markers( - &expected_empty_selections, - &expected_reverse_selections, - &expected_forward_selections, - ) + self.cx.update(|cx| { + Vim::update(cx, |vim, cx| { + vim.switch_mode(mode, cx); + }) }); - let actual_selections = - self.insert_markers(&empty_selections, &reverse_selections, &forward_selections); - - let unmarked_text = self.editor_text(); - let all_eq: Result<(), SetEqError> = - set_eq!(expected_empty_selections, empty_selections) - .map_err(|err| { - err.map(|missing| { - let mut error_text = unmarked_text.clone(); - error_text.insert(missing, '|'); - error_text - }) - }) - .and_then(|_| { - set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| { - err.map(|missing| { - let mut error_text = unmarked_text.clone(); - error_text.insert(missing.start, '{'); - error_text.insert(missing.end, ']'); - error_text - }) - }) - }) - .and_then(|_| { - set_eq!(expected_forward_selections, forward_selections).map_err(|err| { - err.map(|missing| { - let mut error_text = unmarked_text.clone(); - error_text.insert(missing.start, '['); - error_text.insert(missing.end, '}'); - error_text - }) - }) - }); - - match all_eq { - Err(SetEqError::LeftMissing(location_text)) => { - panic!( - indoc! {" - Editor has extra selection - Extra Selection Location: - {} - Asserted selections: - {} - Actual selections: - {}"}, - location_text, asserted_selections, actual_selections, - ); - } - Err(SetEqError::RightMissing(location_text)) => { - panic!( - indoc! {" - Editor is missing empty selection - Missing Selection Location: - {} - Asserted selections: - {} - Actual selections: - {}"}, - location_text, asserted_selections, actual_selections, - ); - } - _ => {} - } - } - - fn insert_markers( - &mut self, - empty_selections: &Vec, - reverse_selections: &Vec>, - forward_selections: &Vec>, - ) -> String { - let mut editor_text_with_selections = self.editor_text(); - let mut selection_marks = BTreeMap::new(); - for offset in empty_selections { - selection_marks.insert(offset, '|'); - } - for range in reverse_selections { - selection_marks.insert(&range.start, '{'); - selection_marks.insert(&range.end, ']'); - } - for range in forward_selections { - selection_marks.insert(&range.start, '['); - selection_marks.insert(&range.end, '}'); - } - for (offset, mark) in selection_marks.into_iter().rev() { - editor_text_with_selections.insert(*offset, mark); - } - - editor_text_with_selections + self.cx.set_state(text); } pub fn assert_binding( @@ -331,8 +110,8 @@ impl<'a> VimTestContext<'a> { mode_after: Mode, ) { self.set_state(initial_state, initial_mode); - self.simulate_keystrokes(keystrokes); - self.assert_editor_state(state_after); + self.cx.simulate_keystrokes(keystrokes); + self.cx.assert_editor_state(state_after); assert_eq!(self.mode(), mode_after); assert_eq!(self.active_operator(), None); } @@ -355,10 +134,16 @@ impl<'a> VimTestContext<'a> { } impl<'a> Deref for VimTestContext<'a> { - type Target = gpui::TestAppContext; + type Target = EditorTestContext<'a>; fn deref(&self) -> &Self::Target { - self.cx + &self.cx + } +} + +impl<'a> DerefMut for VimTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx } } From e104cb94e77592bca13ce461108e6c566c67bcae Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 25 May 2022 14:13:18 -0700 Subject: [PATCH 10/14] fix bug in marked_range utils --- crates/editor/src/editor.rs | 220 +++++++++++----------------- crates/editor/src/test.rs | 130 ++++++++-------- crates/util/src/test/marked_text.rs | 96 ++++++++---- 3 files changed, 218 insertions(+), 228 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bb181a6ea1d2c374261a033288345f2da8ff722c..e7af70f303540d0d2a282f9f52fc685039002925 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2735,28 +2735,31 @@ impl Editor { pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(cx); - for selection in &mut selections { - if selection.is_empty() && !self.selections.line_mode { - let old_head = selection.head(); - let mut new_head = - movement::left(&display_map, old_head.to_display_point(&display_map)) - .to_point(&display_map); - if let Some((buffer, line_buffer_range)) = display_map - .buffer_snapshot - .buffer_line_for_row(old_head.row) - { - let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row); - let language_name = buffer.language().map(|language| language.name()); - let indent = cx.global::().tab_size(language_name.as_deref()); - if old_head.column <= indent_column && old_head.column > 0 { - new_head = cmp::min( - new_head, - Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), - ); + if !self.selections.line_mode { + for selection in &mut selections { + if selection.is_empty() { + let old_head = selection.head(); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .to_point(&display_map); + if let Some((buffer, line_buffer_range)) = display_map + .buffer_snapshot + .buffer_line_for_row(old_head.row) + { + let indent_column = + buffer.indent_column_for_line(line_buffer_range.start.row); + let language_name = buffer.language().map(|language| language.name()); + let indent = cx.global::().tab_size(language_name.as_deref()); + if old_head.column <= indent_column && old_head.column > 0 { + new_head = cmp::min( + new_head, + Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), + ); + } } - } - selection.set_head(new_head, SelectionGoal::None); + selection.set_head(new_head, SelectionGoal::None); + } } } @@ -7492,8 +7495,7 @@ mod tests { |The qu[ick b}rown"}); cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); cx.assert_editor_state(indoc! {" - | - fox jumps over + |fox jumps over the lazy dog|"}); } @@ -7516,13 +7518,11 @@ mod tests { cx.update_editor(|e, _| e.selections.line_mode = true); cx.set_state(indoc! {" The |quick |brown - fox {jum]ps over| + fox {jum]ps over the lazy dog |The qu[ick b}rown"}); cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state(indoc! {" - | - the lazy dog|"}); + cx.assert_editor_state("|the lazy dog|"); } #[gpui::test] @@ -7830,131 +7830,79 @@ mod tests { } #[gpui::test] - fn test_clipboard(cx: &mut gpui::MutableAppContext) { - cx.set_global(Settings::test(cx)); - let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); - let view = cx - .add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)) - .1; + async fn test_clipboard(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; - // Cut with three selections. Clipboard text is divided into three slices. - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| s.select_ranges(vec![0..7, 11..17, 22..27])); - view.cut(&Cut, cx); - assert_eq!(view.display_text(cx), "two four six "); - }); + cx.set_state("[one✅ }two [three }four [five }six "); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state("|two |four |six "); // Paste with three cursors. Each cursor pastes one slice of the clipboard text. - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| s.select_ranges(vec![4..4, 9..9, 13..13])); - view.paste(&Paste, cx); - assert_eq!(view.display_text(cx), "two one✅ four three six five "); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(0, 22)..DisplayPoint::new(0, 22), - DisplayPoint::new(0, 31)..DisplayPoint::new(0, 31) - ] - ); - }); + cx.set_state("two |four |six |"); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state("two one✅ |four three |six five |"); // Paste again but with only two cursors. Since the number of cursors doesn't // match the number of slices in the clipboard, the entire clipboard text // is pasted at each cursor. - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| s.select_ranges(vec![0..0, 31..31])); - view.handle_input(&Input("( ".into()), cx); - view.paste(&Paste, cx); - view.handle_input(&Input(") ".into()), cx); - assert_eq!( - view.display_text(cx), - "( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) " - ); - }); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| s.select_ranges(vec![0..0])); - view.handle_input(&Input("123\n4567\n89\n".into()), cx); - assert_eq!( - view.display_text(cx), - "123\n4567\n89\n( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) " - ); + cx.set_state("|two one✅ four three six five |"); + cx.update_editor(|e, cx| { + e.handle_input(&Input("( ".into()), cx); + e.paste(&Paste, cx); + e.handle_input(&Input(") ".into()), cx); }); + cx.assert_editor_state(indoc! {" + ( one✅ + three + five ) |two one✅ four three six five ( one✅ + three + five ) |"}); // Cut with three selections, one of which is full-line. - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| s.select_display_ranges( - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), - ], - )); - view.cut(&Cut, cx); - assert_eq!( - view.display_text(cx), - "13\n9\n( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) " - ); - }); + cx.set_state(indoc! {" + 1[2}3 + 4|567 + [8}9"}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + 1|3 + |9"}); // Paste with three selections, noticing how the copied selection that was full-line // gets inserted before the second cursor. - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| s.select_display_ranges( - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), - ], - )); - view.paste(&Paste, cx); - assert_eq!( - view.display_text(cx), - "123\n4567\n9\n( 8ne✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) " - ); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), - ] - ); - }); + cx.set_state(indoc! {" + 1|3 + 9| + [o}ne"}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + 12|3 + 4567 + 9| + 8|ne"}); // Copy with a single cursor only, which writes the whole line into the clipboard. - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]) - }); - view.copy(&Copy, cx); - }); + cx.set_state(indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}); + cx.update_editor(|e, cx| e.copy(&Copy, cx)); + cx.assert_clipboard_content(Some("fox jumps over\n")); // Paste with three selections, noticing how the copied full-line selection is inserted // before the empty selections but replaces the selection that is non-empty. - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| s.select_display_ranges( - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - ], - )); - view.paste(&Paste, cx); - assert_eq!( - view.display_text(cx), - "123\n123\n123\n67\n123\n9\n( 8ne✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) " - ); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), - ] - ); - }); + cx.set_state(indoc! {" + T|he quick brown + [fo}x jumps over + t|he lazy dog"}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + fox jumps over + T|he quick brown + fox jumps over + |x jumps over + fox jumps over + t|he lazy dog"}); } #[gpui::test] @@ -8693,8 +8641,10 @@ mod tests { fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text_ranges: &str) { let range_markers = ('<', '>'); let (expected_text, mut selection_ranges_lookup) = - marked_text_ranges_by(marked_text_ranges, vec![range_markers.clone()]); - let selection_ranges = selection_ranges_lookup.remove(&range_markers).unwrap(); + marked_text_ranges_by(marked_text_ranges, vec![range_markers.clone().into()]); + let selection_ranges = selection_ranges_lookup + .remove(&range_markers.into()) + .unwrap(); assert_eq!(editor.text(cx), expected_text); assert_eq!(editor.selections.ranges::(cx), selection_ranges); } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index a8bcc94ee29a7d2829f8e5734a3e4d6be679650b..4c9ceed9aedeb334bb716cd505edcd015cb29337 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -4,7 +4,6 @@ use indoc::indoc; use collections::BTreeMap; use gpui::{keymap::Keystroke, ModelHandle, ViewContext, ViewHandle}; -use itertools::{Either, Itertools}; use language::Selection; use settings::Settings; use util::{ @@ -138,17 +137,26 @@ impl<'a> EditorTestContext<'a> { // `{` to `]` represents a non empty selection with the head at `{` pub fn set_state(&mut self, text: &str) { self.editor.update(self.cx, |editor, cx| { - let (text_with_ranges, empty_selections) = marked_text(&text); - let (unmarked_text, mut selection_ranges) = - marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]); + let (unmarked_text, mut selection_ranges) = marked_text_ranges_by( + &text, + vec!['|'.into(), ('[', '}').into(), ('{', ']').into()], + ); editor.set_text(unmarked_text, cx); - let mut selections: Vec> = empty_selections - .into_iter() - .map(|offset| offset..offset) - .collect(); - selections.extend(selection_ranges.remove(&('{', ']')).unwrap_or_default()); - selections.extend(selection_ranges.remove(&('[', '}')).unwrap_or_default()); + let mut selections: Vec> = + selection_ranges.remove(&'|'.into()).unwrap_or_default(); + selections.extend( + selection_ranges + .remove(&('{', ']').into()) + .unwrap_or_default() + .into_iter() + .map(|range| range.end..range.start), + ); + selections.extend( + selection_ranges + .remove(&('[', '}').into()) + .unwrap_or_default(), + ); editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections)); }) @@ -159,17 +167,23 @@ impl<'a> EditorTestContext<'a> { // `[` to `}` represents a non empty selection with the head at `}` // `{` to `]` represents a non empty selection with the head at `{` pub fn assert_editor_state(&mut self, text: &str) { - let (text_with_ranges, expected_empty_selections) = marked_text(&text); - let (unmarked_text, mut selection_ranges) = - marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]); + let (unmarked_text, mut selection_ranges) = marked_text_ranges_by( + &text, + vec!['|'.into(), ('[', '}').into(), ('{', ']').into()], + ); let editor_text = self.editor_text(); assert_eq!( editor_text, unmarked_text, "Unmarked text doesn't match editor text" ); - let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default(); - let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default(); + let expected_empty_selections = selection_ranges.remove(&'|'.into()).unwrap_or_default(); + let expected_reverse_selections = selection_ranges + .remove(&('{', ']').into()) + .unwrap_or_default(); + let expected_forward_selections = selection_ranges + .remove(&('[', '}').into()) + .unwrap_or_default(); self.assert_selections( expected_empty_selections, @@ -180,65 +194,53 @@ impl<'a> EditorTestContext<'a> { } pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { - let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) = - expected_selections.into_iter().partition_map(|selection| { - if selection.is_empty() { - Either::Left(selection.head()) - } else { - Either::Right(selection) - } - }); - - let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) = - expected_non_empty_selections - .into_iter() - .partition_map(|selection| { - let range = selection.start..selection.end; - if selection.reversed { - Either::Left(range) - } else { - Either::Right(range) - } - }); + let mut empty_selections = Vec::new(); + let mut reverse_selections = Vec::new(); + let mut forward_selections = Vec::new(); + + for selection in expected_selections { + let range = selection.range(); + if selection.is_empty() { + empty_selections.push(range); + } else if selection.reversed { + reverse_selections.push(range); + } else { + forward_selections.push(range) + } + } self.assert_selections( - expected_empty_selections, - expected_reverse_selections, - expected_forward_selections, + empty_selections, + reverse_selections, + forward_selections, None, ) } fn assert_selections( &mut self, - expected_empty_selections: Vec, + expected_empty_selections: Vec>, expected_reverse_selections: Vec>, expected_forward_selections: Vec>, asserted_text: Option, ) { let (empty_selections, reverse_selections, forward_selections) = self.editor.read_with(self.cx, |editor, cx| { - let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor - .selections - .all::(cx) - .into_iter() - .partition_map(|selection| { - if selection.is_empty() { - Either::Left(selection.head()) - } else { - Either::Right(selection) - } - }); - - let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) = - non_empty_selections.into_iter().partition_map(|selection| { - let range = selection.start..selection.end; - if selection.reversed { - Either::Left(range) - } else { - Either::Right(range) - } - }); + let mut empty_selections = Vec::new(); + let mut reverse_selections = Vec::new(); + let mut forward_selections = Vec::new(); + + for selection in editor.selections.all::(cx) { + let range = selection.range(); + if selection.is_empty() { + empty_selections.push(range); + } else if selection.reversed { + reverse_selections.push(range); + } else { + forward_selections.push(range) + } + } + (empty_selections, reverse_selections, forward_selections) }); @@ -258,7 +260,7 @@ impl<'a> EditorTestContext<'a> { .map_err(|err| { err.map(|missing| { let mut error_text = unmarked_text.clone(); - error_text.insert(missing, '|'); + error_text.insert(missing.start, '|'); error_text }) }) @@ -316,14 +318,14 @@ impl<'a> EditorTestContext<'a> { fn insert_markers( &mut self, - empty_selections: &Vec, + empty_selections: &Vec>, reverse_selections: &Vec>, forward_selections: &Vec>, ) -> String { let mut editor_text_with_selections = self.editor_text(); let mut selection_marks = BTreeMap::new(); - for offset in empty_selections { - selection_marks.insert(offset, '|'); + for range in empty_selections { + selection_marks.insert(&range.start, '|'); } for range in reverse_selections { selection_marks.insert(&range.start, '{'); diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs index 23ac35ce86c7f095679a53b817b5b44cc4e7f340..733feeb3f8e6739607b304d3c3b2d82c963cfe97 100644 --- a/crates/util/src/test/marked_text.rs +++ b/crates/util/src/test/marked_text.rs @@ -24,31 +24,67 @@ pub fn marked_text(marked_text: &str) -> (String, Vec) { (unmarked_text, markers.remove(&'|').unwrap_or_default()) } +#[derive(Eq, PartialEq, Hash)] +pub enum TextRangeMarker { + Empty(char), + Range(char, char), +} + +impl TextRangeMarker { + fn markers(&self) -> Vec { + match self { + Self::Empty(m) => vec![*m], + Self::Range(l, r) => vec![*l, *r], + } + } +} + +impl From for TextRangeMarker { + fn from(marker: char) -> Self { + Self::Empty(marker) + } +} + +impl From<(char, char)> for TextRangeMarker { + fn from((left_marker, right_marker): (char, char)) -> Self { + Self::Range(left_marker, right_marker) + } +} + pub fn marked_text_ranges_by( marked_text: &str, - delimiters: Vec<(char, char)>, -) -> (String, HashMap<(char, char), Vec>>) { - let all_markers = delimiters - .iter() - .flat_map(|(start, end)| [*start, *end]) - .collect(); - let (unmarked_text, mut markers) = marked_text_by(marked_text, all_markers); - let range_lookup = delimiters + markers: Vec, +) -> (String, HashMap>>) { + let all_markers = markers.iter().flat_map(|m| m.markers()).collect(); + + let (unmarked_text, mut marker_offsets) = marked_text_by(marked_text, all_markers); + let range_lookup = markers .into_iter() - .map(|(start_marker, end_marker)| { - let starts = markers.remove(&start_marker).unwrap_or_default(); - let ends = markers.remove(&end_marker).unwrap_or_default(); - assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); + .map(|marker| match marker { + TextRangeMarker::Empty(empty_marker_char) => { + let ranges = marker_offsets + .remove(&empty_marker_char) + .unwrap_or_default() + .into_iter() + .map(|empty_index| empty_index..empty_index) + .collect::>>(); + (marker, ranges) + } + TextRangeMarker::Range(start_marker, end_marker) => { + let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); + let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); + assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); - let ranges = starts - .into_iter() - .zip(ends) - .map(|(start, end)| { - assert!(end >= start, "marked ranges must be disjoint"); - start..end - }) - .collect::>>(); - ((start_marker, end_marker), ranges) + let ranges = starts + .into_iter() + .zip(ends) + .map(|(start, end)| { + assert!(end >= start, "marked ranges must be disjoint"); + start..end + }) + .collect::>>(); + (marker, ranges) + } }) .collect(); @@ -58,14 +94,16 @@ pub fn marked_text_ranges_by( // Returns ranges delimited by (), [], and <> ranges. Ranges using the same markers // must not be overlapping. May also include | for empty ranges pub fn marked_text_ranges(full_marked_text: &str) -> (String, Vec>) { - let (range_marked_text, empty_offsets) = marked_text(full_marked_text); - let (unmarked, range_lookup) = - marked_text_ranges_by(&range_marked_text, vec![('[', ']'), ('(', ')'), ('<', '>')]); - let mut combined_ranges: Vec<_> = range_lookup - .into_values() - .flatten() - .chain(empty_offsets.into_iter().map(|offset| offset..offset)) - .collect(); + let (unmarked, range_lookup) = marked_text_ranges_by( + &full_marked_text, + vec![ + '|'.into(), + ('[', ']').into(), + ('(', ')').into(), + ('<', '>').into(), + ], + ); + let mut combined_ranges: Vec<_> = range_lookup.into_values().flatten().collect(); combined_ranges.sort_by_key(|range| range.start); (unmarked, combined_ranges) From d11bc2a4b75433b2c125d42b172c7c85ae0794ac Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 26 May 2022 11:28:05 -0700 Subject: [PATCH 11/14] Fixup paste locations --- crates/editor/src/editor.rs | 32 ++++++++++-------- crates/editor/src/selections_collection.rs | 7 ---- crates/vim/src/normal.rs | 39 ++++++++++++++++++++++ crates/vim/src/visual.rs | 30 +++++++++-------- 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e7af70f303540d0d2a282f9f52fc685039002925..b7ffe9ebbfb759a2f3e844741665a087e263fd49 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1542,12 +1542,10 @@ impl Editor { } self.change_selections(Some(Autoscroll::Fit), cx, |s| { - if add { - if click_count > 1 { - s.delete(newest_selection.id); - } - } else { + if !add { s.clear_disjoint(); + } else if click_count > 1 { + s.delete(newest_selection.id) } s.set_pending_range(start..end, mode); @@ -3283,8 +3281,9 @@ impl Editor { self.transact(cx, |this, cx| { let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| { let mut edits: Vec<(Range, String)> = Default::default(); + let line_mode = s.line_mode; s.move_with(|display_map, selection| { - if !selection.is_empty() { + if !selection.is_empty() || line_mode { return; } @@ -3422,6 +3421,7 @@ impl Editor { let snapshot = buffer.read(cx); let mut start_offset = 0; let mut edits = Vec::new(); + let line_mode = this.selections.line_mode; for (ix, selection) in old_selections.iter().enumerate() { let to_insert; let entire_line; @@ -3439,7 +3439,7 @@ impl Editor { // clipboard text was written, then the entire line containing the // selection was copied. If this selection is also currently empty, // then paste the line before the current line of the buffer. - let range = if selection.is_empty() && entire_line { + let range = if selection.is_empty() && !line_mode && entire_line { let column = selection.start.to_point(&snapshot).column as usize; let line_start = selection.start - column; line_start..line_start @@ -3494,8 +3494,9 @@ impl Editor { pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { self.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - let cursor = if selection.is_empty() { + let cursor = if selection.is_empty() && !line_mode { movement::left(map, selection.start) } else { selection.start @@ -3513,8 +3514,9 @@ impl Editor { pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { self.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - let cursor = if selection.is_empty() { + let cursor = if selection.is_empty() && !line_mode { movement::right(map, selection.end) } else { selection.end @@ -3547,8 +3549,9 @@ impl Editor { } self.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - if !selection.is_empty() { + if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } let (cursor, goal) = movement::up(&map, selection.start, selection.goal, false); @@ -3578,8 +3581,9 @@ impl Editor { } self.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - if !selection.is_empty() { + if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } let (cursor, goal) = movement::down(&map, selection.end, selection.goal, false); @@ -3680,8 +3684,9 @@ impl Editor { ) { self.transact(cx, |this, cx| { this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - if selection.is_empty() { + if selection.is_empty() && !line_mode { let cursor = movement::previous_subword_start(map, selection.head()); selection.set_head(cursor, SelectionGoal::None); } @@ -3734,8 +3739,9 @@ impl Editor { pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext) { self.transact(cx, |this, cx| { this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; s.move_with(|map, selection| { - if selection.is_empty() { + if selection.is_empty() && !line_mode { let cursor = movement::next_word_end(map, selection.head()); selection.set_head(cursor, SelectionGoal::None); } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index b77e55c5cf0bb65697b05e99a56a0f78cd8aa674..db6571cee1f3c3d884912f4e269360daa5070bde 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -22,13 +22,6 @@ pub struct PendingSelection { pub mode: SelectMode, } -#[derive(Clone)] -pub enum LineMode { - None, - WithNewline, - WithoutNewline, -} - #[derive(Clone)] pub struct SelectionsCollection { display_map: ModelHandle, diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 26336838816bb17e56e548f9ab38a82c5eb75a0b..55c9779581d19cd2fb4fd38dd4097eeb54aa97ed 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -194,6 +194,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); } +// Supports non empty selections so it can be bound and called from visual mode fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { @@ -256,6 +257,23 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { new_selections.push(selection.map(|_| selection_point.clone())); point..point } else { + let mut selection = selection.clone(); + if !selection.reversed { + let mut adjusted = selection.end; + // Head is at the end of the selection. Adjust the end position to + // to include the character under the cursor. + *adjusted.column_mut() = adjusted.column() + 1; + adjusted = display_map.clip_point(adjusted, Bias::Right); + // If the selection is empty, move both the start and end forward one + // character + if selection.is_empty() { + selection.start = adjusted; + selection.end = adjusted; + } else { + selection.end = adjusted; + } + } + let range = selection.map(|p| p.to_point(&display_map)).range(); new_selections.push(selection.map(|_| range.start.clone())); range @@ -1141,5 +1159,26 @@ mod test { The quick brown the lazy dog |fox jumps over"}); + + cx.set_state( + indoc! {" + The quick brown + fox [jump}s over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("y"); + cx.set_state( + indoc! {" + The quick brown + fox jump|s over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("p"); + cx.assert_editor_state(indoc! {" + The quick brown + fox jumps|jumps over + the lazy dog"}); } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 665b468b733a1c53963318353236affe0d0fc66c..3020db5e4c9ef6a621c8f646f9e50ec0657a827f 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -6,7 +6,7 @@ use workspace::Workspace; use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; -actions!(vim, [VisualDelete, VisualChange, VisualYank,]); +actions!(vim, [VisualDelete, VisualChange, VisualYank]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(change); @@ -55,7 +55,7 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let line_mode = editor.selections.line_mode; - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if !line_mode && !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Left); - } + if !editor.selections.line_mode { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if !selection.reversed { + // Head is at the end of the selection. Adjust the end position to + // to include the character under the cursor. + *selection.end.column_mut() = selection.end.column() + 1; + selection.end = map.clip_point(selection.end, Bias::Right); + } + }); }); - }); + } copy_selections_content(editor, line_mode, cx); editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { @@ -251,8 +255,8 @@ mod test { cx.simulate_keystrokes(["j", "p"]); cx.assert_editor_state(indoc! {" The ver - the lazy d|quick brown - fox jumps oog"}); + the l|quick brown + fox jumps oazy dog"}); cx.assert( indoc! {" From c53412efcb57eca03bc66eafef70b1e838ea1ddf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 May 2022 12:26:19 -0700 Subject: [PATCH 12/14] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 175661aacfd6d28802e67b92fe97c5d7a4c0be04..27b666d6d08896fbd345a9b4b4cc26ad43ac222c 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 19; +pub const PROTOCOL_VERSION: u32 = 20; From 42cd2ae1426b26f6821b9a47a18151d0159f8b68 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 May 2022 12:47:16 -0700 Subject: [PATCH 13/14] Avoid switching to visual mode when following in vim mode Co-authored-by: Keith Simmons --- crates/editor/src/editor.rs | 4 ++++ crates/vim/src/editor_events.rs | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b7ffe9ebbfb759a2f3e844741665a087e263fd49..b07ca1df2e588dc4b951c78f810c720baa66abdb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1025,6 +1025,10 @@ impl Editor { self.buffer.read(cx).replica_id() } + pub fn leader_replica_id(&self) -> Option { + self.leader_replica_id + } + pub fn buffer(&self) -> &ModelHandle { &self.buffer } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 092d369058270e8e12a1b3da03630cdc7cf8495f..8837f264d35eb40233685dfbf30ac5189359b618 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -21,9 +21,11 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont 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).leader_replica_id().is_none() { + 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); + } } })); @@ -57,7 +59,7 @@ 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 { + if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { vim.switch_mode(Mode::Visual { line: false }, cx) } }) From 8e7c6871dbe0987a0b0a87a0b0b58ec1a234f8c3 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 26 May 2022 17:00:03 -0700 Subject: [PATCH 14/14] Track selection changes in mutable selections collection --- crates/editor/src/editor.rs | 11 ++-- crates/editor/src/element.rs | 10 +-- crates/editor/src/selections_collection.rs | 75 +++++++++++----------- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b07ca1df2e588dc4b951c78f810c720baa66abdb..f4f3483641f06f290952736568268acbca30ec68 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1401,12 +1401,14 @@ impl Editor { let old_cursor_position = self.selections.newest_anchor().head(); self.push_to_selection_history(); - let result = self.selections.change_with(cx, change); + let (changed, result) = self.selections.change_with(cx, change); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); + if changed { + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + self.selections_did_change(true, &old_cursor_position, cx); } - self.selections_did_change(true, &old_cursor_position, cx); result } @@ -4691,6 +4693,7 @@ impl Editor { // Position the selection in the rename editor so that it matches the current selection. this.show_local_selections = false; let rename_editor = cx.add_view(|cx| { + println!("Rename editor created."); let mut editor = Editor::single_line(None, cx); if let Some(old_highlight_id) = old_highlight_id { editor.override_text_style = diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8c0791517dc2d814f3ca557ec5b1388a9656b86c..d5a59c2ecc304b7aaf33d8171099e7a3b5c45738 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -41,7 +41,7 @@ struct SelectionLayout { } impl SelectionLayout { - fn from( + fn new( selection: Selection, line_mode: bool, map: &DisplaySnapshot, @@ -977,7 +977,7 @@ impl Element for EditorElement { remote_selections .entry(replica_id) .or_insert(Vec::new()) - .push(SelectionLayout::from(selection, line_mode, &display_map)); + .push(SelectionLayout::new(selection, line_mode, &display_map)); } selections.extend(remote_selections); @@ -1007,11 +1007,7 @@ impl Element for EditorElement { local_selections .into_iter() .map(|selection| { - SelectionLayout::from( - selection, - view.selections.line_mode, - &display_map, - ) + SelectionLayout::new(selection, view.selections.line_mode, &display_map) }) .collect(), )); diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index db6571cee1f3c3d884912f4e269360daa5070bde..7041062133666a645c3ff18212af2945c918937e 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -289,9 +289,10 @@ impl SelectionsCollection { &mut self, cx: &mut MutableAppContext, change: impl FnOnce(&mut MutableSelectionsCollection) -> R, - ) -> R { + ) -> (bool, R) { let mut mutable_collection = MutableSelectionsCollection { collection: self, + selections_changed: false, cx, }; @@ -300,12 +301,13 @@ impl SelectionsCollection { !mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(), "There must be at least one selection" ); - result + (mutable_collection.selections_changed, result) } } pub struct MutableSelectionsCollection<'a> { collection: &'a mut SelectionsCollection, + selections_changed: bool, cx: &'a mut MutableAppContext, } @@ -323,16 +325,26 @@ impl<'a> MutableSelectionsCollection<'a> { } pub fn delete(&mut self, selection_id: usize) { + let mut changed = false; self.collection.disjoint = self .disjoint .into_iter() - .filter(|selection| selection.id != selection_id) + .filter(|selection| { + let found = selection.id == selection_id; + changed |= found; + !found + }) .cloned() .collect(); + + self.selections_changed |= changed; } pub fn clear_pending(&mut self) { - self.collection.pending = None; + if self.collection.pending.is_some() { + self.collection.pending = None; + self.selections_changed = true; + } } pub fn set_pending_range(&mut self, range: Range, mode: SelectMode) { @@ -345,11 +357,13 @@ impl<'a> MutableSelectionsCollection<'a> { goal: SelectionGoal::None, }, mode, - }) + }); + self.selections_changed = true; } pub fn set_pending(&mut self, selection: Selection, mode: SelectMode) { self.collection.pending = Some(PendingSelection { selection, mode }); + self.selections_changed = true; } pub fn try_cancel(&mut self) -> bool { @@ -357,12 +371,14 @@ impl<'a> MutableSelectionsCollection<'a> { if self.disjoint.is_empty() { self.collection.disjoint = Arc::from([pending.selection]); } + self.selections_changed = true; return true; } let mut oldest = self.oldest_anchor().clone(); if self.count() > 1 { self.collection.disjoint = Arc::from([oldest]); + self.selections_changed = true; return true; } @@ -371,27 +387,13 @@ impl<'a> MutableSelectionsCollection<'a> { oldest.start = head.clone(); oldest.end = head; self.collection.disjoint = Arc::from([oldest]); + self.selections_changed = true; return true; } return false; } - pub fn reset_biases(&mut self) { - let buffer = self.buffer.read(self.cx).snapshot(self.cx); - self.collection.disjoint = self - .collection - .disjoint - .into_iter() - .cloned() - .map(|selection| reset_biases(selection, &buffer)) - .collect(); - - if let Some(pending) = self.collection.pending.as_mut() { - pending.selection = reset_biases(pending.selection.clone(), &buffer); - } - } - pub fn insert_range(&mut self, range: Range) where T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub + std::marker::Copy, @@ -453,6 +455,7 @@ impl<'a> MutableSelectionsCollection<'a> { })); self.collection.pending = None; + self.selections_changed = true; } pub fn select_anchors(&mut self, selections: Vec>) { @@ -551,18 +554,27 @@ impl<'a> MutableSelectionsCollection<'a> { &mut self, mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection), ) { + let mut changed = false; let display_map = self.display_map(); let selections = self .all::(self.cx) .into_iter() .map(|selection| { - let mut selection = selection.map(|point| point.to_display_point(&display_map)); - move_selection(&display_map, &mut selection); - selection.map(|display_point| display_point.to_point(&display_map)) + let mut moved_selection = + selection.map(|point| point.to_display_point(&display_map)); + move_selection(&display_map, &mut moved_selection); + let moved_selection = + moved_selection.map(|display_point| display_point.to_point(&display_map)); + if selection != moved_selection { + changed = true; + } + moved_selection }) .collect(); - self.select(selections) + if changed { + self.select(selections) + } } pub fn move_heads_with( @@ -686,6 +698,7 @@ impl<'a> MutableSelectionsCollection<'a> { pending.selection.end = end; } self.collection.pending = pending; + self.selections_changed = true; selections_with_lost_position } @@ -730,17 +743,3 @@ fn resolve>( ) -> Selection { selection.map(|p| p.summary::(&buffer)) } - -fn reset_biases( - mut selection: Selection, - buffer: &MultiBufferSnapshot, -) -> Selection { - let end_bias = if selection.end.to_offset(buffer) > selection.start.to_offset(buffer) { - Bias::Left - } else { - Bias::Right - }; - selection.start = buffer.anchor_after(selection.start); - selection.end = buffer.anchor_at(selection.end, end_bias); - selection -}