vim: Add basic mark support (#11507)

Release Notes:
- vim: Added support for buffer-local marks (`'a-'z`) and some builtin
marks `'<`,`'>`,`'[`,`']`, `'{`, `'}` and `^`. Global marks (`'A-'Z`),
and other builtin marks (`'0-'9`, `'(`, `')`, `''`, `'.`, `'"`) are not
yet implemented. (#5122)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Zachiah Sawyer 2024-05-09 17:51:19 -07:00 committed by GitHub
parent 9cef0ac869
commit 901cb8b3d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 471 additions and 6 deletions

View File

@ -117,6 +117,9 @@
}
}
],
"m": ["vim::PushOperator", "Mark"],
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"ctrl-o": "pane::GoBack",

View File

@ -13,7 +13,7 @@ use std::ops::Range;
use workspace::Workspace;
use crate::{
normal::normal_motion,
normal::{mark, normal_motion},
state::{Mode, Operator},
surrounds::SurroundsType,
utils::coerce_punctuation,
@ -105,6 +105,10 @@ pub enum Motion {
prior_selections: Vec<Range<Anchor>>,
new_selections: Vec<Range<Anchor>>,
},
Jump {
anchor: Anchor,
line: bool,
},
}
#[derive(Clone, Deserialize, PartialEq)]
@ -469,6 +473,7 @@ impl Motion {
| WindowTop
| WindowMiddle
| WindowBottom
| Jump { line: true, .. }
| EndOfParagraph => true,
EndOfLine { .. }
| Matching
@ -492,6 +497,7 @@ impl Motion {
| FindBackward { .. }
| RepeatFind { .. }
| RepeatFindReversed { .. }
| Jump { line: false, .. }
| ZedSearchResult { .. } => false,
}
}
@ -531,7 +537,8 @@ impl Motion {
| WindowMiddle
| WindowBottom
| NextLineStart
| ZedSearchResult { .. } => false,
| ZedSearchResult { .. }
| Jump { .. } => false,
}
}
@ -570,6 +577,7 @@ impl Motion {
| PreviousSubwordStart { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| Jump { .. }
| ZedSearchResult { .. } => false,
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
motion.inclusive()
@ -761,6 +769,7 @@ impl Motion {
WindowTop => window_top(map, point, &text_layout_details, times - 1),
WindowMiddle => window_middle(map, point, &text_layout_details),
WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
ZedSearchResult { new_selections, .. } => {
// There will be only one selection, as
// Search::SelectNextMatch selects a single match.

View File

@ -2,6 +2,7 @@ mod case;
mod change;
mod delete;
mod increment;
pub(crate) mod mark;
mod paste;
pub(crate) mod repeat;
mod scroll;

View File

@ -0,0 +1,147 @@
use std::{ops::Range, sync::Arc};
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement,
scroll::Autoscroll,
Anchor, Bias, DisplayPoint,
};
use gpui::WindowContext;
use language::SelectionGoal;
use crate::{
motion::{self, Motion},
Vim,
};
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| if tail { s.tail() } else { s.head() })
.collect::<Vec<_>>()
}) else {
return;
};
vim.update_state(|state| state.marks.insert(text.to_string(), anchors));
vim.clear_operator(cx);
}
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() {
motion::motion(
Motion::Jump {
anchor: *anchor,
line,
},
cx,
)
}
return;
} else {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
let map = editor.snapshot(cx);
let mut ranges: Vec<Range<Anchor>> = Vec::new();
for mut anchor in anchors {
if line {
let mut point = anchor.to_display_point(&map.display_snapshot);
point = motion::first_non_whitespace(&map.display_snapshot, false, point);
anchor = map
.display_snapshot
.buffer_snapshot
.anchor_before(point.to_point(&map.display_snapshot));
}
if ranges.last() != Some(&(anchor..anchor)) {
ranges.push(anchor..anchor);
}
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(ranges)
})
});
})
}
}
pub fn jump_motion(
map: &DisplaySnapshot,
anchor: Anchor,
line: bool,
) -> (DisplayPoint, SelectionGoal) {
let mut point = anchor.to_display_point(map);
if line {
point = motion::first_non_whitespace(map, false, point)
}
(point, SelectionGoal::None)
}

View File

