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