Skip to content

Commit

Permalink
vim: Add support for <, >, and ^, marks
Browse files Browse the repository at this point in the history
  • Loading branch information
Zachiah committed May 8, 2024
1 parent 8566b64 commit 0dba953
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 12 deletions.
3 changes: 2 additions & 1 deletion crates/vim/src/motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ impl Motion {
| WindowTop
| WindowMiddle
| WindowBottom
| Jump { line: true, .. }
| EndOfParagraph => true,
EndOfLine { .. }
| Matching
Expand All @@ -496,7 +497,7 @@ impl Motion {
| FindBackward { .. }
| RepeatFind { .. }
| RepeatFindReversed { .. }
| Jump { .. }
| Jump { line: false, .. }
| ZedSearchResult { .. } => false,
}
}
Expand Down
67 changes: 62 additions & 5 deletions crates/vim/src/normal/mark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{ops::Range, sync::Arc};
use collections::HashSet;
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
Anchor, DisplayPoint,
movement, Anchor, Bias, DisplayPoint,
};
use gpui::WindowContext;
use language::SelectionGoal;
Expand All @@ -13,13 +13,13 @@ use crate::{
Vim,
};

pub fn create_mark(vim: &mut Vim, text: Arc<str>, cx: &mut WindowContext) {
pub fn create_mark(vim: &mut Vim, text: Arc<str>, tail: bool, cx: &mut WindowContext) {
let Some(anchors) = vim.update_active_editor(cx, |_, editor, _| {
editor
.selections
.disjoint_anchors()
.iter()
.map(|s| s.head().clone())
.map(|s| if tail { s.tail() } else { s.head() })
.collect::<Vec<_>>()
}) else {
return;
Expand All @@ -29,15 +29,72 @@ pub fn create_mark(vim: &mut Vim, text: Arc<str>, cx: &mut WindowContext) {
vim.clear_operator(cx);
}

pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
let Some(anchors) = Vim::read(cx).state().marks.get(&*text).cloned() else {
pub fn create_mark_after(vim: &mut Vim, text: Arc<str>, cx: &mut WindowContext) {
let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
selections
.into_iter()
.map(|selection| {
let point = movement::saturating_right(&map, selection.tail());
map.buffer_snapshot
.anchor_before(point.to_offset(&map, Bias::Left))
})
.collect::<Vec<_>>()
}) else {
return;
};

vim.update_state(|state| state.marks.insert(text.to_string(), anchors));
vim.clear_operator(cx);
}

pub fn create_mark_before(vim: &mut Vim, text: Arc<str>, cx: &mut WindowContext) {
let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
selections
.into_iter()
.map(|selection| {
let point = movement::saturating_left(&map, selection.head());
map.buffer_snapshot
.anchor_before(point.to_offset(&map, Bias::Left))
})
.collect::<Vec<_>>()
}) else {
return;
};

vim.update_state(|state| state.marks.insert(text.to_string(), anchors));
vim.clear_operator(cx);
}

pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
let anchors = match &*text {
"{" | "}" => Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
selections
.into_iter()
.map(|selection| {
let point = if &*text == "{" {
movement::start_of_paragraph(&map, selection.head(), 1)
} else {
movement::end_of_paragraph(&map, selection.head(), 1)
};
map.buffer_snapshot
.anchor_before(point.to_offset(&map, Bias::Left))
})
.collect::<Vec<Anchor>>()
})
}),
_ => Vim::read(cx).state().marks.get(&*text).cloned(),
};

Vim::update(cx, |vim, cx| {
vim.pop_operator(cx);
});

let Some(anchors) = anchors else { return };

