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 (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 }
124
125 fn open_path_mark(
126 &mut self,
127 line: bool,
128 path: Arc<Path>,
129 points: Vec<Point>,
130 window: &mut Window,
131 cx: &mut Context<Self>,
132 ) {
133 let Some(workspace) = self.workspace(window) else {
134 return;
135 };
136 let task = workspace.update(cx, |workspace, cx| {
137 workspace.open_abs_path(
138 path.to_path_buf(),
139 OpenOptions {
140 visible: Some(workspace::OpenVisible::All),
141 focus: Some(true),
142 ..Default::default()
143 },
144 window,
145 cx,
146 )
147 });
148 cx.spawn_in(window, async move |this, cx| {
149 let editor = task.await?;
150 this.update_in(cx, |_, window, cx| {
151 if let Some(editor) = editor.act_as::<Editor>(cx) {
152 editor.update(cx, |editor, cx| {
153 let map = editor.snapshot(window, cx);
154 let points: Vec<_> = points
155 .into_iter()
156 .map(|p| {
157 if line {
158 let point = p.to_display_point(&map.display_snapshot);
159 motion::first_non_whitespace(
160 &map.display_snapshot,
161 false,
162 point,
163 )
164 .to_point(&map.display_snapshot)
165 } else {
166 p
167 }
168 })
169 .collect();
170 editor.change_selections(Default::default(), window, cx, |s| {
171 s.select_ranges(points.into_iter().map(|p| p..p))
172 })
173 })
174 }
175 })
176 })
177 .detach_and_log_err(cx);
178 }
179
180 pub fn jump(
181 &mut self,
182 text: Arc<str>,
183 line: bool,
184 should_pop_operator: bool,
185 window: &mut Window,
186 cx: &mut Context<Self>,
187 ) {
188 if should_pop_operator {
189 self.pop_operator(window, cx);
190 }
191 let mark = self
192 .update_editor(cx, |vim, editor, cx| {
193 vim.get_mark(&text, editor, window, cx)
194 })
195 .flatten();
196 let anchors = match mark {
197 None => None,
198 Some(Mark::Local(anchors)) => Some(anchors),
199 Some(Mark::Buffer(entity_id, anchors)) => {
200 self.open_buffer_mark(line, entity_id, anchors, window, cx);
201 return;
202 }
203 Some(Mark::Path(path, points)) => {
204 self.open_path_mark(line, path, points, window, cx);
205 return;
206 }
207 };
208
209 let Some(mut anchors) = anchors else { return };
210
211 self.update_editor(cx, |_, editor, cx| {
212 editor.create_nav_history_entry(cx);
213 });
214 let is_active_operator = self.active_operator().is_some();
215 if is_active_operator {
216 if let Some(anchor) = anchors.last() {
217 self.motion(
218 Motion::Jump {
219 anchor: *anchor,
220 line,
221 },
222 window,
223 cx,
224 )
225 }
226 } else {
227 // Save the last anchor so as to jump to it later.
228 let anchor: Option<Anchor> = anchors.last_mut().map(|anchor| *anchor);
229 let should_jump = self.mode == Mode::Visual
230 || self.mode == Mode::VisualLine
231 || self.mode == Mode::VisualBlock;
232
233 self.update_editor(cx, |_, editor, cx| {
234 let map = editor.snapshot(window, cx);
235 let mut ranges: Vec<Range<Anchor>> = Vec::new();
236 for mut anchor in anchors {
237 if line {
238 let mut point = anchor.to_display_point(&map.display_snapshot);
239 point = motion::first_non_whitespace(&map.display_snapshot, false, point);
240 anchor = map
241 .display_snapshot
242 .buffer_snapshot
243 .anchor_before(point.to_point(&map.display_snapshot));
244 }
245
246 if ranges.last() != Some(&(anchor..anchor)) {
247 ranges.push(anchor..anchor);
248 }
249 }
250
251 if !should_jump && !ranges.is_empty() {
252 editor.change_selections(Default::default(), window, cx, |s| {
253 s.select_anchor_ranges(ranges)
254 });
255 }
256 });
257
258 if should_jump && let Some(anchor) = anchor {
259 self.motion(Motion::Jump { anchor, line }, window, cx)
260 }
261 }
262 }
263
264 pub fn set_mark(
265 &mut self,
266 mut name: String,
267 anchors: Vec<Anchor>,
268 buffer_entity: &Entity<MultiBuffer>,
269 window: &mut Window,
270 cx: &mut App,
271 ) {
272 let Some(workspace) = self.workspace(window) else {
273 return;
274 };
275 if name == "`" {
276 name = "'".to_string();
277 }
278 if matches!(&name[..], "-" | " ") {
279 // Not allowed marks
280 return;
281 }
282 let entity_id = workspace.entity_id();
283 Vim::update_globals(cx, |vim_globals, cx| {
284 let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
285 return;
286 };
287 marks_state.update(cx, |ms, cx| {
288 ms.set_mark(name.clone(), buffer_entity, anchors, cx);
289 });
290 });
291 }
292
293 pub fn get_mark(
294 &self,
295 mut name: &str,
296 editor: &mut Editor,
297 window: &mut Window,
298 cx: &mut App,
299 ) -> Option<Mark> {
300 if name == "`" {
301 name = "'";
302 }
303 if matches!(name, "{" | "}" | "(" | ")") {
304 let (map, selections) = editor.selections.all_display(cx);
305 let anchors = selections
306 .into_iter()
307 .map(|selection| {
308 let point = match name {
309 "{" => movement::start_of_paragraph(&map, selection.head(), 1),
310 "}" => movement::end_of_paragraph(&map, selection.head(), 1),
311 "(" => motion::sentence_backwards(&map, selection.head(), 1),
312 ")" => motion::sentence_forwards(&map, selection.head(), 1),
313 _ => unreachable!(),
314 };
315 map.buffer_snapshot
316 .anchor_before(point.to_offset(&map, Bias::Left))
317 })
318 .collect::<Vec<Anchor>>();
319 return Some(Mark::Local(anchors));
320 }
321 VimGlobals::update_global(cx, |globals, cx| {
322 let workspace_id = self.workspace(window)?.entity_id();
323 globals
324 .marks
325 .get_mut(&workspace_id)?
326 .update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
327 })
328 }
329
330 pub fn delete_mark(
331 &self,
332 name: String,
333 editor: &mut Editor,
334 window: &mut Window,
335 cx: &mut App,
336 ) {
337 let Some(workspace) = self.workspace(window) else {
338 return;
339 };
340 if name == "`" || name == "'" {
341 return;
342 }
343 let entity_id = workspace.entity_id();
344 Vim::update_globals(cx, |vim_globals, cx| {
345 let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
346 return;
347 };
348 marks_state.update(cx, |ms, cx| {
349 ms.delete_mark(name.clone(), editor.buffer(), cx);
350 });
351 });
352 }
353}
354
355pub fn jump_motion(
356 map: &DisplaySnapshot,
357 anchor: Anchor,
358 line: bool,
359) -> (DisplayPoint, SelectionGoal) {
360 let mut point = anchor.to_display_point(map);
361 if line {
362 point = motion::first_non_whitespace(map, false, point)
363 }
364
365 (point, SelectionGoal::None)
366}
367
368#[cfg(test)]
369mod test {
370 use gpui::TestAppContext;
371
372 use crate::test::NeovimBackedTestContext;
373
374 #[gpui::test]
375 async fn test_quote_mark(cx: &mut TestAppContext) {
376 let mut cx = NeovimBackedTestContext::new(cx).await;
377
378 cx.set_shared_state("ˇHello, world!").await;
379 cx.simulate_shared_keystrokes("w m o").await;
380 cx.shared_state().await.assert_eq("Helloˇ, world!");
381 cx.simulate_shared_keystrokes("$ ` o").await;
382 cx.shared_state().await.assert_eq("Helloˇ, world!");
383 cx.simulate_shared_keystrokes("` `").await;
384 cx.shared_state().await.assert_eq("Hello, worldˇ!");
385 cx.simulate_shared_keystrokes("` `").await;
386 cx.shared_state().await.assert_eq("Helloˇ, world!");
387 cx.simulate_shared_keystrokes("$ m '").await;
388 cx.shared_state().await.assert_eq("Hello, worldˇ!");
389 cx.simulate_shared_keystrokes("^ ` `").await;
390 cx.shared_state().await.assert_eq("Hello, worldˇ!");
391 }
392}