1use std::{ops::Range, path::Path, sync::Arc};
2
3use editor::{
4 Anchor, Bias, DisplayPoint, Editor, MultiBuffer,
5 display_map::{DisplaySnapshot, ToDisplayPoint},
6 movement,
7};
8use gpui::{Context, Entity, EntityId, UpdateGlobal, Window};
9use language::SelectionGoal;
10use text::Point;
11use ui::App;
12use workspace::OpenOptions;
13
14use crate::{
15 Vim,
16 motion::{self, Motion},
17 state::{Mark, Mode, VimGlobals},
18};
19
20impl Vim {
21 pub fn create_mark(&mut self, text: Arc<str>, window: &mut Window, cx: &mut Context<Self>) {
22 self.update_editor(cx, |vim, editor, cx| {
23 let anchors = editor
24 .selections
25 .disjoint_anchors()
26 .iter()
27 .map(|s| s.head())
28 .collect::<Vec<_>>();
29 vim.set_mark(text.to_string(), anchors, editor.buffer(), window, cx);
30 });
31 self.clear_operator(window, cx);
32 }
33
34 // When handling an action, you must create visual marks if you will switch to normal
35 // mode without the default selection behavior.
36 pub(crate) fn store_visual_marks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
37 if self.mode.is_visual() {
38 self.create_visual_marks(self.mode, window, cx);
39 }
40 }
41
42 pub(crate) fn create_visual_marks(
43 &mut self,
44 mode: Mode,
45 window: &mut Window,
46 cx: &mut Context<Self>,
47 ) {
48 let mut starts = vec![];
49 let mut ends = vec![];
50 let mut reversed = vec![];
51
52 self.update_editor(cx, |vim, editor, cx| {
53 let (map, selections) = editor.selections.all_display(cx);
54 for selection in selections {
55 let end = movement::saturating_left(&map, selection.end);
56 ends.push(
57 map.buffer_snapshot
58 .anchor_before(end.to_offset(&map, Bias::Left)),
59 );
60 starts.push(
61 map.buffer_snapshot
62 .anchor_before(selection.start.to_offset(&map, Bias::Left)),
63 );
64 reversed.push(selection.reversed)
65 }
66 vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx);
67 vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx);
68 });
69
70 self.stored_visual_mode.replace((mode, reversed));
71 }
72
73 fn open_buffer_mark(
74 &mut self,
75 line: bool,
76 entity_id: EntityId,
77 anchors: Vec<Anchor>,
78 window: &mut Window,
79 cx: &mut Context<Self>,
80 ) {
81 let Some(workspace) = self.workspace(window) else {
82 return;
83 };
84 workspace.update(cx, |workspace, cx| {
85 let item = workspace.items(cx).find(|item| {
86 item.act_as::<Editor>(cx)
87 .is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id)
88 });
89 let Some(item) = item.cloned() else {
90 return;
91 };
92 if let Some(pane) = workspace.pane_for(item.as_ref()) {
93 pane.update(cx, |pane, cx| {
94 if let Some(index) = pane.index_for_item(item.as_ref()) {
95 pane.activate_item(index, true, true, window, cx);
96 }
97 });
98 };
99
100 item.act_as::<Editor>(cx).unwrap().update(cx, |editor, cx| {
101 let map = editor.snapshot(window, cx);
102 let mut ranges: Vec<Range<Anchor>> = Vec::new();
103 for mut anchor in anchors {
104 if line {
105 let mut point = anchor.to_display_point(&map.display_snapshot);
106 point = motion::first_non_whitespace(&map.display_snapshot, false, point);
107 anchor = map
108 .display_snapshot
109 .buffer_snapshot
110 .anchor_before(point.to_point(&map.display_snapshot));
111 }
112
113 if ranges.last() != Some(&(anchor..anchor)) {
114 ranges.push(anchor..anchor);
115 }
116 }
117
118 editor.change_selections(Default::default(), window, cx, |s| {
119 s.select_anchor_ranges(ranges)
120 });
121 })
122 });
123 return;
124 }
125
126 fn open_path_mark(
127 &mut self,
128 line: bool,
129 path: Arc<Path>,
130 points: Vec<Point>,
131 window: &mut Window,
132 cx: &mut Context<Self>,
133 ) {
134 let Some(workspace) = self.workspace(window) else {
135 return;
136 };
137 let task = workspace.update(cx, |workspace, cx| {
138 workspace.open_abs_path(
139 path.to_path_buf(),
140 OpenOptions {
141 visible: Some(workspace::OpenVisible::All),
142 focus: Some(true),
143 ..Default::default()
144 },
145 window,
146 cx,
147 )
148 });
149 cx.spawn_in(window, async move |this, cx| {
150 let editor = task.await?;
151 this.update_in(cx, |_, window, cx| {
152 if let Some(editor) = editor.act_as::<Editor>(cx) {
153 editor.update(cx, |editor, cx| {
154 let map = editor.snapshot(window, cx);
155 let points: Vec<_> = points
156 .into_iter()
157 .map(|p| {
158 if line {
159 let point = p.to_display_point(&map.display_snapshot);
160 motion::first_non_whitespace(
161 &map.display_snapshot,
162 false,
163 point,
164 )
165 .to_point(&map.display_snapshot)
166 } else {
167 p
168 }
169 })
170 .collect();
171 editor.change_selections(Default::default(), window, cx, |s| {
172 s.select_ranges(points.into_iter().map(|p| p..p))
173 })
174 })
175 }
176 })
177 })
178 .detach_and_log_err(cx);
179 }
180
181 pub fn jump(
182 &mut self,
183 text: Arc<str>,
184 line: bool,
185 should_pop_operator: bool,
186 window: &mut Window,
187 cx: &mut Context<Self>,
188 ) {
189 if should_pop_operator {
190 self.pop_operator(window, cx);
191 }
192 let mark = self
193 .update_editor(cx, |vim, editor, cx| {
194 vim.get_mark(&text, editor, window, cx)
195 })
196 .flatten();
197 let anchors = match mark {
198 None => None,
199 Some(Mark::Local(anchors)) => Some(anchors),
200 Some(Mark::Buffer(entity_id, anchors)) => {
201 self.open_buffer_mark(line, entity_id, anchors, window, cx);
202 return;
203 }
204 Some(Mark::Path(path, points)) => {
205 self.open_path_mark(line, path, points, window, cx);
206 return;
207 }
208 };
209
210 let Some(mut anchors) = anchors else { return };
211
212 self.update_editor(cx, |_, editor, cx| {
213 editor.create_nav_history_entry(cx);
214 });
215 let is_active_operator = self.active_operator().is_some();
216 if is_active_operator {
217 if let Some(anchor) = anchors.last() {
218 self.motion(
219 Motion::Jump {
220 anchor: *anchor,
221 line,
222 },
223 window,
224 cx,
225 )
226 }
227 } else {
228 // Save the last anchor so as to jump to it later.
229 let anchor: Option<Anchor> = anchors.last_mut().map(|anchor| *anchor);
230 let should_jump = self.mode == Mode::Visual
231 || self.mode == Mode::VisualLine
232 || self.mode == Mode::VisualBlock;
233
234 self.update_editor(cx, |_, editor, cx| {
235 let map = editor.snapshot(window, cx);
236 let mut ranges: Vec<Range<Anchor>> = Vec::new();
237 for mut anchor in anchors {
238 if line {
239 let mut point = anchor.to_display_point(&map.display_snapshot);
240 point = motion::first_non_whitespace(&map.display_snapshot, false, point);
241 anchor = map
242 .display_snapshot
243 .buffer_snapshot
244 .anchor_before(point.to_point(&map.display_snapshot));
245 }
246
247 if ranges.last() != Some(&(anchor..anchor)) {
248 ranges.push(anchor..anchor);
249 }
250 }
251
252 if !should_jump && !ranges.is_empty() {
253 editor.change_selections(Default::default(), window, cx, |s| {
254 s.select_anchor_ranges(ranges)
255 });
256 }
257 });
258
259 if should_jump {
260 if let Some(anchor) = anchor {
261 self.motion(Motion::Jump { anchor, line }, window, cx)
262 }
263 }
264 }
265 }
266
267 pub fn set_mark(
268 &mut self,
269 mut name: String,
270 anchors: Vec<Anchor>,
271 buffer_entity: &Entity<MultiBuffer>,
272 window: &mut Window,
273 cx: &mut App,
274 ) {
275 let Some(workspace) = self.workspace(window) else {
276 return;
277 };
278 if name == "`" {
279 name = "'".to_string();
280 }
281 if matches!(&name[..], "-" | " ") {
282 // Not allowed marks
283 return;
284 }
285 let entity_id = workspace.entity_id();
286 Vim::update_globals(cx, |vim_globals, cx| {
287 let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
288 return;
289 };
290 marks_state.update(cx, |ms, cx| {
291 ms.set_mark(name.clone(), buffer_entity, anchors, cx);
292 });
293 });
294 }
295
296 pub fn get_mark(
297 &self,
298 mut name: &str,
299 editor: &mut Editor,
300 window: &mut Window,
301 cx: &mut App,
302 ) -> Option<Mark> {
303 if name == "`" {
304 name = "'";
305 }
306 if matches!(name, "{" | "}" | "(" | ")") {
307 let (map, selections) = editor.selections.all_display(cx);
308 let anchors = selections
309 .into_iter()
310 .map(|selection| {
311 let point = match name {
312 "{" => movement::start_of_paragraph(&map, selection.head(), 1),
313 "}" => movement::end_of_paragraph(&map, selection.head(), 1),
314 "(" => motion::sentence_backwards(&map, selection.head(), 1),
315 ")" => motion::sentence_forwards(&map, selection.head(), 1),
316 _ => unreachable!(),
317 };
318 map.buffer_snapshot
319 .anchor_before(point.to_offset(&map, Bias::Left))
320 })
321 .collect::<Vec<Anchor>>();
322 return Some(Mark::Local(anchors));
323 }
324 VimGlobals::update_global(cx, |globals, cx| {
325 let workspace_id = self.workspace(window)?.entity_id();
326 globals
327 .marks
328 .get_mut(&workspace_id)?
329 .update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
330 })
331 }
332
333 pub fn delete_mark(
334 &self,
335 name: String,
336 editor: &mut Editor,
337 window: &mut Window,
338 cx: &mut App,
339 ) {
340 let Some(workspace) = self.workspace(window) else {
341 return;
342 };
343 if name == "`" || name == "'" {
344 return;
345 }
346 let entity_id = workspace.entity_id();
347 Vim::update_globals(cx, |vim_globals, cx| {
348 let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
349 return;
350 };
351 marks_state.update(cx, |ms, cx| {
352 ms.delete_mark(name.clone(), editor.buffer(), cx);
353 });
354 });
355 }
356}
357
358pub fn jump_motion(
359 map: &DisplaySnapshot,
360 anchor: Anchor,
361 line: bool,
362) -> (DisplayPoint, SelectionGoal) {
363 let mut point = anchor.to_display_point(map);
364 if line {
365 point = motion::first_non_whitespace(map, false, point)
366 }
367
368 (point, SelectionGoal::None)
369}
370
371#[cfg(test)]
372mod test {
373 use gpui::TestAppContext;
374
375 use crate::test::NeovimBackedTestContext;
376
377 #[gpui::test]
378 async fn test_quote_mark(cx: &mut TestAppContext) {
379 let mut cx = NeovimBackedTestContext::new(cx).await;
380
381 cx.set_shared_state("ˇHello, world!").await;
382 cx.simulate_shared_keystrokes("w m o").await;
383 cx.shared_state().await.assert_eq("Helloˇ, world!");
384 cx.simulate_shared_keystrokes("$ ` o").await;
385 cx.shared_state().await.assert_eq("Helloˇ, world!");
386 cx.simulate_shared_keystrokes("` `").await;
387 cx.shared_state().await.assert_eq("Hello, worldˇ!");
388 cx.simulate_shared_keystrokes("` `").await;
389 cx.shared_state().await.assert_eq("Helloˇ, world!");
390 cx.simulate_shared_keystrokes("$ m '").await;
391 cx.shared_state().await.assert_eq("Hello, worldˇ!");
392 cx.simulate_shared_keystrokes("^ ` `").await;
393 cx.shared_state().await.assert_eq("Hello, worldˇ!");
394 }
395}