let is_active_operator = Vim::read(cx).state().active_operator().is_some();
if is_active_operator {
if let Some(anchor) = anchors.last() {
Expand Down
115 changes: 114 additions & 1 deletion crates/vim/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ async fn test_mouse_selection(cx: &mut TestAppContext) {
}

#[gpui::test]
async fn test_marks(cx: &mut TestAppContext) {
async fn test_lowercase_marks(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

cx.set_shared_state("line one\nline ˇtwo\nline three").await;
Expand All @@ -1090,3 +1090,116 @@ async fn test_marks(cx: &mut TestAppContext) {
cx.simulate_shared_keystrokes(["^", "d", "`", "a"]).await;
cx.assert_shared_state("line one\nˇtwo\nline three").await;
}

#[gpui::test]
async fn test_lt_gt_marks(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

cx.set_shared_state(indoc!(
"
Line one
Line two
Line ˇthree
Line four
Line five
"
))
.await;

cx.simulate_shared_keystrokes(["v", "j", "escape", "k", "k"])
.await;

cx.simulate_shared_keystrokes(["'", "<"]).await;
cx.assert_shared_state(indoc!(
"
Line one
Line two
ˇLine three
Line four
Line five
"
))
.await;

cx.simulate_shared_keystrokes(["`", "<"]).await;
cx.assert_shared_state(indoc!(
"
Line one
Line two
Line ˇthree
Line four
Line five
"
))
.await;

cx.simulate_shared_keystrokes(["'", ">"]).await;
cx.assert_shared_state(indoc!(
"
Line one
Line two
Line three
ˇLine four
Line five
"
))
.await;

cx.simulate_shared_keystrokes(["`", ">"]).await;
cx.assert_shared_state(indoc!(
"
Line one
Line two
Line three
Line ˇfour
Line five
"
))
.await;
}

#[gpui::test]
async fn test_caret_mark(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

cx.set_shared_state(indoc!(
"
Line one
Line two
Line three
ˇLine four
Line five
"
))
.await;

cx.simulate_shared_keystrokes([
"c", "w", "shift-s", "t", "r", "a", "i", "g", "h", "t", " ", "t", "h", "i", "n", "g",
"escape", "j", "j",
])
.await;

cx.simulate_shared_keystrokes(["'", "^"]).await;
cx.assert_shared_state(indoc!(
"
Line one
Line two
Line three
ˇStraight thing four
Line five
"
))
.await;

cx.simulate_shared_keystrokes(["`", "^"]).await;
cx.assert_shared_state(indoc!(
"
Line one
Line two
Line three
Straight thingˇ four
Line five
"
))
.await;
}
18 changes: 18 additions & 0 deletions crates/vim/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ fn copy_selections_content_internal(
let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len());
let mut ranges_to_highlight = Vec::new();

vim.update_state(|state| {
state.marks.insert(
"[".to_string(),
selections
.iter()
.map(|s| buffer.anchor_before(s.start))
.collect(),
);
state.marks.insert(
"]".to_string(),
selections
.iter()
.map(|s| buffer.anchor_after(s.end))
.collect(),
)
});

{
let mut is_first = true;
for selection in selections.iter() {
Expand Down
25 changes: 20 additions & 5 deletions crates/vim/src/vim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ use gpui::{
use language::{CursorShape, Point, SelectionGoal, TransactionId};
pub use mode_indicator::ModeIndicator;
use motion::Motion;
use normal::normal_replace;
use normal::{
mark::{create_mark, create_mark_after, create_mark_before},
normal_replace,
};
use replace::multi_replace;
use schemars::JsonSchema;
use serde::Deserialize;
Expand Down Expand Up @@ -420,6 +423,10 @@ impl Vim {
// Sync editor settings like clip mode
self.sync_vim_settings(cx);

if mode != Mode::Insert && last_mode == Mode::Insert {
create_mark_after(self, "^".into(), cx)
}

if leave_selections {
return;
}
Expand Down Expand Up @@ -616,6 +623,7 @@ impl Vim {
let is_multicursor = editor.read(cx).selections.count() > 1;

let state = self.state();
let mut is_visual = state.mode.is_visual();
if state.mode == Mode::Insert && state.current_tx.is_some() {
if state.current_anchor.is_none() {
self.update_state(|state| state.current_anchor = Some(newest));
Expand All @@ -632,11 +640,18 @@ impl Vim {
} else {
self.switch_mode(Mode::Visual, false, cx)
}
is_visual = true;
} else if newest.start == newest.end
&& !is_multicursor
&& [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&state.mode)
{
self.switch_mode(Mode::Normal, true, cx)
self.switch_mode(Mode::Normal, true, cx);
is_visual = false;
}

if is_visual {
create_mark_before(self, ">".into(), cx);
create_mark(self, "<".into(), true, cx)
}
}

Expand Down Expand Up @@ -708,9 +723,9 @@ impl Vim {
}
_ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
},
Some(Operator::Mark) => {
Vim::update(cx, |vim, cx| normal::mark::create_mark(vim, text, cx))
}
Some(Operator::Mark) => Vim::update(cx, |vim, cx| {
normal::mark::create_mark(vim, text, false, cx)
}),
Some(Operator::Jump { line }) => normal::mark::jump(text, line, cx),
_ => match Vim::read(cx).state().mode {
Mode::Replace => multi_replace(text, cx),
Expand Down
36 changes: 36 additions & 0 deletions crates/vim/test_data/test_builtin_marks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}}
{"Key":"v"}
{"Key":"j"}
{"Key":"escape"}
{"Key":"k"}
{"Key":"k"}
{"Key":"'"}
{"Key":"<"}
{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}}
{"Key":"`"}
{"Key":"<"}
{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}}
{"Key":"'"}
{"Key":">"}
{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}}
{"Key":"`"}
{"Key":">"}
{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}}
{"Key":"g"}
{"Key":"g"}
{"Key":"^"}
{"Key":"j"}
{"Key":"j"}
{"Key":"l"}
{"Key":"l"}
{"Key":"c"}
{"Key":"e"}
{"Key":"k"}
{"Key":"e"}
{"Key":"escape"}
{"Key":"'"}
{"Key":"."}
{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}}
{"Key":"`"}
{"Key":"."}
{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}}
26 changes: 26 additions & 0 deletions crates/vim/test_data/test_caret_mark.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{"Put":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n"}}
{"Key":"c"}
{"Key":"w"}
{"Key":"shift-s"}
{"Key":"t"}
{"Key":"r"}
{"Key":"a"}
{"Key":"i"}
{"Key":"g"}
{"Key":"h"}
{"Key":"t"}
{"Key":" "}
{"Key":"t"}
{"Key":"h"}
{"Key":"i"}
{"Key":"n"}
{"Key":"g"}
{"Key":"escape"}
{"Key":"j"}
{"Key":"j"}
{"Key":"'"}
{"Key":"^"}
{"Get":{"state":"Line one\nLine two\nLine three\nˇStraight thing four\nLine five\n","mode":"Normal"}}
{"Key":"`"}
{"Key":"^"}
{"Get":{"state":"Line one\nLine two\nLine three\nStraight thingˇ four\nLine five\n","mode":"Normal"}}
Loading

0 comments on commit 0dba953

Please sign in to comment.