
In #32656 I generalized the argument to change selections to allow controling both the scroll and the nav history (and the completion trigger). To avoid conflicting with ongoing debugger cherry-picks I left the argument as an `impl Into<>`, but I think it's clearer to make callers specify what they want here. I converted a lot of `None` arguments to `SelectionEffects::no_scroll()` to be exactly compatible; but I think many people used none as an "i don't care" value in which case Default::default() might be more appropraite Closes #ISSUE Release Notes: - N/A
395 lines
14 KiB
Rust
395 lines
14 KiB
Rust
use std::{ops::Range, path::Path, sync::Arc};
|
|
|
|
use editor::{
|
|
Anchor, Bias, DisplayPoint, Editor, MultiBuffer,
|
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
|
movement,
|
|
};
|
|
use gpui::{Context, Entity, EntityId, UpdateGlobal, Window};
|
|
use language::SelectionGoal;
|
|
use text::Point;
|
|
use ui::App;
|
|
use workspace::OpenOptions;
|
|
|
|
use crate::{
|
|
Vim,
|
|
motion::{self, Motion},
|
|
state::{Mark, Mode, VimGlobals},
|
|
};
|
|
|
|
impl Vim {
|
|
pub fn create_mark(&mut self, text: Arc<str>, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.update_editor(window, cx, |vim, editor, window, cx| {
|
|
let anchors = editor
|
|
.selections
|
|
.disjoint_anchors()
|
|
.iter()
|
|
.map(|s| s.head())
|
|
.collect::<Vec<_>>();
|
|
vim.set_mark(text.to_string(), anchors, editor.buffer(), window, cx);
|
|
});
|
|
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, |vim, editor, window, 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)
|
|
}
|
|
vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx);
|
|
vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx);
|
|
});
|
|
|
|
self.stored_visual_mode.replace((mode, reversed));
|
|
}
|
|
|
|
fn open_buffer_mark(
|
|
&mut self,
|
|
line: bool,
|
|
entity_id: EntityId,
|
|
anchors: Vec<Anchor>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(workspace) = self.workspace(window) else {
|
|
return;
|
|
};
|
|
workspace.update(cx, |workspace, cx| {
|
|
let item = workspace.items(cx).find(|item| {
|
|
item.act_as::<Editor>(cx)
|
|
.is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id)
|
|
});
|
|
let Some(item) = item.cloned() else {
|
|
return;
|
|
};
|
|
if let Some(pane) = workspace.pane_for(item.as_ref()) {
|
|
pane.update(cx, |pane, cx| {
|
|
if let Some(index) = pane.index_for_item(item.as_ref()) {
|
|
pane.activate_item(index, true, true, window, cx);
|
|
}
|
|
});
|
|
};
|
|
|
|
item.act_as::<Editor>(cx).unwrap().update(cx, |editor, 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);
|
|
}
|
|
}
|
|
|
|
editor.change_selections(Default::default(), window, cx, |s| {
|
|
s.select_anchor_ranges(ranges)
|
|
});
|
|
})
|
|
});
|
|
return;
|
|
}
|
|
|
|
fn open_path_mark(
|
|
&mut self,
|
|
line: bool,
|
|
path: Arc<Path>,
|
|
points: Vec<Point>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(workspace) = self.workspace(window) else {
|
|
return;
|
|
};
|
|
let task = workspace.update(cx, |workspace, cx| {
|
|
workspace.open_abs_path(
|
|
path.to_path_buf(),
|
|
OpenOptions {
|
|
visible: Some(workspace::OpenVisible::All),
|
|
focus: Some(true),
|
|
..Default::default()
|
|
},
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
let editor = task.await?;
|
|
this.update_in(cx, |_, window, cx| {
|
|
if let Some(editor) = editor.act_as::<Editor>(cx) {
|
|
editor.update(cx, |editor, cx| {
|
|
let map = editor.snapshot(window, cx);
|
|
let points: Vec<_> = points
|
|
.into_iter()
|
|
.map(|p| {
|
|
if line {
|
|
let point = p.to_display_point(&map.display_snapshot);
|
|
motion::first_non_whitespace(
|
|
&map.display_snapshot,
|
|
false,
|
|
point,
|
|
)
|
|
.to_point(&map.display_snapshot)
|
|
} else {
|
|
p
|
|
}
|
|
})
|
|
.collect();
|
|
editor.change_selections(Default::default(), window, cx, |s| {
|
|
s.select_ranges(points.into_iter().map(|p| p..p))
|
|
})
|
|
})
|
|
}
|
|
})
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
pub fn jump(
|
|
&mut self,
|
|
text: Arc<str>,
|
|
line: bool,
|
|
should_pop_operator: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if should_pop_operator {
|
|
self.pop_operator(window, cx);
|
|
}
|
|
let mark = self
|
|
.update_editor(window, cx, |vim, editor, window, cx| {
|
|
vim.get_mark(&text, editor, window, cx)
|
|
})
|
|
.flatten();
|
|
let anchors = match mark {
|
|
None => None,
|
|
Some(Mark::Local(anchors)) => Some(anchors),
|
|
Some(Mark::Buffer(entity_id, anchors)) => {
|
|
self.open_buffer_mark(line, entity_id, anchors, window, cx);
|
|
return;
|
|
}
|
|
Some(Mark::Path(path, points)) => {
|
|
self.open_path_mark(line, path, points, window, cx);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let Some(mut anchors) = anchors else { return };
|
|
|
|
self.update_editor(window, cx, |_, editor, _, cx| {
|
|
editor.create_nav_history_entry(cx);
|
|
});
|
|
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 && !ranges.is_empty() {
|
|
editor.change_selections(Default::default(), 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 set_mark(
|
|
&mut self,
|
|
mut name: String,
|
|
anchors: Vec<Anchor>,
|
|
buffer_entity: &Entity<MultiBuffer>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
let Some(workspace) = self.workspace(window) else {
|
|
return;
|
|
};
|
|
if name == "`" {
|
|
name = "'".to_string();
|
|
}
|
|
if matches!(&name[..], "-" | " ") {
|
|
// Not allowed marks
|
|
return;
|
|
}
|
|
let entity_id = workspace.entity_id();
|
|
Vim::update_globals(cx, |vim_globals, cx| {
|
|
let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
|
|
return;
|
|
};
|
|
marks_state.update(cx, |ms, cx| {
|
|
ms.set_mark(name.clone(), buffer_entity, anchors, cx);
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn get_mark(
|
|
&self,
|
|
mut name: &str,
|
|
editor: &mut Editor,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Option<Mark> {
|
|
if name == "`" {
|
|
name = "'";
|
|
}
|
|
if matches!(name, "{" | "}" | "(" | ")") {
|
|
let (map, selections) = editor.selections.all_display(cx);
|
|
let anchors = selections
|
|
.into_iter()
|
|
.map(|selection| {
|
|
let point = match name {
|
|
"{" => movement::start_of_paragraph(&map, selection.head(), 1),
|
|
"}" => movement::end_of_paragraph(&map, selection.head(), 1),
|
|
"(" => motion::sentence_backwards(&map, selection.head(), 1),
|
|
")" => motion::sentence_forwards(&map, selection.head(), 1),
|
|
_ => unreachable!(),
|
|
};
|
|
map.buffer_snapshot
|
|
.anchor_before(point.to_offset(&map, Bias::Left))
|
|
})
|
|
.collect::<Vec<Anchor>>();
|
|
return Some(Mark::Local(anchors));
|
|
}
|
|
VimGlobals::update_global(cx, |globals, cx| {
|
|
let workspace_id = self.workspace(window)?.entity_id();
|
|
globals
|
|
.marks
|
|
.get_mut(&workspace_id)?
|
|
.update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
|
|
})
|
|
}
|
|
|
|
pub fn delete_mark(
|
|
&self,
|
|
name: String,
|
|
editor: &mut Editor,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
let Some(workspace) = self.workspace(window) else {
|
|
return;
|
|
};
|
|
if name == "`" || name == "'" {
|
|
return;
|
|
}
|
|
let entity_id = workspace.entity_id();
|
|
Vim::update_globals(cx, |vim_globals, cx| {
|
|
let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
|
|
return;
|
|
};
|
|
marks_state.update(cx, |ms, cx| {
|
|
ms.delete_mark(name.clone(), editor.buffer(), 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)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use gpui::TestAppContext;
|
|
|
|
use crate::test::NeovimBackedTestContext;
|
|
|
|
#[gpui::test]
|
|
async fn test_quote_mark(cx: &mut TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("ˇHello, world!").await;
|
|
cx.simulate_shared_keystrokes("w m o").await;
|
|
cx.shared_state().await.assert_eq("Helloˇ, world!");
|
|
cx.simulate_shared_keystrokes("$ ` o").await;
|
|
cx.shared_state().await.assert_eq("Helloˇ, world!");
|
|
cx.simulate_shared_keystrokes("` `").await;
|
|
cx.shared_state().await.assert_eq("Hello, worldˇ!");
|
|
cx.simulate_shared_keystrokes("` `").await;
|
|
cx.shared_state().await.assert_eq("Helloˇ, world!");
|
|
cx.simulate_shared_keystrokes("$ m '").await;
|
|
cx.shared_state().await.assert_eq("Hello, worldˇ!");
|
|
cx.simulate_shared_keystrokes("^ ` `").await;
|
|
cx.shared_state().await.assert_eq("Hello, worldˇ!");
|
|
}
|
|
}
|