go_to_line.rs

  1pub mod cursor_position;
  2
  3use cursor_position::{LineIndicatorFormat, UserCaretPosition};
  4use editor::{
  5    Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, SelectionEffects, ToOffset, ToPoint,
  6    actions::Tab, scroll::Autoscroll,
  7};
  8use gpui::{
  9    App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
 10    Subscription, div, prelude::*,
 11};
 12use language::Buffer;
 13use settings::Settings;
 14use text::{Bias, Point};
 15use theme::ActiveTheme;
 16use ui::prelude::*;
 17use util::paths::FILE_ROW_COLUMN_DELIMITER;
 18use workspace::ModalView;
 19
 20pub fn init(cx: &mut App) {
 21    LineIndicatorFormat::register(cx);
 22    cx.observe_new(GoToLine::register).detach();
 23}
 24
 25pub struct GoToLine {
 26    line_editor: Entity<Editor>,
 27    active_editor: Entity<Editor>,
 28    current_text: SharedString,
 29    prev_scroll_position: Option<gpui::Point<f32>>,
 30    _subscriptions: Vec<Subscription>,
 31}
 32
 33impl ModalView for GoToLine {}
 34
 35impl Focusable for GoToLine {
 36    fn focus_handle(&self, cx: &App) -> FocusHandle {
 37        self.line_editor.focus_handle(cx)
 38    }
 39}
 40impl EventEmitter<DismissEvent> for GoToLine {}
 41
 42enum GoToLineRowHighlights {}
 43
 44impl GoToLine {
 45    fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<Editor>) {
 46        let handle = cx.entity().downgrade();
 47        editor
 48            .register_action(move |_: &editor::actions::ToggleGoToLine, window, cx| {
 49                let Some(editor_handle) = handle.upgrade() else {
 50                    return;
 51                };
 52                let Some(workspace) = editor_handle.read(cx).workspace() else {
 53                    return;
 54                };
 55                let editor = editor_handle.read(cx);
 56                let Some((_, buffer, _)) = editor.active_excerpt(cx) else {
 57                    return;
 58                };
 59                workspace.update(cx, |workspace, cx| {
 60                    workspace.toggle_modal(window, cx, move |window, cx| {
 61                        GoToLine::new(editor_handle, buffer, window, cx)
 62                    });
 63                })
 64            })
 65            .detach();
 66    }
 67
 68    pub fn new(
 69        active_editor: Entity<Editor>,
 70        active_buffer: Entity<Buffer>,
 71        window: &mut Window,
 72        cx: &mut Context<Self>,
 73    ) -> Self {
 74        let (user_caret, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
 75            let user_caret = UserCaretPosition::at_selection_end(
 76                &editor.selections.last::<Point>(cx),
 77                &editor.buffer().read(cx).snapshot(cx),
 78            );
 79
 80            let snapshot = active_buffer.read(cx).snapshot();
 81            let last_line = editor
 82                .buffer()
 83                .read(cx)
 84                .excerpts_for_buffer(snapshot.remote_id(), cx)
 85                .into_iter()
 86                .map(move |(_, range)| text::ToPoint::to_point(&range.context.end, &snapshot).row)
 87                .max()
 88                .unwrap_or(0);
 89
 90            (user_caret, last_line, editor.scroll_position(cx))
 91        });
 92
 93        let line = user_caret.line.get();
 94        let column = user_caret.character.get();
 95
 96        let line_editor = cx.new(|cx| {
 97            let mut editor = Editor::single_line(window, cx);
 98            let editor_handle = cx.entity().downgrade();
 99            editor
100                .register_action::<Tab>({
101                    move |_, window, cx| {
102                        let Some(editor) = editor_handle.upgrade() else {
103                            return;
104                        };
105                        editor.update(cx, |editor, cx| {
106                            if let Some(placeholder_text) = editor.placeholder_text()
107                                && editor.text(cx).is_empty()
108                            {
109                                let placeholder_text = placeholder_text.to_string();
110                                editor.set_text(placeholder_text, window, cx);
111                            }
112                        });
113                    }
114                })
115                .detach();
116            editor.set_placeholder_text(format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"), cx);
117            editor
118        });
119        let line_editor_change = cx.subscribe_in(&line_editor, window, Self::on_line_editor_event);
120
121        let current_text = format!(
122            "Current Line: {} of {} (column {})",
123            line,
124            last_line + 1,
125            column
126        );
127
128        Self {
129            line_editor,
130            active_editor,
131            current_text: current_text.into(),
132            prev_scroll_position: Some(scroll_position),
133            _subscriptions: vec![line_editor_change, cx.on_release_in(window, Self::release)],
134        }
135    }
136
137    fn release(&mut self, window: &mut Window, cx: &mut App) {
138        let scroll_position = self.prev_scroll_position.take();
139        self.active_editor.update(cx, |editor, cx| {
140            editor.clear_row_highlights::<GoToLineRowHighlights>();
141            if let Some(scroll_position) = scroll_position {
142                editor.set_scroll_position(scroll_position, window, cx);
143            }
144            cx.notify();
145        })
146    }
147
148    fn on_line_editor_event(
149        &mut self,
150        _: &Entity<Editor>,
151        event: &editor::EditorEvent,
152        _window: &mut Window,
153        cx: &mut Context<Self>,
154    ) {
155        match event {
156            editor::EditorEvent::Blurred => {
157                self.prev_scroll_position.take();
158                cx.emit(DismissEvent)
159            }
160            editor::EditorEvent::BufferEdited => self.highlight_current_line(cx),
161            _ => {}
162        }
163    }
164
165    fn highlight_current_line(&mut self, cx: &mut Context<Self>) {
166        self.active_editor.update(cx, |editor, cx| {
167            editor.clear_row_highlights::<GoToLineRowHighlights>();
168            let snapshot = editor.buffer().read(cx).snapshot(cx);
169            let Some(start) = self.anchor_from_query(&snapshot, cx) else {
170                return;
171            };
172            let mut start_point = start.to_point(&snapshot);
173            start_point.column = 0;
174            // Force non-empty range to ensure the line is highlighted.
175            let mut end_point = snapshot.clip_point(start_point + Point::new(0, 1), Bias::Left);
176            if start_point == end_point {
177                end_point = snapshot.clip_point(start_point + Point::new(1, 0), Bias::Left);
178            }
179
180            let end = snapshot.anchor_after(end_point);
181            editor.highlight_rows::<GoToLineRowHighlights>(
182                start..end,
183                cx.theme().colors().editor_highlighted_line_background,
184                RowHighlightOptions {
185                    autoscroll: true,
186                    ..Default::default()
187                },
188                cx,
189            );
190            editor.request_autoscroll(Autoscroll::center(), cx);
191        });
192        cx.notify();
193    }
194
195    fn anchor_from_query(
196        &self,
197        snapshot: &MultiBufferSnapshot,
198        cx: &Context<Editor>,
199    ) -> Option<Anchor> {
200        let (query_row, query_char) = self.line_and_char_from_query(cx)?;
201        let row = query_row.saturating_sub(1);
202        let character = query_char.unwrap_or(0).saturating_sub(1);
203
204        let start_offset = Point::new(row, 0).to_offset(snapshot);
205        const MAX_BYTES_IN_UTF_8: u32 = 4;
206        let max_end_offset = snapshot
207            .clip_point(
208                Point::new(row, character * MAX_BYTES_IN_UTF_8 + 1),
209                Bias::Right,
210            )
211            .to_offset(snapshot);
212
213        let mut chars_to_iterate = character;
214        let mut end_offset = start_offset;
215        'outer: for text_chunk in snapshot.text_for_range(start_offset..max_end_offset) {
216            let mut offset_increment = 0;
217            for c in text_chunk.chars() {
218                if chars_to_iterate == 0 {
219                    end_offset += offset_increment;
220                    break 'outer;
221                } else {
222                    chars_to_iterate -= 1;
223                    offset_increment += c.len_utf8();
224                }
225            }
226            end_offset += offset_increment;
227        }
228        Some(snapshot.anchor_before(snapshot.clip_offset(end_offset, Bias::Left)))
229    }
230
231    fn line_and_char_from_query(&self, cx: &App) -> Option<(u32, Option<u32>)> {
232        let input = self.line_editor.read(cx).text(cx);
233        let mut components = input
234            .splitn(2, FILE_ROW_COLUMN_DELIMITER)
235            .map(str::trim)
236            .fuse();
237        let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
238        let column = components.next().and_then(|col| col.parse::<u32>().ok());
239        Some((row, column))
240    }
241
242    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
243        cx.emit(DismissEvent);
244    }
245
246    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
247        self.active_editor.update(cx, |editor, cx| {
248            let snapshot = editor.buffer().read(cx).snapshot(cx);
249            let Some(start) = self.anchor_from_query(&snapshot, cx) else {
250                return;
251            };
252            editor.change_selections(
253                SelectionEffects::scroll(Autoscroll::center()),
254                window,
255                cx,
256                |s| s.select_anchor_ranges([start..start]),
257            );
258            editor.focus_handle(cx).focus(window);
259            cx.notify()
260        });
261        self.prev_scroll_position.take();
262
263        cx.emit(DismissEvent);
264    }
265}
266
267impl Render for GoToLine {
268    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
269        let help_text = match self.line_and_char_from_query(cx) {
270            Some((line, Some(character))) => {
271                format!("Go to line {line}, character {character}").into()
272            }
273            Some((line, None)) => format!("Go to line {line}").into(),
274            None => self.current_text.clone(),
275        };
276
277        v_flex()
278            .w(rems(24.))
279            .elevation_2(cx)
280            .key_context("GoToLine")
281            .on_action(cx.listener(Self::cancel))
282            .on_action(cx.listener(Self::confirm))
283            .child(
284                div()
285                    .border_b_1()
286                    .border_color(cx.theme().colors().border_variant)
287                    .px_2()
288                    .py_1()
289                    .child(self.line_editor.clone()),
290            )
291            .child(
292                h_flex()
293                    .px_2()
294                    .py_1()
295                    .gap_1()
296                    .child(Label::new(help_text).color(Color::Muted)),
297            )
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use cursor_position::{CursorPosition, SelectionStats, UserCaretPosition};
305    use editor::actions::{MoveRight, MoveToBeginning, SelectAll};
306    use gpui::{TestAppContext, VisualTestContext};
307    use indoc::indoc;
308    use project::{FakeFs, Project};
309    use serde_json::json;
310    use std::{num::NonZeroU32, sync::Arc, time::Duration};
311    use util::path;
312    use workspace::{AppState, Workspace};
313
314    #[gpui::test]
315    async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
316        init_test(cx);
317        let fs = FakeFs::new(cx.executor());
318        fs.insert_tree(
319            path!("/dir"),
320            json!({
321                "a.rs": indoc!{"
322                    struct SingleLine; // display line 0
323                                       // display line 1
324                    struct MultiLine { // display line 2
325                        field_1: i32,  // display line 3
326                        field_2: i32,  // display line 4
327                    }                  // display line 5
328                                       // display line 6
329                    struct Another {   // display line 7
330                        field_1: i32,  // display line 8
331                        field_2: i32,  // display line 9
332                        field_3: i32,  // display line 10
333                        field_4: i32,  // display line 11
334                    }                  // display line 12
335                "}
336            }),
337        )
338        .await;
339
340        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
341        let (workspace, cx) =
342            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
343        let worktree_id = workspace.update(cx, |workspace, cx| {
344            workspace.project().update(cx, |project, cx| {
345                project.worktrees(cx).next().unwrap().read(cx).id()
346            })
347        });
348        let _buffer = project
349            .update(cx, |project, cx| {
350                project.open_local_buffer(path!("/dir/a.rs"), cx)
351            })
352            .await
353            .unwrap();
354        let editor = workspace
355            .update_in(cx, |workspace, window, cx| {
356                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
357            })
358            .await
359            .unwrap()
360            .downcast::<Editor>()
361            .unwrap();
362
363        let go_to_line_view = open_go_to_line_view(&workspace, cx);
364        assert_eq!(
365            highlighted_display_rows(&editor, cx),
366            Vec::<u32>::new(),
367            "Initially opened go to line modal should not highlight any rows"
368        );
369        assert_single_caret_at_row(&editor, 0, cx);
370
371        cx.simulate_input("1");
372        assert_eq!(
373            highlighted_display_rows(&editor, cx),
374            vec![0],
375            "Go to line modal should highlight a row, corresponding to the query"
376        );
377        assert_single_caret_at_row(&editor, 0, cx);
378
379        cx.simulate_input("8");
380        assert_eq!(
381            highlighted_display_rows(&editor, cx),
382            vec![13],
383            "If the query is too large, the last row should be highlighted"
384        );
385        assert_single_caret_at_row(&editor, 0, cx);
386
387        cx.dispatch_action(menu::Cancel);
388        drop(go_to_line_view);
389        editor.update(cx, |_, _| {});
390        assert_eq!(
391            highlighted_display_rows(&editor, cx),
392            Vec::<u32>::new(),
393            "After cancelling and closing the modal, no rows should be highlighted"
394        );
395        assert_single_caret_at_row(&editor, 0, cx);
396
397        let go_to_line_view = open_go_to_line_view(&workspace, cx);
398        assert_eq!(
399            highlighted_display_rows(&editor, cx),
400            Vec::<u32>::new(),
401            "Reopened modal should not highlight any rows"
402        );
403        assert_single_caret_at_row(&editor, 0, cx);
404
405        let expected_highlighted_row = 4;
406        cx.simulate_input("5");
407        assert_eq!(
408            highlighted_display_rows(&editor, cx),
409            vec![expected_highlighted_row]
410        );
411        assert_single_caret_at_row(&editor, 0, cx);
412        cx.dispatch_action(menu::Confirm);
413        drop(go_to_line_view);
414        editor.update(cx, |_, _| {});
415        assert_eq!(
416            highlighted_display_rows(&editor, cx),
417            Vec::<u32>::new(),
418            "After confirming and closing the modal, no rows should be highlighted"
419        );
420        // On confirm, should place the caret on the highlighted row.
421        assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
422    }
423
424    #[gpui::test]
425    async fn test_unicode_characters_selection(cx: &mut TestAppContext) {
426        init_test(cx);
427
428        let fs = FakeFs::new(cx.executor());
429        fs.insert_tree(
430            path!("/dir"),
431            json!({
432                "a.rs": "ēlo"
433            }),
434        )
435        .await;
436
437        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
438        let (workspace, cx) =
439            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
440        workspace.update_in(cx, |workspace, window, cx| {
441            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
442            workspace.status_bar().update(cx, |status_bar, cx| {
443                status_bar.add_right_item(cursor_position, window, cx);
444            });
445        });
446
447        let worktree_id = workspace.update(cx, |workspace, cx| {
448            workspace.project().update(cx, |project, cx| {
449                project.worktrees(cx).next().unwrap().read(cx).id()
450            })
451        });
452        let _buffer = project
453            .update(cx, |project, cx| {
454                project.open_local_buffer(path!("/dir/a.rs"), cx)
455            })
456            .await
457            .unwrap();
458        let editor = workspace
459            .update_in(cx, |workspace, window, cx| {
460                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
461            })
462            .await
463            .unwrap()
464            .downcast::<Editor>()
465            .unwrap();
466
467        cx.executor().advance_clock(Duration::from_millis(200));
468        workspace.update(cx, |workspace, cx| {
469            assert_eq!(
470                &SelectionStats {
471                    lines: 0,
472                    characters: 0,
473                    selections: 1,
474                },
475                workspace
476                    .status_bar()
477                    .read(cx)
478                    .item_of_type::<CursorPosition>()
479                    .expect("missing cursor position item")
480                    .read(cx)
481                    .selection_stats(),
482                "No selections should be initially"
483            );
484        });
485        editor.update_in(cx, |editor, window, cx| {
486            editor.select_all(&SelectAll, window, cx)
487        });
488        cx.executor().advance_clock(Duration::from_millis(200));
489        workspace.update(cx, |workspace, cx| {
490            assert_eq!(
491                &SelectionStats {
492                    lines: 1,
493                    characters: 3,
494                    selections: 1,
495                },
496                workspace
497                    .status_bar()
498                    .read(cx)
499                    .item_of_type::<CursorPosition>()
500                    .expect("missing cursor position item")
501                    .read(cx)
502                    .selection_stats(),
503                "After selecting a text with multibyte unicode characters, the character count should be correct"
504            );
505        });
506    }
507
508    #[gpui::test]
509    async fn test_unicode_line_numbers(cx: &mut TestAppContext) {
510        init_test(cx);
511
512        let text = "ēlo你好";
513        let fs = FakeFs::new(cx.executor());
514        fs.insert_tree(
515            path!("/dir"),
516            json!({
517                "a.rs": text
518            }),
519        )
520        .await;
521
522        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
523        let (workspace, cx) =
524            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
525        workspace.update_in(cx, |workspace, window, cx| {
526            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
527            workspace.status_bar().update(cx, |status_bar, cx| {
528                status_bar.add_right_item(cursor_position, window, cx);
529            });
530        });
531
532        let worktree_id = workspace.update(cx, |workspace, cx| {
533            workspace.project().update(cx, |project, cx| {
534                project.worktrees(cx).next().unwrap().read(cx).id()
535            })
536        });
537        let _buffer = project
538            .update(cx, |project, cx| {
539                project.open_local_buffer(path!("/dir/a.rs"), cx)
540            })
541            .await
542            .unwrap();
543        let editor = workspace
544            .update_in(cx, |workspace, window, cx| {
545                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
546            })
547            .await
548            .unwrap()
549            .downcast::<Editor>()
550            .unwrap();
551
552        editor.update_in(cx, |editor, window, cx| {
553            editor.move_to_beginning(&MoveToBeginning, window, cx)
554        });
555        cx.executor().advance_clock(Duration::from_millis(200));
556        assert_eq!(
557            user_caret_position(1, 1),
558            current_position(&workspace, cx),
559            "Beginning of the line should be at first line, before any characters"
560        );
561
562        for (i, c) in text.chars().enumerate() {
563            let i = i as u32 + 1;
564            editor.update_in(cx, |editor, window, cx| {
565                editor.move_right(&MoveRight, window, cx)
566            });
567            cx.executor().advance_clock(Duration::from_millis(200));
568            assert_eq!(
569                user_caret_position(1, i + 1),
570                current_position(&workspace, cx),
571                "Wrong position for char '{c}' in string '{text}'",
572            );
573        }
574
575        editor.update_in(cx, |editor, window, cx| {
576            editor.move_right(&MoveRight, window, cx)
577        });
578        cx.executor().advance_clock(Duration::from_millis(200));
579        assert_eq!(
580            user_caret_position(1, text.chars().count() as u32 + 1),
581            current_position(&workspace, cx),
582            "After reaching the end of the text, position should not change when moving right"
583        );
584    }
585
586    #[gpui::test]
587    async fn test_go_into_unicode(cx: &mut TestAppContext) {
588        init_test(cx);
589
590        let text = "ēlo你好";
591        let fs = FakeFs::new(cx.executor());
592        fs.insert_tree(
593            path!("/dir"),
594            json!({
595                "a.rs": text
596            }),
597        )
598        .await;
599
600        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
601        let (workspace, cx) =
602            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
603        workspace.update_in(cx, |workspace, window, cx| {
604            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
605            workspace.status_bar().update(cx, |status_bar, cx| {
606                status_bar.add_right_item(cursor_position, window, cx);
607            });
608        });
609
610        let worktree_id = workspace.update(cx, |workspace, cx| {
611            workspace.project().update(cx, |project, cx| {
612                project.worktrees(cx).next().unwrap().read(cx).id()
613            })
614        });
615        let _buffer = project
616            .update(cx, |project, cx| {
617                project.open_local_buffer(path!("/dir/a.rs"), cx)
618            })
619            .await
620            .unwrap();
621        let editor = workspace
622            .update_in(cx, |workspace, window, cx| {
623                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
624            })
625            .await
626            .unwrap()
627            .downcast::<Editor>()
628            .unwrap();
629
630        editor.update_in(cx, |editor, window, cx| {
631            editor.move_to_beginning(&MoveToBeginning, window, cx)
632        });
633        cx.executor().advance_clock(Duration::from_millis(200));
634        assert_eq!(user_caret_position(1, 1), current_position(&workspace, cx));
635
636        for (i, c) in text.chars().enumerate() {
637            let i = i as u32 + 1;
638            let point = user_caret_position(1, i + 1);
639            go_to_point(point, user_caret_position(1, i), &workspace, cx);
640            cx.executor().advance_clock(Duration::from_millis(200));
641            assert_eq!(
642                point,
643                current_position(&workspace, cx),
644                "When going to {point:?}, expecting the cursor to be at char '{c}' in string '{text}'",
645            );
646        }
647
648        go_to_point(
649            user_caret_position(111, 222),
650            user_caret_position(1, text.chars().count() as u32 + 1),
651            &workspace,
652            cx,
653        );
654        cx.executor().advance_clock(Duration::from_millis(200));
655        assert_eq!(
656            user_caret_position(1, text.chars().count() as u32 + 1),
657            current_position(&workspace, cx),
658            "When going into too large point, should go to the end of the text"
659        );
660    }
661
662    fn current_position(
663        workspace: &Entity<Workspace>,
664        cx: &mut VisualTestContext,
665    ) -> UserCaretPosition {
666        workspace.update(cx, |workspace, cx| {
667            workspace
668                .status_bar()
669                .read(cx)
670                .item_of_type::<CursorPosition>()
671                .expect("missing cursor position item")
672                .read(cx)
673                .position()
674                .expect("No position found")
675        })
676    }
677
678    fn user_caret_position(line: u32, character: u32) -> UserCaretPosition {
679        UserCaretPosition {
680            line: NonZeroU32::new(line).unwrap(),
681            character: NonZeroU32::new(character).unwrap(),
682        }
683    }
684
685    fn go_to_point(
686        new_point: UserCaretPosition,
687        expected_placeholder: UserCaretPosition,
688        workspace: &Entity<Workspace>,
689        cx: &mut VisualTestContext,
690    ) {
691        let go_to_line_view = open_go_to_line_view(workspace, cx);
692        go_to_line_view.update(cx, |go_to_line_view, cx| {
693            assert_eq!(
694                go_to_line_view
695                    .line_editor
696                    .read(cx)
697                    .placeholder_text()
698                    .expect("No placeholder text"),
699                format!(
700                    "{}:{}",
701                    expected_placeholder.line, expected_placeholder.character
702                )
703            );
704        });
705        cx.simulate_input(&format!("{}:{}", new_point.line, new_point.character));
706        cx.dispatch_action(menu::Confirm);
707    }
708
709    fn open_go_to_line_view(
710        workspace: &Entity<Workspace>,
711        cx: &mut VisualTestContext,
712    ) -> Entity<GoToLine> {
713        cx.dispatch_action(editor::actions::ToggleGoToLine);
714        workspace.update(cx, |workspace, cx| {
715            workspace.active_modal::<GoToLine>(cx).unwrap()
716        })
717    }
718
719    fn highlighted_display_rows(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
720        editor.update_in(cx, |editor, window, cx| {
721            editor
722                .highlighted_display_rows(window, cx)
723                .into_keys()
724                .map(|r| r.0)
725                .collect()
726        })
727    }
728
729    #[track_caller]
730    fn assert_single_caret_at_row(
731        editor: &Entity<Editor>,
732        buffer_row: u32,
733        cx: &mut VisualTestContext,
734    ) {
735        let selections = editor.update(cx, |editor, cx| {
736            editor
737                .selections
738                .all::<rope::Point>(cx)
739                .into_iter()
740                .map(|s| s.start..s.end)
741                .collect::<Vec<_>>()
742        });
743        assert!(
744            selections.len() == 1,
745            "Expected one caret selection but got: {selections:?}"
746        );
747        let selection = &selections[0];
748        assert!(
749            selection.start == selection.end,
750            "Expected a single caret selection, but got: {selection:?}"
751        );
752        assert_eq!(selection.start.row, buffer_row);
753    }
754
755    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
756        cx.update(|cx| {
757            let state = AppState::test(cx);
758            language::init(cx);
759            crate::init(cx);
760            editor::init(cx);
761            workspace::init_settings(cx);
762            Project::init_settings(cx);
763            state
764        })
765    }
766}