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 && let Some(anchor) = anchor {
260 self.motion(Motion::Jump { anchor, line }, window, cx)
261 }
262 }
263 }
264
265 pub fn set_mark(
266 &mut self,
267 mut name: String,
268 anchors: Vec<Anchor>,
269 buffer_entity: &Entity<MultiBuffer>,
270 window: &mut Window,
271 cx: &mut App,
272 ) {
273 let Some(workspace) = self.workspace(window) else {
274 return;
275 };
276 if name == "`" {
277 name = "'".to_string();
278 }
279 if matches!(&name[..], "-" | " ") {
280 // Not allowed marks
281 return;
282 }
283 let entity_id = workspace.entity_id();
284 Vim::update_globals(cx, |vim_globals, cx| {
285 let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
286 return;
287 };
288 marks_state.update(cx, |ms, cx| {
289 ms.set_mark(name.clone(), buffer_entity, anchors, cx);
290 });
291 });
292 }
293
294 pub fn get_mark(
295 &self,
296 mut name: &str,
297 editor: &mut Editor,
298 window: &mut Window,
299 cx: &mut App,
300 ) -> Option<Mark> {
301 if name == "`" {
302 name = "'";
303 }
304 if matches!(name, "{" | "}" | "(" | ")") {
305 let (map, selections) = editor.selections.all_display(cx);
306 let anchors = selections
307 .into_iter()
308 .map(|selection| {
309 let point = match name {
310 "{" => movement::start_of_paragraph(&map, selection.head(), 1),
311 "}" => movement::end_of_paragraph(&map, selection.head(), 1),
312 "(" => motion::sentence_backwards(&map, selection.head(), 1),
313 ")" => motion::sentence_forwards(&map, selection.head(), 1),
314 _ => unreachable!(),
315 };
316 map.buffer_snapshot
317 .anchor_before(point.to_offset(&map, Bias::Left))
318 })
319 .collect::<Vec<Anchor>>();
320 return Some(Mark::Local(anchors));
321 }
322 VimGlobals::update_global(cx, |globals, cx| {
323 let workspace_id = self.workspace(window)?.entity_id();
324 globals
325 .marks
326 .get_mut(&workspace_id)?
327 .update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
328 })
329 }
330
331 pub fn delete_mark(
332 &self,
333 name: String,
334 editor: &mut Editor,
335 window: &mut Window,
336 cx: &mut App,
337 ) {
338 let Some(workspace) = self.workspace(window) else {
339 return;
340 };
341 if name == "`" || name == "'" {
342 return;
343 }
344 let entity_id = workspace.entity_id();
345 Vim::update_globals(cx, |vim_globals, cx| {
346 let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
347 return;
348 };
349 marks_state.update(cx, |ms, cx| {
350 ms.delete_mark(name.clone(), editor.buffer(), cx);
351 });
352 });
353 }
354}
355
356pub fn jump_motion(
357 map: &DisplaySnapshot,
358 anchor: Anchor,
359 line: bool,
360) -> (DisplayPoint, SelectionGoal) {
361 let mut point = anchor.to_display_point(map);
362 if line {
363 point = motion::first_non_whitespace(map, false, point)
364 }
365
366 (point, SelectionGoal::None)
367}
368
369#[cfg(test)]
370mod test {
371 use gpui::TestAppContext;
372
373 use crate::test::NeovimBackedTestContext;
374
375 #[gpui::test]
376 async fn test_quote_mark(cx: &mut TestAppContext) {
377 let mut cx = NeovimBackedTestContext::new(cx).await;
378
379 cx.set_shared_state("ˇHello, world!").await;
380 cx.simulate_shared_keystrokes("w m o").await;
381 cx.shared_state().await.assert_eq("Helloˇ, world!");
382 cx.simulate_shared_keystrokes("$ ` o").await;
383 cx.shared_state().await.assert_eq("Helloˇ, world!");
384 cx.simulate_shared_keystrokes("` `").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("$ m '").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 }
393}