mark.rs

  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}