Detailed changes
@@ -57,6 +57,10 @@
"Delete"
],
"shift-D": "vim::DeleteToEndOfLine",
+ "y": [
+ "vim::PushOperator",
+ "Yank"
+ ],
"i": [
"vim::SwitchMode",
"Insert"
@@ -71,8 +75,24 @@
"shift-O": "vim::InsertLineAbove",
"v": [
"vim::SwitchMode",
- "Visual"
- ]
+ {
+ "Visual": {
+ "line": false
+ }
+ }
+ ],
+ "shift-V": [
+ "vim::SwitchMode",
+ {
+ "Visual": {
+ "line": true
+ }
+ }
+ ],
+ "p": "vim::Paste",
+ "u": "editor::Undo",
+ "ctrl-r": "editor::Redo",
+ "ctrl-o": "pane::GoBack"
}
},
{
@@ -104,12 +124,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"
}
},
{
@@ -279,6 +279,23 @@ impl DisplaySnapshot {
}
}
+ pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
+ 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);
+ }
+ }
+
+ new_start..new_end
+ }
+
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);
@@ -3,10 +3,10 @@ mod element;
pub mod items;
pub mod movement;
mod multi_buffer;
-mod selections_collection;
+pub mod selections_collection;
-#[cfg(test)]
-mod test;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;
use aho_corasick::AhoCorasick;
use anyhow::Result;
@@ -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)]
@@ -1025,6 +1025,10 @@ impl Editor {
self.buffer.read(cx).replica_id()
}
+ pub fn leader_replica_id(&self) -> Option<ReplicaId> {
+ self.leader_replica_id
+ }
+
pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
&self.buffer
}
@@ -1319,7 +1323,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,
+ )
});
}
@@ -1393,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
}
@@ -1538,12 +1548,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);
@@ -1856,13 +1864,16 @@ impl Editor {
pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
let text: Arc<str> = text.into();
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 anchors = {
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::<Vec<_>>()
};
buffer.edit_with_autoindent(
@@ -1874,25 +1885,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);
})
});
}
@@ -2745,28 +2739,31 @@ impl Editor {
pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections.all::<Point>(cx);
- 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::<Settings>().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::<Settings>().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);
+ }
}
}
@@ -2779,8 +2776,9 @@ impl Editor {
pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
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);
}
@@ -2803,7 +2801,7 @@ impl Editor {
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()) {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
@@ -3289,8 +3287,9 @@ impl Editor {
self.transact(cx, |this, cx| {
let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| {
let mut edits: Vec<(Range<usize>, 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;
}
@@ -3343,7 +3342,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));
@@ -3374,16 +3373,17 @@ impl Editor {
let selections = self.selections.all::<Point>(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) {
@@ -3427,6 +3427,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;
@@ -3444,12 +3445,12 @@ 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
} else {
- selection.start..selection.end
+ selection.range()
};
edits.push((range, to_insert));
@@ -3499,8 +3500,9 @@ impl Editor {
pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
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
@@ -3518,8 +3520,9 @@ impl Editor {
pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
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
@@ -3552,8 +3555,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);
@@ -3583,8 +3587,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);
@@ -3666,8 +3671,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);
}
@@ -3684,8 +3690,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);
}
@@ -3738,8 +3745,9 @@ impl Editor {
pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
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);
}
@@ -4685,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 =
@@ -5599,7 +5608,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,
+ );
}
});
}
@@ -6020,7 +6033,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::{
@@ -7292,117 +7307,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));
+ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+ let mut cx = EditorTestContext::new(cx).await;
- 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,
- );
-
- // 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]
@@ -7511,73 +7471,71 @@ 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("|the lazy dog|");
}
#[gpui::test]
@@ -7885,131 +7843,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]
@@ -8748,8 +8654,10 @@ mod tests {
fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, 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::<usize>(cx), selection_ranges);
}
@@ -9798,10 +9706,6 @@ mod tests {
point..point
}
- fn build_editor(buffer: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
- Editor::new(EditorMode::Full, buffer, None, None, None, cx)
- }
-
fn assert_selection_ranges(
marked_text: &str,
selection_marker_pairs: Vec<(char, char)>,
@@ -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<DisplayPoint>,
+}
+
+impl SelectionLayout {
+ fn new<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 {
view: WeakViewHandle<Editor>,
style: EditorStyle,
@@ -356,7 +388,7 @@ impl EditorElement {
for selection in selections {
self.paint_highlighted_range(
- selection.start..selection.end,
+ selection.range.clone(),
start_row,
end_row,
selection_style.selection,
@@ -371,7 +403,7 @@ impl EditorElement {
);
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];
@@ -918,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<SelectionLayout>)> = Vec::new();
let mut active_rows = BTreeMap::new();
let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new();
@@ -934,7 +966,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()))
{
@@ -942,17 +974,10 @@ impl Element for EditorElement {
if Some(replica_id) == view.leader_replica_id {
continue;
}
-
remote_selections
.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::new(selection, line_mode, &display_map));
}
selections.extend(remote_selections);
@@ -981,12 +1006,8 @@ impl Element for EditorElement {
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::new(selection, view.selections.line_mode, &display_map)
})
.collect(),
));
@@ -1237,7 +1258,7 @@ pub struct LayoutState {
em_width: f32,
em_advance: f32,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
- selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
+ selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
}
@@ -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,
+ );
}
});
}
@@ -509,6 +509,7 @@ impl MultiBuffer {
pub fn set_active_selections(
&mut self,
selections: &[Selection<Anchor>],
+ line_mode: bool,
cx: &mut ModelContext<Self>,
) {
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
@@ -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<Anchor>,
- ) -> impl 'a + Iterator<Item = (ReplicaId, Selection<Anchor>)> {
+ ) -> impl 'a + Iterator<Item = (ReplicaId, bool, Selection<Anchor>)> {
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
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,
@@ -27,6 +27,7 @@ pub struct SelectionsCollection {
display_map: ModelHandle<DisplayMap>,
buffer: ModelHandle<MultiBuffer>,
pub next_selection_id: usize,
+ pub line_mode: bool,
disjoint: Arc<[Selection<Anchor>]>,
pending: Option<PendingSelection>,
}
@@ -37,6 +38,7 @@ impl SelectionsCollection {
display_map,
buffer,
next_selection_id: 1,
+ line_mode: false,
disjoint: Arc::from([]),
pending: Some(PendingSelection {
selection: Selection {
@@ -126,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<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>(
&self,
range: Range<Anchor>,
@@ -273,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,
};
@@ -284,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,
}
@@ -307,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<Anchor>, mode: SelectMode) {
@@ -329,11 +357,13 @@ impl<'a> MutableSelectionsCollection<'a> {
goal: SelectionGoal::None,
},
mode,
- })
+ });
+ self.selections_changed = true;
}
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { selection, mode });
+ self.selections_changed = true;
}
pub fn try_cancel(&mut self) -> bool {
@@ -341,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;
}
@@ -355,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<T>(&mut self, range: Range<T>)
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
@@ -437,6 +455,7 @@ impl<'a> MutableSelectionsCollection<'a> {
}));
self.collection.pending = None;
+ self.selections_changed = true;
}
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
@@ -535,18 +554,27 @@ impl<'a> MutableSelectionsCollection<'a> {
&mut self,
mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
) {
+ let mut changed = false;
let display_map = self.display_map();
let selections = self
.all::<Point>(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(
@@ -670,6 +698,7 @@ impl<'a> MutableSelectionsCollection<'a> {
pending.selection.end = end;
}
self.collection.pending = pending;
+ self.selections_changed = true;
selections_with_lost_position
}
@@ -714,17 +743,3 @@ fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
) -> Selection<D> {
selection.map(|p| p.summary::<D>(&buffer))
}
-
-fn reset_biases(
- mut selection: Selection<Anchor>,
- buffer: &MultiBufferSnapshot,
-) -> Selection<Anchor> {
- 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
-}
@@ -1,9 +1,19 @@
-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 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 +66,301 @@ 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<MultiBuffer>,
+ cx: &mut ViewContext<Editor>,
+) -> 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<Editor>,
+}
+
+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<F, T>(&mut self, update: F) -> T
+ where
+ F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> 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<const COUNT: usize>(&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 (unmarked_text, mut selection_ranges) = marked_text_ranges_by(
+ &text,
+ vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
+ );
+ editor.set_text(unmarked_text, cx);
+
+ let mut selections: Vec<Range<usize>> =
+ 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));
+ })
+ }
+
+ // 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 (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_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,
+ expected_reverse_selections,
+ expected_forward_selections,
+ Some(text.to_string()),
+ )
+ }
+
+ pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
+ 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(
+ empty_selections,
+ reverse_selections,
+ forward_selections,
+ None,
+ )
+ }
+
+ fn assert_selections(
+ &mut self,
+ expected_empty_selections: Vec<Range<usize>>,
+ expected_reverse_selections: Vec<Range<usize>>,
+ expected_forward_selections: Vec<Range<usize>>,
+ asserted_text: Option<String>,
+ ) {
+ let (empty_selections, reverse_selections, forward_selections) =
+ self.editor.read_with(self.cx, |editor, cx| {
+ let mut empty_selections = Vec::new();
+ let mut reverse_selections = Vec::new();
+ let mut forward_selections = Vec::new();
+
+ for selection in editor.selections.all::<usize>(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)
+ });
+
+ 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<String>> =
+ set_eq!(expected_empty_selections, empty_selections)
+ .map_err(|err| {
+ err.map(|missing| {
+ let mut error_text = unmarked_text.clone();
+ error_text.insert(missing.start, '|');
+ 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<Range<usize>>,
+ reverse_selections: &Vec<Range<usize>>,
+ forward_selections: &Vec<Range<usize>>,
+ ) -> String {
+ let mut editor_text_with_selections = self.editor_text();
+ let mut selection_marks = BTreeMap::new();
+ for range in empty_selections {
+ selection_marks.insert(&range.start, '|');
+ }
+ 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
+ }
+}
@@ -766,7 +766,7 @@ type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> b
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ObservationCallback = 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 DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
@@ -1274,7 +1274,7 @@ impl MutableAppContext {
pub fn observe_global<G, F>(&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::<G>();
let id = post_inc(&mut self.next_subscription_id);
@@ -1285,11 +1285,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 {
@@ -2272,27 +2269,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);
}
}
}
@@ -5617,7 +5611,7 @@ mod tests {
let observation_count = Rc::new(RefCell::new(0));
let subscription = cx.observe_global::<Global, _>({
let observation_count = observation_count.clone();
- move |_, _| {
+ move |_| {
*observation_count.borrow_mut() += 1;
}
});
@@ -5647,7 +5641,7 @@ mod tests {
let observation_count = Rc::new(RefCell::new(0));
cx.observe_global::<OtherGlobal, _>({
let observation_count = observation_count.clone();
- move |_, _| {
+ move |_| {
*observation_count.borrow_mut() += 1;
}
})
@@ -6021,7 +6015,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;
}
@@ -83,6 +83,7 @@ pub struct BufferSnapshot {
#[derive(Clone, Debug)]
struct SelectionSet {
+ line_mode: bool,
selections: Arc<[Selection<Anchor>]>,
lamport_timestamp: clock::Lamport,
}
@@ -129,6 +130,7 @@ pub enum Operation {
UpdateSelections {
selections: Arc<[Selection<Anchor>]>,
lamport_timestamp: clock::Lamport,
+ line_mode: bool,
},
UpdateCompletionTriggers {
triggers: Vec<String>,
@@ -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<Anchor>]>,
+ line_mode: bool,
cx: &mut ModelContext<Self>,
) {
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>) {
- self.set_active_selections(Arc::from([]), cx);
+ self.set_active_selections(Arc::from([]), false, cx);
}
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
@@ -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<Anchor>,
- ) -> impl 'a + Iterator<Item = (ReplicaId, impl 'a + Iterator<Item = &'a Selection<Anchor>>)>
- {
+ ) -> impl 'a
+ + Iterator<
+ Item = (
+ ReplicaId,
+ bool,
+ impl 'a + Iterator<Item = &'a Selection<Anchor>>,
+ ),
+ > {
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(),
+ )
})
}
@@ -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<Operation> {
value: message.lamport_timestamp,
},
selections: Arc::from(selections),
+ line_mode: message.line_mode,
}
}
proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics {
@@ -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::<Vec<_>>()))
+ .map(|(replica_id, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
.collect::<Vec<_>>();
let expected_remote_selections = active_selections
.iter()
@@ -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 {
@@ -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;
@@ -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<T: Copy + Ord> Selection<T> {
self.goal = new_goal;
self.reversed = false;
}
+
+ pub fn range(&self) -> Range<T> {
+ self.start..self.end
+ }
}
impl Selection<usize> {
@@ -24,31 +24,67 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
(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<char> {
+ match self {
+ Self::Empty(m) => vec![*m],
+ Self::Range(l, r) => vec![*l, *r],
+ }
+ }
+}
+
+impl From<char> 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<Range<usize>>>) {
- 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<TextRangeMarker>,
+) -> (String, HashMap<TextRangeMarker, Vec<Range<usize>>>) {
+ 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::<Vec<Range<usize>>>();
+ (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::<Vec<Range<usize>>>();
- ((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::<Vec<Range<usize>>>();
+ (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<Range<usize>>) {
- 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)
@@ -18,22 +18,31 @@ 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 editor.read(cx).leader_replica_id().is_none() {
+ if let editor::Event::SelectionsChanged { local: true } = event {
+ let newest_empty = editor.read(cx).selections.newest::<usize>(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 +56,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.enabled && vim.state.mode == Mode::Normal && !newest_empty {
+ vim.switch_mode(Mode::Visual { line: false }, cx)
+ }
+ })
+}
@@ -111,7 +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::Visual { .. } => visual_motion(motion, cx),
Mode::Insert => {
// Shouldn't execute a motion in insert mode. Ignoring
}
@@ -192,11 +192,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);
}
}
@@ -1,5 +1,8 @@
mod change;
mod delete;
+mod yank;
+
+use std::borrow::Cow;
use crate::{
motion::Motion,
@@ -8,12 +11,12 @@ 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};
+use self::{change::change_over, delete::delete_over, yank::yank_over};
actions!(
vim,
@@ -27,6 +30,8 @@ actions!(
DeleteRight,
ChangeToEndOfLine,
DeleteToEndOfLine,
+ Paste,
+ Yank,
]
);
@@ -56,6 +61,7 @@ pub fn init(cx: &mut MutableAppContext) {
delete_over(vim, Motion::EndOfLine, cx);
})
});
+ cx.add_action(paste);
change_init(cx);
}
@@ -64,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);
});
@@ -187,6 +194,116 @@ 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<Workspace>) {
+ 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::<Vec<ClipboardSelection>>()
+ {
+ 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 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
+ };
+
+ 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;
@@ -678,14 +795,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]
@@ -1026,4 +1137,48 @@ 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"});
+
+ 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"});
+ }
}
@@ -1,4 +1,4 @@
-use crate::{motion::Motion, state::Mode, Vim};
+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;
@@ -27,6 +27,7 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
motion.expand_selection(map, selection, false);
});
});
+ copy_selections_content(editor, motion.linewise(), cx);
editor.insert(&"", cx);
});
});
@@ -65,6 +66,7 @@ fn change_word(
});
});
});
+ copy_selections_content(editor, false, cx);
editor.insert(&"", cx);
});
});
@@ -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
@@ -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);
+ });
+ });
+ });
+ });
+}
@@ -6,7 +6,7 @@ use serde::Deserialize;
pub enum Mode {
Normal,
Insert,
- Visual,
+ Visual { line: bool },
}
impl Default for Mode {
@@ -25,6 +25,7 @@ pub enum Operator {
Namespace(Namespace),
Change,
Delete,
+ Yank,
}
#[derive(Default)]
@@ -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 { .. } => CursorShape::Block,
Mode::Insert => CursorShape::Bar,
}
}
@@ -46,13 +46,24 @@ impl VimState {
!matches!(self.mode, Mode::Insert)
}
+ pub fn clip_at_line_end(&self) -> bool {
+ match self.mode {
+ Mode::Insert | Mode::Visual { .. } => false,
+ _ => true,
+ }
+ }
+
+ pub fn empty_selections_only(&self) -> bool {
+ !matches!(self.mode, Mode::Visual { .. })
+ }
+
pub fn keymap_context_layer(&self) -> Context {
let mut context = Context::default();
context.map.insert(
"vim_mode".to_string(),
match self.mode {
Mode::Normal => "normal",
- Mode::Visual => "visual",
+ Mode::Visual { .. } => "visual",
Mode::Insert => "insert",
}
.to_string(),
@@ -75,6 +86,7 @@ impl Operator {
Operator::Namespace(Namespace::G) => "g",
Operator::Change => "c",
Operator::Delete => "d",
+ Operator::Yank => "y",
}
.to_owned();
@@ -0,0 +1,25 @@
+use editor::{ClipboardSelection, Editor};
+use gpui::{ClipboardItem, MutableAppContext};
+
+pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) {
+ 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());
+ {
+ 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));
+}
@@ -6,11 +6,12 @@ mod insert;
mod motion;
mod normal;
mod state;
+mod utils;
mod visual;
use collections::HashMap;
-use editor::{CursorShape, Editor};
-use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
+use editor::{Bias, CursorShape, Editor, Input};
+use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
use serde::Deserialize;
use settings::Settings;
@@ -40,9 +41,19 @@ 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() {
+ // Defer without updating editor
+ MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx)))
+ } else {
+ cx.propagate_action()
+ }
+ });
- cx.observe_global::<Settings, _>(|settings, cx| {
- Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
+ cx.observe_global::<Settings, _>(|cx| {
+ Vim::update(cx, |state, cx| {
+ state.set_enabled(cx.global::<Settings>().vim_mode, cx)
+ })
})
.detach();
}
@@ -51,6 +62,7 @@ pub fn init(cx: &mut MutableAppContext) {
pub struct Vim {
editors: HashMap<usize, WeakViewHandle<Editor>>,
active_editor: Option<WeakViewHandle<Editor>>,
+ selection_subscription: Option<Subscription>,
enabled: bool,
state: VimState,
@@ -101,7 +113,7 @@ impl Vim {
self.sync_editor_options(cx);
}
- fn active_operator(&mut self) -> Option<Operator> {
+ fn active_operator(&self) -> Option<Operator> {
self.state.operator_stack.last().copied()
}
@@ -118,23 +130,38 @@ 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 =
+ matches!(state.mode, Mode::Visual { line: true });
let context_layer = state.keymap_context_layer();
editor.set_keymap_context_layer::<Self>(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::<Self>();
}
+
+ 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)
+ }
+ });
+ })
});
}
}
@@ -169,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);
@@ -1,31 +1,21 @@
-use std::ops::{Deref, 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::{AppState, WorkspaceHandle};
+use workspace::{pane, AppState, WorkspaceHandle};
use crate::{state::Operator, *};
pub struct VimTestContext<'a> {
- cx: &'a mut gpui::TestAppContext,
- window_id: usize,
- editor: ViewHandle<Editor>,
+ cx: EditorTestContext<'a>,
}
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();
@@ -69,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,
+ },
}
}
@@ -100,219 +92,13 @@ impl<'a> VimTestContext<'a> {
.read(|cx| cx.global::<Vim>().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<const COUNT: usize>(&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<Selection<usize>>) {
- 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<usize>,
- expected_reverse_selections: Vec<Range<usize>>,
- expected_forward_selections: Vec<Range<usize>>,
- asserted_text: Option<String>,
- ) {
- 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::<usize>(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<String>> =
- 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<usize>,
- reverse_selections: &Vec<Range<usize>>,
- forward_selections: &Vec<Range<usize>>,
- ) -> 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<const COUNT: usize>(
@@ -324,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);
}
@@ -337,13 +123,27 @@ 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> {
- 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
}
}
@@ -404,3 +204,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
+ }
+}
@@ -1,14 +1,17 @@
-use editor::{Autoscroll, Bias};
+use collections::HashMap;
+use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
use gpui::{actions, MutableAppContext, ViewContext};
+use language::SelectionGoal;
use workspace::Workspace;
-use crate::{motion::Motion, state::Mode, Vim};
+use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
-actions!(vim, [VisualDelete, VisualChange]);
+actions!(vim, [VisualDelete, VisualChange, VisualYank]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(change);
cx.add_action(delete);
+ cx.add_action(yank);
}
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
@@ -17,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);
@@ -30,7 +32,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);
}
});
});
@@ -42,17 +44,47 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
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| {
+ // Compute edits and resulting anchor selections. If in line mode, adjust
+ // the anchor location and additional newline
+ let mut edits = Vec::new();
+ let mut new_selections = Vec::new();
+ let line_mode = editor.selections.line_mode;
+ editor.change_selections(None, 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);
+ }
+
+ 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)
+ };
+
+ 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()));
}
+ selection.goal = SelectionGoal::None;
});
});
- editor.insert("", 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.select_anchors(new_selections);
+ });
});
vim.switch_mode(Mode::Insert, cx);
});
@@ -60,31 +92,70 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
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();
+ let line_mode = editor.selections.line_mode;
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.
+ 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::Left);
+ selection.end = map.clip_point(selection.end, Bias::Right);
}
+ selection.goal = SelectionGoal::None;
});
});
+ 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();
- cursor = map.clip_point(cursor, Bias::Left);
+ let mut cursor = selection.head().to_point(map);
+
+ if let Some(column) = original_columns.get(&selection.id) {
+ cursor.column = *column
+ }
+ let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
+ vim.switch_mode(Mode::Normal, cx);
+ });
+}
+
+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);
+ let line_mode = editor.selections.line_mode;
+ 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| {
+ selection.collapse_to(selection.start, SelectionGoal::None)
+ });
+ });
+ });
+ vim.switch_mode(Mode::Normal, cx);
});
}
@@ -97,7 +168,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
@@ -128,7 +201,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
@@ -176,6 +251,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 l|quick brown
+ fox jumps oazy dog"});
+
cx.assert(
indoc! {"
The quick brown
@@ -226,6 +308,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;
@@ -290,4 +443,168 @@ 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
+ |"},
+ );
+ }
+
+ #[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 o"}));
+ 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"}));
+ }
}
@@ -179,8 +179,8 @@ fn main() {
cx.observe_global::<Settings, _>({
let languages = languages.clone();
- move |settings, _| {
- languages.set_theme(&settings.theme.editor.syntax);
+ move |cx| {
+ languages.set_theme(&cx.global::<Settings>().theme.editor.syntax);
}
})
.detach();