@ -59,6 +59,8 @@ pub enum Operator {
AddSurrounds { target: Option<SurroundsType> },
ChangeSurrounds { target: Option<Object> },
DeleteSurrounds,
Mark,
Jump { line: bool },
}
#[derive(Default, Clone)]
@ -74,6 +76,8 @@ pub struct EditorState {
pub operator_stack: Vec<Operator>,
pub replacements: Vec<(Range<editor::Anchor>, String)>,
pub marks: HashMap<String, Vec<Anchor>>,
pub current_tx: Option<TransactionId>,
pub current_anchor: Option<Selection<Anchor>>,
pub undo_modes: HashMap<TransactionId, Mode>,
@ -172,7 +176,10 @@ impl EditorState {
}
matches!(
self.operator_stack.last(),
Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. })
Some(Operator::FindForward { .. })
| Some(Operator::FindBackward { .. })
| Some(Operator::Mark)
| Some(Operator::Jump { .. })
)
}
@ -254,6 +261,9 @@ impl Operator {
Operator::AddSurrounds { .. } => "ys",
Operator::ChangeSurrounds { .. } => "cs",
Operator::DeleteSurrounds => "ds",
Operator::Mark => "m",
Operator::Jump { line: true } => "'",
Operator::Jump { line: false } => "`",
}
}
@ -261,6 +271,8 @@ impl Operator {
match self {
Operator::Object { .. } | Operator::ChangeSurrounds { target: None } => &["VimObject"],
Operator::FindForward { .. }
| Operator::Mark
| Operator::Jump { .. }
| Operator::FindBackward { .. }
| Operator::Replace
| Operator::AddSurrounds { target: Some(_) }

View File

@ -1073,3 +1073,133 @@ async fn test_mouse_selection(cx: &mut TestAppContext) {
cx.assert_state("one «ˇtwo» three", Mode::Visual)
}
#[gpui::test]
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;
cx.simulate_shared_keystrokes(["m", "a", "l", "'", "a"])
.await;
cx.assert_shared_state("line one\nˇline two\nline three")
.await;
cx.simulate_shared_keystrokes(["`", "a"]).await;
cx.assert_shared_state("line one\nline ˇtwo\nline three")
.await;
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;
}

View File

@ -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() {

View File

@ -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;
@ -194,7 +197,9 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext)
| Operator::Replace
| Operator::AddSurrounds { .. }
| Operator::ChangeSurrounds { .. }
| Operator::DeleteSurrounds,
| Operator::DeleteSurrounds
| Operator::Mark
| Operator::Jump { .. },
) => {}
Some(_) => {
vim.clear_operator(cx);
@ -418,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;
}
@ -614,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));
@ -630,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)
}
}
@ -706,6 +723,10 @@ 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, false, cx)
}),
Some(Operator::Jump { line }) => normal::mark::jump(text, line, cx),
_ => match Vim::read(cx).state().mode {
Mode::Replace => multi_replace(text, cx),
_ => {}

View File

@ -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"}}

View File

@ -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"}}

View File

@ -0,0 +1,15 @@
{"Put":{"state":"line one\nline ˇtwo\nline three"}}
{"Key":"m"}
{"Key":"a"}
{"Key":"l"}
{"Key":"'"}
{"Key":"a"}
{"Get":{"state":"line one\nˇline two\nline three","mode":"Normal"}}
{"Key":"`"}
{"Key":"a"}
{"Get":{"state":"line one\nline ˇtwo\nline three","mode":"Normal"}}
{"Key":"^"}
{"Key":"d"}
{"Key":"`"}
{"Key":"a"}
{"Get":{"state":"line one\nˇtwo\nline three","mode":"Normal"}}

View File

@ -0,0 +1,18 @@
{"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"}}

View File

@ -0,0 +1,15 @@
{"Put":{"state":"line one\nline ˇtwo\nline three"}}
{"Key":"m"}
{"Key":"a"}
{"Key":"l"}
{"Key":"'"}
{"Key":"a"}
{"Get":{"state":"line one\nˇline two\nline three","mode":"Normal"}}
{"Key":"`"}
{"Key":"a"}
{"Get":{"state":"line one\nline ˇtwo\nline three","mode":"Normal"}}
{"Key":"^"}
{"Key":"d"}
{"Key":"`"}
{"Key":"a"}
{"Get":{"state":"line one\nˇtwo\nline three","mode":"Normal"}}

View File

@ -0,0 +1,14 @@
{"Put":{"state":"Line one\nLine two\nLiˇne three\nLine four\nLine five\n"}}
{"Key":"c"}
{"Key":"e"}
{"Key":"k"}
{"Key":"e"}
{"Key":"escape"}
{"Key":"j"}
{"Key":"j"}
{"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"}}