From 61f0daa5c5215082d8bbba4e50a0f19711f4f231 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Mon, 23 May 2022 09:23:25 -0700 Subject: [PATCH] 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 00e7fdba2c..f1dca985ab 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 3de44e0315..f76d23a187 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 d80b03da9e..f46fa83141 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 3ef169a2e0..8c0791517d 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 dfed550777..db6571cee1 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 eb4b9650a6..93e5f8279d 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 8ab485b58c..16533f89f1 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 d9b5d470e7..0d68b29bf9 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 0000000000..17a9e47d3d --- /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 a5ae5448fb..c4c6d4850b 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 00ef989874..5ee3f3d38b 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 a319d6dbea..b6120848a3 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 c3416da471..17a4272117 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 3e21e454f2..40ef2a84ab 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();