Detailed changes
@@ -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",
@@ -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.
@@ -2,6 +2,7 @@ mod case;
mod change;
mod delete;
mod increment;
+pub(crate) mod mark;
mod paste;
pub(crate) mod repeat;
mod scroll;
@@ -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)
+}
@@ -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(_) }
@@ -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;
+}
@@ -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() {
@@ -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),
_ => {}
@@ -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"}}
@@ -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"}}
@@ -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"}}
@@ -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"}}
@@ -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"}}
@@ -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"}}