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