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_arc()
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 display_map = editor.display_snapshot(cx);
54 let selections = editor.selections.all_display(&display_map);
55 for selection in selections {
56 let end = movement::saturating_left(&display_map, selection.end);
57 ends.push(
58 display_map
59 .buffer_snapshot()
60 .anchor_before(end.to_offset(&display_map, Bias::Left)),
61 );
62 starts.push(
63 display_map
64 .buffer_snapshot()
65 .anchor_before(selection.start.to_offset(&display_map, Bias::Left)),
66 );
67 reversed.push(selection.reversed)
68 }
69 vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx);
70 vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx);
71 });
72
73 self.stored_visual_mode.replace((mode, reversed));
74 }
75
76 fn open_buffer_mark(
77 &mut self,
78 line: bool,
79 entity_id: EntityId,
80 anchors: Vec<Anchor>,
81 window: &mut Window,
82 cx: &mut Context<Self>,
83 ) {
84 let Some(workspace) = self.workspace(window) else {
85 return;
86 };
87 workspace.update(cx, |workspace, cx| {
88 let item = workspace.items(cx).find(|item| {
89 item.act_as::<Editor>(cx)
90 .is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id)
91 });
92 let Some(item) = item.cloned() else {
93 return;
94 };
95 if let Some(pane) = workspace.pane_for(item.as_ref()) {
96 pane.update(cx, |pane, cx| {
97 if let Some(index) = pane.index_for_item(item.as_ref()) {
98 pane.activate_item(index, true, true, window, cx);
99 }
100 });
101 };
102
103 item.act_as::<Editor>(cx).unwrap().update(cx, |editor, cx| {
104 let map = editor.snapshot(window, cx);
105 let mut ranges: Vec<Range<Anchor>> = Vec::new();
106 for mut anchor in anchors {
107 if line {
108 let mut point = anchor.to_display_point(&map.display_snapshot);
109 point = motion::first_non_whitespace(&map.display_snapshot, false, point);
110 anchor = map
111 .display_snapshot
112 .buffer_snapshot()
113 .anchor_before(point.to_point(&map.display_snapshot));
114 }
115
116 if ranges.last() != Some(&(anchor..anchor)) {
117 ranges.push(anchor..anchor);
118 }
119 }
120
121 editor.change_selections(Default::default(), window, cx, |s| {
122 s.select_anchor_ranges(ranges)
123 });
124 })
125 });
126 }
127
128 fn open_path_mark(
129 &mut self,
130 line: bool,
131 path: Arc<Path>,
132 points: Vec<Point>,
133 window: &mut Window,
134 cx: &mut Context<Self>,
135 ) {
136 let Some(workspace) = self.workspace(window) else {
137 return;
138 };
139 let task = workspace.update(cx, |workspace, cx| {
140 workspace.open_abs_path(
141 path.to_path_buf(),
142 OpenOptions {
143 visible: Some(workspace::OpenVisible::All),
144 focus: Some(true),
145 ..Default::default()
146 },
147 window,
148 cx,
149 )
150 });
151 cx.spawn_in(window, async move |this, cx| {
152 let editor = task.await?;
153 this.update_in(cx, |_, window, cx| {
154 if let Some(editor) = editor.act_as::<Editor>(cx) {
155 editor.update(cx, |editor, cx| {
156 let map = editor.snapshot(window, cx);
157 let points: Vec<_> = points
158 .into_iter()
159 .map(|p| {
160 if line {
161 let point = p.to_display_point(&map.display_snapshot);
162 motion::first_non_whitespace(
163 &map.display_snapshot,
164 false,
165 point,
166 )
167 .to_point(&map.display_snapshot)
168 } else {
169 p
170 }
171 })
172 .collect();
173 editor.change_selections(Default::default(), window, cx, |s| {
174 s.select_ranges(points.into_iter().map(|p| p..p))
175 })
176 })
177 }
178 })
179 })
180 .detach_and_log_err(cx);
181 }
182
183 pub fn jump(
184 &mut self,
185 text: Arc<str>,
186 line: bool,
187 should_pop_operator: bool,
188 window: &mut Window,
189 cx: &mut Context<Self>,
190 ) {
191 if should_pop_operator {
192 self.pop_operator(window, cx);
193 }
194 let mark = self
195 .update_editor(cx, |vim, editor, cx| {
196 vim.get_mark(&text, editor, window, cx)
197 })
198 .flatten();
199 let anchors = match mark {
200 None => None,
201 Some(Mark::Local(anchors)) => Some(anchors),
202 Some(Mark::Buffer(entity_id, anchors)) => {
203 self.open_buffer_mark(line, entity_id, anchors, window, cx);
204 return;
205 }
206 Some(Mark::Path(path, points)) => {
207 self.open_path_mark(line, path, points, window, cx);
208 return;
209 }
210 };
211
212 let Some(mut anchors) = anchors else { return };
213
214 self.update_editor(cx, |_, editor, cx| {
215 editor.create_nav_history_entry(cx);
216 });
217 let is_active_operator = self.active_operator().is_some();
218 if is_active_operator {
219 if let Some(anchor) = anchors.last() {
220 self.motion(
221 Motion::Jump {
222 anchor: *anchor,
223 line,
224 },
225 window,
226 cx,
227 )
228 }
229 } else {
230 // Save the last anchor so as to jump to it later.
231 let anchor: Option<Anchor> = anchors.last_mut().map(|anchor| *anchor);
232 let should_jump = self.mode == Mode::Visual
233 || self.mode == Mode::VisualLine
234 || self.mode == Mode::VisualBlock;
235
236 self.update_editor(cx, |_, editor, cx| {
237 let map = editor.snapshot(window, cx);
238 let mut ranges: Vec<Range<Anchor>> = Vec::new();
239 for mut anchor in anchors {
240 if line {
241 let mut point = anchor.to_display_point(&map.display_snapshot);
242 point = motion::first_non_whitespace(&map.display_snapshot, false, point);
243 anchor = map
244 .display_snapshot
245 .buffer_snapshot()
246 .anchor_before(point.to_point(&map.display_snapshot));
247 }
248
249 if ranges.last() != Some(&(anchor..anchor)) {
250 ranges.push(anchor..anchor);
251 }
252 }
253
254 if !should_jump && !ranges.is_empty() {
255 editor.change_selections(Default::default(), window, cx, |s| {
256 s.select_anchor_ranges(ranges)
257 });
258 }
259 });
260
261 if should_jump && let Some(anchor) = anchor {
262 self.motion(Motion::Jump { anchor, line }, window, cx)
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 display_map = editor.display_snapshot(cx);
308 let selections = editor.selections.all_display(&display_map);
309 let anchors = selections
310 .into_iter()
311 .map(|selection| {
312 let point = match name {
313 "{" => movement::start_of_paragraph(&display_map, selection.head(), 1),
314 "}" => movement::end_of_paragraph(&display_map, selection.head(), 1),
315 "(" => motion::sentence_backwards(&display_map, selection.head(), 1),
316 ")" => motion::sentence_forwards(&display_map, selection.head(), 1),
317 _ => unreachable!(),
318 };
319 display_map
320 .buffer_snapshot()
321 .anchor_before(point.to_offset(&display_map, Bias::Left))
322 })
323 .collect::<Vec<Anchor>>();
324 return Some(Mark::Local(anchors));
325 }
326 VimGlobals::update_global(cx, |globals, cx| {
327 let workspace_id = self.workspace(window)?.entity_id();
328 globals
329 .marks
330 .get_mut(&workspace_id)?
331 .update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
332 })
333 }
334
335 pub fn delete_mark(
336 &self,
337 name: String,
338 editor: &mut Editor,
339 window: &mut Window,
340 cx: &mut App,
341 ) {
342 let Some(workspace) = self.workspace(window) else {
343 return;
344 };
345 if name == "`" || name == "'" {
346 return;
347 }
348 let entity_id = workspace.entity_id();
349 Vim::update_globals(cx, |vim_globals, cx| {
350 let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
351 return;
352 };
353 marks_state.update(cx, |ms, cx| {
354 ms.delete_mark(name.clone(), editor.buffer(), cx);
355 });
356 });
357 }
358}
359
360pub fn jump_motion(
361 map: &DisplaySnapshot,
362 anchor: Anchor,
363 line: bool,
364) -> (DisplayPoint, SelectionGoal) {
365 let mut point = anchor.to_display_point(map);
366 if line {
367 point = motion::first_non_whitespace(map, false, point)
368 }
369
370 (point, SelectionGoal::None)
371}
372
373#[cfg(test)]
374mod test {
375 use gpui::TestAppContext;
376
377 use crate::test::NeovimBackedTestContext;
378
379 #[gpui::test]
380 async fn test_quote_mark(cx: &mut TestAppContext) {
381 let mut cx = NeovimBackedTestContext::new(cx).await;
382
383 cx.set_shared_state("ˇHello, world!").await;
384 cx.simulate_shared_keystrokes("w m o").await;
385 cx.shared_state().await.assert_eq("Helloˇ, world!");
386 cx.simulate_shared_keystrokes("$ ` o").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("` `").await;
391 cx.shared_state().await.assert_eq("Helloˇ, world!");
392 cx.simulate_shared_keystrokes("$ m '").await;
393 cx.shared_state().await.assert_eq("Hello, worldˇ!");
394 cx.simulate_shared_keystrokes("^ ` `").await;
395 cx.shared_state().await.assert_eq("Hello, worldˇ!");
396 }
397}