Visual line mode handles soft wraps

This commit is contained in:
Keith Simmons 2022-05-23 09:23:25 -07:00
parent 33940b5dd9
commit 61f0daa5c5
14 changed files with 314 additions and 96 deletions

View file

@ -9,7 +9,7 @@
} }
], ],
"h": "vim::Left", "h": "vim::Left",
"backspace": "vim::Left", "backspace": "editor::Backspace", // "vim::Left",
"j": "vim::Down", "j": "vim::Down",
"k": "vim::Up", "k": "vim::Up",
"l": "vim::Right", "l": "vim::Right",
@ -57,6 +57,10 @@
"Delete" "Delete"
], ],
"shift-D": "vim::DeleteToEndOfLine", "shift-D": "vim::DeleteToEndOfLine",
"y": [
"vim::PushOperator",
"Yank"
],
"i": [ "i": [
"vim::SwitchMode", "vim::SwitchMode",
"Insert" "Insert"
@ -77,7 +81,10 @@
"vim::SwitchMode", "vim::SwitchMode",
"VisualLine" "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" "d": "vim::CurrentLine"
} }
}, },
{
"context": "Editor && vim_operator == y",
"bindings": {
"y": "vim::CurrentLine"
}
},
{ {
"context": "Editor && vim_mode == visual", "context": "Editor && vim_mode == visual",
"bindings": { "bindings": {
"c": "vim::VisualChange", "c": "vim::VisualChange",
"d": "vim::VisualDelete", "d": "vim::VisualDelete",
"x": "vim::VisualDelete" "x": "vim::VisualDelete",
"y": "vim::VisualYank"
} }
}, },
{ {
@ -122,7 +136,8 @@
"bindings": { "bindings": {
"c": "vim::VisualLineChange", "c": "vim::VisualLineChange",
"d": "vim::VisualLineDelete", "d": "vim::VisualLineDelete",
"x": "vim::VisualLineDelete" "x": "vim::VisualLineDelete",
"y": "vim::VisualLineYank"
} }
}, },
{ {

View file

@ -279,6 +279,18 @@ impl DisplaySnapshot {
} }
} }
pub fn expand_to_line(&self, mut range: Range<Point>) -> Range<Point> {
(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 { fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
let fold_point = self.folds_snapshot.to_fold_point(point, bias); let fold_point = self.folds_snapshot.to_fold_point(point, bias);
let tab_point = self.tabs_snapshot.to_tab_point(fold_point); let tab_point = self.tabs_snapshot.to_tab_point(fold_point);

View file

@ -1860,7 +1860,7 @@ impl Editor {
pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) { pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
let text: Arc<str> = text.into(); let text: Arc<str> = text.into();
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
let old_selections = this.selections.all::<usize>(cx); let old_selections = this.selections.all_adjusted(cx);
let selection_anchors = this.buffer.update(cx, |buffer, cx| { let selection_anchors = this.buffer.update(cx, |buffer, cx| {
let anchors = { let anchors = {
let snapshot = buffer.read(cx); 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 display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections.all::<Point>(cx); let mut selections = self.selections.all::<Point>(cx);
for selection in &mut selections { for selection in &mut selections {
if selection.is_empty() { if selection.is_empty() && !self.selections.line_mode {
let old_head = selection.head(); let old_head = selection.head();
let mut new_head = let mut new_head =
movement::left(&display_map, old_head.to_display_point(&display_map)) 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>) { pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
this.change_selections(Some(Autoscroll::Fit), cx, |s| { this.change_selections(Some(Autoscroll::Fit), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| { s.move_with(|map, selection| {
if selection.is_empty() { if selection.is_empty() && !line_mode {
let cursor = movement::right(map, selection.head()); let cursor = movement::right(map, selection.head());
selection.set_head(cursor, SelectionGoal::None); selection.set_head(cursor, SelectionGoal::None);
} }
@ -2807,7 +2808,7 @@ impl Editor {
return; return;
} }
let mut selections = self.selections.all::<Point>(cx); let mut selections = self.selections.all_adjusted(cx);
if selections.iter().all(|s| s.is_empty()) { if selections.iter().all(|s| s.is_empty()) {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| { this.buffer.update(cx, |buffer, cx| {
@ -3347,7 +3348,7 @@ impl Editor {
{ {
let max_point = buffer.max_point(); let max_point = buffer.max_point();
for selection in &mut selections { 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 { if is_entire_line {
selection.start = Point::new(selection.start.row, 0); selection.start = Point::new(selection.start.row, 0);
selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 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::<Point>(cx); let selections = self.selections.all::<Point>(cx);
let buffer = self.buffer.read(cx).read(cx); let buffer = self.buffer.read(cx).read(cx);
let mut text = String::new(); let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len()); let mut clipboard_selections = Vec::with_capacity(selections.len());
{ {
let max_point = buffer.max_point(); let max_point = buffer.max_point();
for selection in selections.iter() { for selection in selections.iter() {
let mut start = selection.start; let mut start = selection.start;
let mut end = selection.end; 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 { if is_entire_line {
start = Point::new(start.row, 0); 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; let mut len = 0;
for chunk in buffer.text_for_range(start..end) { for chunk in buffer.text_for_range(start..end) {
@ -3453,7 +3455,7 @@ impl Editor {
let line_start = selection.start - column; let line_start = selection.start - column;
line_start..line_start line_start..line_start
} else { } else {
selection.start..selection.end selection.range()
}; };
edits.push((range, to_insert)); edits.push((range, to_insert));
@ -3670,8 +3672,9 @@ impl Editor {
) { ) {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
this.change_selections(Some(Autoscroll::Fit), cx, |s| { this.change_selections(Some(Autoscroll::Fit), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| { s.move_with(|map, selection| {
if selection.is_empty() { if selection.is_empty() && !line_mode {
let cursor = movement::previous_word_start(map, selection.head()); let cursor = movement::previous_word_start(map, selection.head());
selection.set_head(cursor, SelectionGoal::None); selection.set_head(cursor, SelectionGoal::None);
} }

View file

@ -3,7 +3,10 @@ use super::{
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase, Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
SoftWrap, ToPoint, MAX_LINE_LEN, SoftWrap, ToPoint, MAX_LINE_LEN,
}; };
use crate::{display_map::TransformBlock, EditorStyle}; use crate::{
display_map::{DisplaySnapshot, TransformBlock},
EditorStyle,
};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, HashMap}; use collections::{BTreeMap, HashMap};
use gpui::{ use gpui::{
@ -22,7 +25,7 @@ use gpui::{
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
}; };
use json::json; use json::json;
use language::{Bias, DiagnosticSeverity}; use language::{Bias, DiagnosticSeverity, Selection};
use settings::Settings; use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
@ -32,6 +35,35 @@ use std::{
ops::Range, ops::Range,
}; };
struct SelectionLayout {
head: DisplayPoint,
range: Range<DisplayPoint>,
}
impl SelectionLayout {
fn from<T: ToPoint + ToDisplayPoint + Clone>(
selection: Selection<T>,
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 { pub struct EditorElement {
view: WeakViewHandle<Editor>, view: WeakViewHandle<Editor>,
style: EditorStyle, style: EditorStyle,
@ -345,19 +377,18 @@ impl EditorElement {
scroll_top, scroll_top,
scroll_left, scroll_left,
bounds, bounds,
false,
cx, cx,
); );
} }
let mut cursors = SmallVec::<[Cursor; 32]>::new(); 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 selection_style = style.replica_selection_style(*replica_id);
let corner_radius = 0.15 * layout.line_height; let corner_radius = 0.15 * layout.line_height;
for selection in selections { for selection in selections {
self.paint_highlighted_range( self.paint_highlighted_range(
selection.start..selection.end, selection.range.clone(),
start_row, start_row,
end_row, end_row,
selection_style.selection, selection_style.selection,
@ -368,12 +399,11 @@ impl EditorElement {
scroll_top, scroll_top,
scroll_left, scroll_left,
bounds, bounds,
*line_mode,
cx, cx,
); );
if view.show_local_cursors() || *replica_id != local_replica_id { 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()) { if (start_row..end_row).contains(&cursor_position.row()) {
let cursor_row_layout = let cursor_row_layout =
&layout.line_layouts[(cursor_position.row() - start_row) as usize]; &layout.line_layouts[(cursor_position.row() - start_row) as usize];
@ -485,11 +515,10 @@ impl EditorElement {
scroll_top: f32, scroll_top: f32,
scroll_left: f32, scroll_left: f32,
bounds: RectF, bounds: RectF,
line_mode: bool,
cx: &mut PaintContext, cx: &mut PaintContext,
) { ) {
if range.start != range.end || line_mode { if range.start != range.end {
let row_range = if range.end.column() == 0 && !line_mode { let row_range = if range.end.column() == 0 {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
} else { } else {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
@ -506,14 +535,14 @@ impl EditorElement {
.map(|row| { .map(|row| {
let line_layout = &layout.line_layouts[(row - start_row) as usize]; let line_layout = &layout.line_layouts[(row - start_row) as usize];
HighlightedRangeLine { HighlightedRangeLine {
start_x: if row == range.start.row() && !line_mode { start_x: if row == range.start.row() {
content_origin.x() content_origin.x()
+ line_layout.x_for_index(range.start.column() as usize) + line_layout.x_for_index(range.start.column() as usize)
- scroll_left - scroll_left
} else { } else {
content_origin.x() - scroll_left content_origin.x() - scroll_left
}, },
end_x: if row == range.end.row() && !line_mode { end_x: if row == range.end.row() {
content_origin.x() content_origin.x()
+ line_layout.x_for_index(range.end.column() as usize) + line_layout.x_for_index(range.end.column() as usize)
- scroll_left - scroll_left
@ -921,7 +950,7 @@ impl Element for EditorElement {
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
}; };
let mut selections = Vec::new(); let mut selections: Vec<(ReplicaId, Vec<SelectionLayout>)> = Vec::new();
let mut active_rows = BTreeMap::new(); let mut active_rows = BTreeMap::new();
let mut highlighted_rows = None; let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new(); let mut highlighted_ranges = Vec::new();
@ -945,17 +974,10 @@ impl Element for EditorElement {
if Some(replica_id) == view.leader_replica_id { if Some(replica_id) == view.leader_replica_id {
continue; continue;
} }
remote_selections remote_selections
.entry((replica_id, line_mode)) .entry(replica_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.push(crate::Selection { .push(SelectionLayout::from(selection, line_mode, &display_map));
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),
});
} }
selections.extend(remote_selections); 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)); let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
selections.push(( selections.push((
(local_replica_id, view.selections.line_mode), local_replica_id,
local_selections local_selections
.into_iter() .into_iter()
.map(|selection| crate::Selection { .map(|selection| {
id: selection.id, SelectionLayout::from(
goal: selection.goal, selection,
reversed: selection.reversed, view.selections.line_mode,
start: selection.start.to_display_point(&display_map), &display_map,
end: selection.end.to_display_point(&display_map), )
}) })
.collect(), .collect(),
)); ));
@ -1240,7 +1262,7 @@ pub struct LayoutState {
em_width: f32, em_width: f32,
em_advance: f32, em_advance: f32,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>, highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: Vec<((ReplicaId, bool), Vec<text::Selection<DisplayPoint>>)>, selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
context_menu: Option<(DisplayPoint, ElementBox)>, context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>,
} }

View file

@ -128,6 +128,20 @@ impl SelectionsCollection {
.collect() .collect()
} }
// Returns all of the selections, adjusted to take into account the selection line_mode
pub fn all_adjusted(&self, cx: &mut MutableAppContext) -> Vec<Selection<Point>> {
let mut selections = self.all::<Point>(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>( pub fn disjoint_in_range<'a, D>(
&self, &self,
range: Range<Anchor>, range: Range<Anchor>,

View file

@ -755,7 +755,7 @@ type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> b
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>; type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>; type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>; type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>; type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>; type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>; type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
@ -1263,7 +1263,7 @@ impl MutableAppContext {
pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
where where
G: Any, G: Any,
F: 'static + FnMut(&G, &mut MutableAppContext), F: 'static + FnMut(&mut MutableAppContext),
{ {
let type_id = TypeId::of::<G>(); let type_id = TypeId::of::<G>();
let id = post_inc(&mut self.next_subscription_id); let id = post_inc(&mut self.next_subscription_id);
@ -1274,11 +1274,8 @@ impl MutableAppContext {
.or_default() .or_default()
.insert( .insert(
id, id,
Some( Some(Box::new(move |cx: &mut MutableAppContext| observe(cx))
Box::new(move |global: &dyn Any, cx: &mut MutableAppContext| { as GlobalObservationCallback),
observe(global.downcast_ref().unwrap(), cx)
}) as GlobalObservationCallback,
),
); );
Subscription::GlobalObservation { Subscription::GlobalObservation {
@ -2261,27 +2258,24 @@ impl MutableAppContext {
fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) { fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) {
let callbacks = self.global_observations.lock().remove(&observed_type_id); let callbacks = self.global_observations.lock().remove(&observed_type_id);
if let Some(callbacks) = callbacks { if let Some(callbacks) = callbacks {
if let Some(global) = self.cx.globals.remove(&observed_type_id) { for (id, callback) in callbacks {
for (id, callback) in callbacks { if let Some(mut callback) = callback {
if let Some(mut callback) = callback { callback(self);
callback(global.as_ref(), self); match self
match self .global_observations
.global_observations .lock()
.lock() .entry(observed_type_id)
.entry(observed_type_id) .or_default()
.or_default() .entry(id)
.entry(id) {
{ collections::btree_map::Entry::Vacant(entry) => {
collections::btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback));
entry.insert(Some(callback)); }
} collections::btree_map::Entry::Occupied(entry) => {
collections::btree_map::Entry::Occupied(entry) => { entry.remove();
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 observation_count = Rc::new(RefCell::new(0));
let subscription = cx.observe_global::<Global, _>({ let subscription = cx.observe_global::<Global, _>({
let observation_count = observation_count.clone(); let observation_count = observation_count.clone();
move |_, _| { move |_| {
*observation_count.borrow_mut() += 1; *observation_count.borrow_mut() += 1;
} }
}); });
@ -5629,7 +5623,7 @@ mod tests {
let observation_count = Rc::new(RefCell::new(0)); let observation_count = Rc::new(RefCell::new(0));
cx.observe_global::<OtherGlobal, _>({ cx.observe_global::<OtherGlobal, _>({
let observation_count = observation_count.clone(); let observation_count = observation_count.clone();
move |_, _| { move |_| {
*observation_count.borrow_mut() += 1; *observation_count.borrow_mut() += 1;
} }
}) })
@ -6003,7 +5997,7 @@ mod tests {
*subscription.borrow_mut() = Some(cx.observe_global::<(), _>({ *subscription.borrow_mut() = Some(cx.observe_global::<(), _>({
let observation_count = observation_count.clone(); let observation_count = observation_count.clone();
let subscription = subscription.clone(); let subscription = subscription.clone();
move |_, _| { move |_| {
subscription.borrow_mut().take(); subscription.borrow_mut().take();
*observation_count.borrow_mut() += 1; *observation_count.borrow_mut() += 1;
} }

View file

@ -193,11 +193,13 @@ impl Motion {
if selection.end.row() < map.max_point().row() { if selection.end.row() < map.max_point().row() {
*selection.end.row_mut() += 1; *selection.end.row_mut() += 1;
*selection.end.column_mut() = 0; *selection.end.column_mut() = 0;
selection.end = map.clip_point(selection.end, Bias::Right);
// Don't reset the end here // Don't reset the end here
return; return;
} else if selection.start.row() > 0 { } else if selection.start.row() > 0 {
*selection.start.row_mut() -= 1; *selection.start.row_mut() -= 1;
*selection.start.column_mut() = map.line_len(selection.start.row()); *selection.start.column_mut() = map.line_len(selection.start.row());
selection.start = map.clip_point(selection.start, Bias::Left);
} }
} }

View file

@ -1,5 +1,6 @@
mod change; mod change;
mod delete; mod delete;
mod yank;
use std::borrow::Cow; use std::borrow::Cow;
@ -15,7 +16,7 @@ use gpui::{actions, MutableAppContext, ViewContext};
use language::{Point, SelectionGoal}; use language::{Point, SelectionGoal};
use workspace::Workspace; use workspace::Workspace;
use self::{change::change_over, delete::delete_over}; use self::{change::change_over, delete::delete_over, yank::yank_over};
actions!( actions!(
vim, vim,
@ -69,11 +70,12 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
match vim.state.operator_stack.pop() { match vim.state.operator_stack.pop() {
None => move_cursor(vim, motion, cx), 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(_)) => { Some(Operator::Namespace(_)) => {
// Can't do anything for a namespace operator. Ignoring // 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); vim.clear_operator(cx);
}); });

View file

@ -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);
});
});
});
});
}

View file

@ -26,6 +26,7 @@ pub enum Operator {
Namespace(Namespace), Namespace(Namespace),
Change, Change,
Delete, Delete,
Yank,
} }
#[derive(Default)] #[derive(Default)]
@ -80,6 +81,7 @@ impl Operator {
Operator::Namespace(Namespace::G) => "g", Operator::Namespace(Namespace::G) => "g",
Operator::Change => "c", Operator::Change => "c",
Operator::Delete => "d", Operator::Delete => "d",
Operator::Yank => "y",
} }
.to_owned(); .to_owned();

View file

@ -42,8 +42,10 @@ pub fn init(cx: &mut MutableAppContext) {
}, },
); );
cx.observe_global::<Settings, _>(|settings, cx| { cx.observe_global::<Settings, _>(|cx| {
Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx)) Vim::update(cx, |state, cx| {
state.set_enabled(cx.global::<Settings>().vim_mode, cx)
})
}) })
.detach(); .detach();
} }
@ -141,14 +143,11 @@ impl Vim {
} }
if state.empty_selections_only() { if state.empty_selections_only() {
// Defer so that access to global settings object doesn't panic editor.change_selections(None, cx, |s| {
cx.defer(|editor, cx| { s.move_with(|_, selection| {
editor.change_selections(None, cx, |s| { selection.collapse_to(selection.head(), selection.goal)
s.move_with(|_, selection| { });
selection.collapse_to(selection.head(), selection.goal) })
});
})
});
} }
}); });
} }

View file

@ -337,6 +337,14 @@ impl<'a> VimTestContext<'a> {
let mode = self.mode(); let mode = self.mode();
VimBindingTestContext::new(keystrokes, mode, mode, self) 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> { impl<'a> Deref for VimTestContext<'a> {

View file

@ -9,9 +9,11 @@ actions!(
vim, vim,
[ [
VisualDelete, VisualDelete,
VisualChange,
VisualLineDelete, VisualLineDelete,
VisualLineChange VisualChange,
VisualLineChange,
VisualYank,
VisualLineYank,
] ]
); );
@ -20,6 +22,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(change_line); cx.add_action(change_line);
cx.add_action(delete); cx.add_action(delete);
cx.add_action(delete_line); cx.add_action(delete_line);
cx.add_action(yank);
cx.add_action(yank_line);
} }
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) { pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
@ -56,8 +60,8 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
editor.change_selections(Some(Autoscroll::Fit), cx, |s| { editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
if !selection.reversed { if !selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end // Head is at the end of the selection. Adjust the end position to
// forward by one if possible in order to compensate for this change. // to include the character under the cursor.
*selection.end.column_mut() = selection.end.column() + 1; *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::Left);
} }
@ -74,12 +78,9 @@ pub fn change_line(_: &mut Workspace, _: &VisualLineChange, cx: &mut ViewContext
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| { let adjusted = editor.selections.all_adjusted(cx);
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1; editor.change_selections(None, cx, |s| s.select(adjusted));
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
});
});
copy_selections_content(editor, true, cx); copy_selections_content(editor, true, cx);
editor.insert("", cx); editor.insert("", cx);
}); });
@ -131,11 +132,13 @@ pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext
if selection.end.row() < map.max_point().row() { if selection.end.row() < map.max_point().row() {
*selection.end.row_mut() += 1; *selection.end.row_mut() += 1;
*selection.end.column_mut() = 0; *selection.end.column_mut() = 0;
selection.end = map.clip_point(selection.end, Bias::Right);
// Don't reset the end here // Don't reset the end here
return; return;
} else if selection.start.row() > 0 { } else if selection.start.row() > 0 {
*selection.start.row_mut() -= 1; *selection.start.row_mut() -= 1;
*selection.start.column_mut() = map.line_len(selection.start.row()); *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; 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<Workspace>) {
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<Workspace>) {
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)] #[cfg(test)]
mod test { mod test {
use indoc::indoc; 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"}));
}
} }

View file

@ -179,8 +179,8 @@ fn main() {
cx.observe_global::<Settings, _>({ cx.observe_global::<Settings, _>({
let languages = languages.clone(); let languages = languages.clone();
move |settings, _| { move |cx| {
languages.set_theme(&settings.theme.editor.syntax); languages.set_theme(&cx.global::<Settings>().theme.editor.syntax);
} }
}) })
.detach(); .detach();