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