Files
oak-gpui/crates/vim/src/normal/mark.rs
T
Dino 5d751cd407 vim: Handle visual selection when jumping to mark (#25360)
Fix how vim mode handles jumping to mark, when one of vim's visual modes
is active, in order to behave just like neovim. Here's a quick video
showing the updated behavior ↓


https://github.com/user-attachments/assets/db91f574-d7e8-429d-952e-3435c43e31bd

Closes #18131 

Release Notes:

- Fixed vim's visual selections when jumping to marks
2025-02-21 19:24:54 -07:00

175 lines
5.6 KiB
Rust

use std::{ops::Range, sync::Arc};
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement,
scroll::Autoscroll,
Anchor, Bias, DisplayPoint,
};
use gpui::{Context, Window};
use language::SelectionGoal;
use crate::{
motion::{self, Motion},
state::Mode,
Vim,
};
impl Vim {
pub fn create_mark(
&mut self,
text: Arc<str>,
tail: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(anchors) = self.update_editor(window, cx, |_, editor, _, _| {
editor
.selections
.disjoint_anchors()
.iter()
.map(|s| if tail { s.tail() } else { s.head() })
.collect::<Vec<_>>()
}) else {
return;
};
self.marks.insert(text.to_string(), anchors);
self.clear_operator(window, cx);
}
// When handling an action, you must create visual marks if you will switch to normal
// mode without the default selection behavior.
pub(crate) fn store_visual_marks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_visual() {
self.create_visual_marks(self.mode, window, cx);
}
}
pub(crate) fn create_visual_marks(
&mut self,
mode: Mode,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut starts = vec![];
let mut ends = vec![];
let mut reversed = vec![];
self.update_editor(window, cx, |_, editor, _, cx| {
let (map, selections) = editor.selections.all_display(cx);
for selection in selections {
let end = movement::saturating_left(&map, selection.end);
ends.push(
map.buffer_snapshot
.anchor_before(end.to_offset(&map, Bias::Left)),
);
starts.push(
map.buffer_snapshot
.anchor_before(selection.start.to_offset(&map, Bias::Left)),
);
reversed.push(selection.reversed)
}
});
self.marks.insert("<".to_string(), starts);
self.marks.insert(">".to_string(), ends);
self.stored_visual_mode.replace((mode, reversed));
}
pub fn jump(
&mut self,
text: Arc<str>,
line: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.pop_operator(window, cx);
let anchors = match &*text {
"{" | "}" => self.update_editor(window, 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>>()
}),
"." => self.change_list.last().cloned(),
_ => self.marks.get(&*text).cloned(),
};
let Some(mut anchors) = anchors else { return };
let is_active_operator = self.active_operator().is_some();
if is_active_operator {
if let Some(anchor) = anchors.last() {
self.motion(
Motion::Jump {
anchor: *anchor,
line,
},
window,
cx,
)
}
} else {
// Save the last anchor so as to jump to it later.
let anchor: Option<Anchor> = anchors.last_mut().map(|anchor| *anchor);
let should_jump = self.mode == Mode::Visual
|| self.mode == Mode::VisualLine
|| self.mode == Mode::VisualBlock;
self.update_editor(window, cx, |_, editor, window, cx| {
let map = editor.snapshot(window, 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);
}
}
if !should_jump {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_anchor_ranges(ranges)
});
}
});
if should_jump {
if let Some(anchor) = anchor {
self.motion(Motion::Jump { anchor, line }, window, cx)
}
}
}
}
}
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)
}