go_to_line.rs

  1pub mod cursor_position;
  2
  3use cursor_position::{LineIndicatorFormat, UserCaretPosition};
  4use editor::{
  5    Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, ToOffset, ToPoint, actions::Tab,
  6    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                                if editor.text(cx).is_empty() {
108                                    let placeholder_text = placeholder_text.to_string();
109                                    editor.set_text(placeholder_text, window, cx);
110                                }
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(Some(Autoscroll::center()), window, cx, |s| {
253                s.select_anchor_ranges([start..start])
254            });
255            editor.focus_handle(cx).focus(window);
256            cx.notify()
257        });
258        self.prev_scroll_position.take();
259
260        cx.emit(DismissEvent);
261    }
262}
263
264impl Render for GoToLine {
265    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
266        let help_text = match self.line_and_char_from_query(cx) {
267            Some((line, Some(character))) => {
268                format!("Go to line {line}, character {character}").into()
269            }
270            Some((line, None)) => format!("Go to line {line}").into(),
271            None => self.current_text.clone(),
272        };
273
274        v_flex()
275            .w(rems(24.))
276            .elevation_2(cx)
277            .key_context("GoToLine")
278            .on_action(cx.listener(Self::cancel))
279            .on_action(cx.listener(Self::confirm))
280            .child(
281                div()
282                    .border_b_1()
283                    .border_color(cx.theme().colors().border_variant)
284                    .px_2()
285                    .py_1()
286                    .child(self.line_editor.clone()),
287            )
288            .child(
289                h_flex()
290                    .px_2()
291                    .py_1()
292                    .gap_1()
293                    .child(Label::new(help_text).color(Color::Muted)),
294            )
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use cursor_position::{CursorPosition, SelectionStats, UserCaretPosition};
302    use editor::actions::{MoveRight, MoveToBeginning, SelectAll};
303    use gpui::{TestAppContext, VisualTestContext};
304    use indoc::indoc;
305    use project::{FakeFs, Project};
306    use serde_json::json;
307    use std::{num::NonZeroU32, sync::Arc, time::Duration};
308    use util::path;
309    use workspace::{AppState, Workspace};
310
311    #[gpui::test]
312    async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
313        init_test(cx);
314        let fs = FakeFs::new(cx.executor());
315        fs.insert_tree(
316            path!("/dir"),
317            json!({
318                "a.rs": indoc!{"
319                    struct SingleLine; // display line 0
320                                       // display line 1
321                    struct MultiLine { // display line 2
322                        field_1: i32,  // display line 3
323                        field_2: i32,  // display line 4
324                    }                  // display line 5
325                                       // display line 6
326                    struct Another {   // display line 7
327                        field_1: i32,  // display line 8
328                        field_2: i32,  // display line 9
329                        field_3: i32,  // display line 10
330                        field_4: i32,  // display line 11
331                    }                  // display line 12
332                "}
333            }),
334        )
335        .await;
336
337        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
338        let (workspace, cx) =
339            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
340        let worktree_id = workspace.update(cx, |workspace, cx| {
341            workspace.project().update(cx, |project, cx| {
342                project.worktrees(cx).next().unwrap().read(cx).id()
343            })
344        });
345        let _buffer = project
346            .update(cx, |project, cx| {
347                project.open_local_buffer(path!("/dir/a.rs"), cx)
348            })
349            .await
350            .unwrap();
351        let editor = workspace
352            .update_in(cx, |workspace, window, cx| {
353                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
354            })
355            .await
356            .unwrap()
357            .downcast::<Editor>()
358            .unwrap();
359
360        let go_to_line_view = open_go_to_line_view(&workspace, cx);
361        assert_eq!(
362            highlighted_display_rows(&editor, cx),
363            Vec::<u32>::new(),
364            "Initially opened go to line modal should not highlight any rows"
365        );
366        assert_single_caret_at_row(&editor, 0, cx);
367
368        cx.simulate_input("1");
369        assert_eq!(
370            highlighted_display_rows(&editor, cx),
371            vec![0],
372            "Go to line modal should highlight a row, corresponding to the query"
373        );
374        assert_single_caret_at_row(&editor, 0, cx);
375
376        cx.simulate_input("8");
377        assert_eq!(
378            highlighted_display_rows(&editor, cx),
379            vec![13],
380            "If the query is too large, the last row should be highlighted"
381        );
382        assert_single_caret_at_row(&editor, 0, cx);
383
384        cx.dispatch_action(menu::Cancel);
385        drop(go_to_line_view);
386        editor.update(cx, |_, _| {});
387        assert_eq!(
388            highlighted_display_rows(&editor, cx),
389            Vec::<u32>::new(),
390            "After cancelling and closing the modal, no rows should be highlighted"
391        );
392        assert_single_caret_at_row(&editor, 0, cx);
393
394        let go_to_line_view = open_go_to_line_view(&workspace, cx);
395        assert_eq!(
396            highlighted_display_rows(&editor, cx),
397            Vec::<u32>::new(),
398            "Reopened modal should not highlight any rows"
399        );
400        assert_single_caret_at_row(&editor, 0, cx);
401
402        let expected_highlighted_row = 4;
403        cx.simulate_input("5");
404        assert_eq!(
405            highlighted_display_rows(&editor, cx),
406            vec![expected_highlighted_row]
407        );
408        assert_single_caret_at_row(&editor, 0, cx);
409        cx.dispatch_action(menu::Confirm);
410        drop(go_to_line_view);
411        editor.update(cx, |_, _| {});
412        assert_eq!(
413            highlighted_display_rows(&editor, cx),
414            Vec::<u32>::new(),
415            "After confirming and closing the modal, no rows should be highlighted"
416        );
417        // On confirm, should place the caret on the highlighted row.
418        assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
419    }
420
421    #[gpui::test]
422    async fn test_unicode_characters_selection(cx: &mut TestAppContext) {
423        init_test(cx);
424
425        let fs = FakeFs::new(cx.executor());
426        fs.insert_tree(
427            path!("/dir"),
428            json!({
429                "a.rs": "ēlo"
430            }),
431        )
432        .await;
433
434        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
435        let (workspace, cx) =
436            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
437        workspace.update_in(cx, |workspace, window, cx| {
438            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
439            workspace.status_bar().update(cx, |status_bar, cx| {
440                status_bar.add_right_item(cursor_position, window, cx);
441            });
442        });
443
444        let worktree_id = workspace.update(cx, |workspace, cx| {
445            workspace.project().update(cx, |project, cx| {
446                project.worktrees(cx).next().unwrap().read(cx).id()
447            })
448        });
449        let _buffer = project
450            .update(cx, |project, cx| {
451                project.open_local_buffer(path!("/dir/a.rs"), cx)
452            })
453            .await
454            .unwrap();
455        let editor = workspace
456            .update_in(cx, |workspace, window, cx| {
457                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
458            })
459            .await
460            .unwrap()
461            .downcast::<Editor>()
462            .unwrap();
463
464        cx.executor().advance_clock(Duration::from_millis(200));
465        workspace.update(cx, |workspace, cx| {
466            assert_eq!(
467                &SelectionStats {
468                    lines: 0,
469                    characters: 0,
470                    selections: 1,
471                },
472                workspace
473                    .status_bar()
474                    .read(cx)
475                    .item_of_type::<CursorPosition>()
476                    .expect("missing cursor position item")
477                    .read(cx)
478                    .selection_stats(),
479                "No selections should be initially"
480            );
481        });
482        editor.update_in(cx, |editor, window, cx| {
483            editor.select_all(&SelectAll, window, cx)
484        });
485        cx.executor().advance_clock(Duration::from_millis(200));
486        workspace.update(cx, |workspace, cx| {
487            assert_eq!(
488                &SelectionStats {
489                    lines: 1,
490                    characters: 3,
491                    selections: 1,
492                },
493                workspace
494                    .status_bar()
495                    .read(cx)
496                    .item_of_type::<CursorPosition>()
497                    .expect("missing cursor position item")
498                    .read(cx)
499                    .selection_stats(),
500                "After selecting a text with multibyte unicode characters, the character count should be correct"
501            );
502        });
503    }
504
505    #[gpui::test]
506    async fn test_unicode_line_numbers(cx: &mut TestAppContext) {
507        init_test(cx);
508
509        let text = "ēlo你好";
510        let fs = FakeFs::new(cx.executor());
511        fs.insert_tree(
512            path!("/dir"),
513            json!({
514                "a.rs": text
515            }),
516        )
517        .await;
518
519        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
520        let (workspace, cx) =
521            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
522        workspace.update_in(cx, |workspace, window, cx| {
523            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
524            workspace.status_bar().update(cx, |status_bar, cx| {
525                status_bar.add_right_item(cursor_position, window, cx);
526            });
527        });
528
529        let worktree_id = workspace.update(cx, |workspace, cx| {
530            workspace.project().update(cx, |project, cx| {
531                project.worktrees(cx).next().unwrap().read(cx).id()
532            })
533        });
534        let _buffer = project
535            .update(cx, |project, cx| {
536                project.open_local_buffer(path!("/dir/a.rs"), cx)
537            })
538            .await
539            .unwrap();
540        let editor = workspace
541            .update_in(cx, |workspace, window, cx| {
542                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
543            })
544            .await
545            .unwrap()
546            .downcast::<Editor>()
547            .unwrap();
548
549        editor.update_in(cx, |editor, window, cx| {
550            editor.move_to_beginning(&MoveToBeginning, window, cx)
551        });
552        cx.executor().advance_clock(Duration::from_millis(200));
553        assert_eq!(
554            user_caret_position(1, 1),
555            current_position(&workspace, cx),
556            "Beginning of the line should be at first line, before any characters"
557        );
558
559        for (i, c) in text.chars().enumerate() {
560            let i = i as u32 + 1;
561            editor.update_in(cx, |editor, window, cx| {
562                editor.move_right(&MoveRight, window, cx)
563            });
564            cx.executor().advance_clock(Duration::from_millis(200));
565            assert_eq!(
566                user_caret_position(1, i + 1),
567                current_position(&workspace, cx),
568                "Wrong position for char '{c}' in string '{text}'",
569            );
570        }
571
572        editor.update_in(cx, |editor, window, cx| {
573            editor.move_right(&MoveRight, window, cx)
574        });
575        cx.executor().advance_clock(Duration::from_millis(200));
576        assert_eq!(
577            user_caret_position(1, text.chars().count() as u32 + 1),
578            current_position(&workspace, cx),
579            "After reaching the end of the text, position should not change when moving right"
580        );
581    }
582
583    #[gpui::test]
584    async fn test_go_into_unicode(cx: &mut TestAppContext) {
585        init_test(cx);
586
587        let text = "ēlo你好";
588        let fs = FakeFs::new(cx.executor());
589        fs.insert_tree(
590            path!("/dir"),
591            json!({
592                "a.rs": text
593            }),
594        )
595        .await;
596
597        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
598        let (workspace, cx) =
599            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
600        workspace.update_in(cx, |workspace, window, cx| {
601            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
602            workspace.status_bar().update(cx, |status_bar, cx| {
603                status_bar.add_right_item(cursor_position, window, cx);
604            });
605        });
606
607        let worktree_id = workspace.update(cx, |workspace, cx| {
608            workspace.project().update(cx, |project, cx| {
609                project.worktrees(cx).next().unwrap().read(cx).id()
610            })
611        });
612        let _buffer = project
613            .update(cx, |project, cx| {
614                project.open_local_buffer(path!("/dir/a.rs"), cx)
615            })
616            .await
617            .unwrap();
618        let editor = workspace
619            .update_in(cx, |workspace, window, cx| {
620                workspace.open_path((worktree_id, "a.rs"), None, true, window, cx)
621            })
622            .await
623            .unwrap()
624            .downcast::<Editor>()
625            .unwrap();
626
627        editor.update_in(cx, |editor, window, cx| {
628            editor.move_to_beginning(&MoveToBeginning, window, cx)
629        });
630        cx.executor().advance_clock(Duration::from_millis(200));
631        assert_eq!(user_caret_position(1, 1), current_position(&workspace, cx));
632
633        for (i, c) in text.chars().enumerate() {
634            let i = i as u32 + 1;
635            let point = user_caret_position(1, i + 1);
636            go_to_point(point, user_caret_position(1, i), &workspace, cx);
637            cx.executor().advance_clock(Duration::from_millis(200));
638            assert_eq!(
639                point,
640                current_position(&workspace, cx),
641                "When going to {point:?}, expecting the cursor to be at char '{c}' in string '{text}'",
642            );
643        }
644
645        go_to_point(
646            user_caret_position(111, 222),
647            user_caret_position(1, text.chars().count() as u32 + 1),
648            &workspace,
649            cx,
650        );
651        cx.executor().advance_clock(Duration::from_millis(200));
652        assert_eq!(
653            user_caret_position(1, text.chars().count() as u32 + 1),
654            current_position(&workspace, cx),
655            "When going into too large point, should go to the end of the text"
656        );
657    }
658
659    fn current_position(
660        workspace: &Entity<Workspace>,
661        cx: &mut VisualTestContext,
662    ) -> UserCaretPosition {
663        workspace.update(cx, |workspace, cx| {
664            workspace
665                .status_bar()
666                .read(cx)
667                .item_of_type::<CursorPosition>()
668                .expect("missing cursor position item")
669                .read(cx)
670                .position()
671                .expect("No position found")
672        })
673    }
674
675    fn user_caret_position(line: u32, character: u32) -> UserCaretPosition {
676        UserCaretPosition {
677            line: NonZeroU32::new(line).unwrap(),
678            character: NonZeroU32::new(character).unwrap(),
679        }
680    }
681
682    fn go_to_point(
683        new_point: UserCaretPosition,
684        expected_placeholder: UserCaretPosition,
685        workspace: &Entity<Workspace>,
686        cx: &mut VisualTestContext,
687    ) {
688        let go_to_line_view = open_go_to_line_view(workspace, cx);
689        go_to_line_view.update(cx, |go_to_line_view, cx| {
690            assert_eq!(
691                go_to_line_view
692                    .line_editor
693                    .read(cx)
694                    .placeholder_text()
695                    .expect("No placeholder text"),
696                format!(
697                    "{}:{}",
698                    expected_placeholder.line, expected_placeholder.character
699                )
700            );
701        });
702        cx.simulate_input(&format!("{}:{}", new_point.line, new_point.character));
703        cx.dispatch_action(menu::Confirm);
704    }
705
706    fn open_go_to_line_view(
707        workspace: &Entity<Workspace>,
708        cx: &mut VisualTestContext,
709    ) -> Entity<GoToLine> {
710        cx.dispatch_action(editor::actions::ToggleGoToLine);
711        workspace.update(cx, |workspace, cx| {
712            workspace.active_modal::<GoToLine>(cx).unwrap().clone()
713        })
714    }
715
716    fn highlighted_display_rows(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
717        editor.update_in(cx, |editor, window, cx| {
718            editor
719                .highlighted_display_rows(window, cx)
720                .into_keys()
721                .map(|r| r.0)
722                .collect()
723        })
724    }
725
726    #[track_caller]
727    fn assert_single_caret_at_row(
728        editor: &Entity<Editor>,
729        buffer_row: u32,
730        cx: &mut VisualTestContext,
731    ) {
732        let selections = editor.update(cx, |editor, cx| {
733            editor
734                .selections
735                .all::<rope::Point>(cx)
736                .into_iter()
737                .map(|s| s.start..s.end)
738                .collect::<Vec<_>>()
739        });
740        assert!(
741            selections.len() == 1,
742            "Expected one caret selection but got: {selections:?}"
743        );
744        let selection = &selections[0];
745        assert!(
746            selection.start == selection.end,
747            "Expected a single caret selection, but got: {selection:?}"
748        );
749        assert_eq!(selection.start.row, buffer_row);
750    }
751
752    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
753        cx.update(|cx| {
754            let state = AppState::test(cx);
755            language::init(cx);
756            crate::init(cx);
757            editor::init(cx);
758            workspace::init_settings(cx);
759            Project::init_settings(cx);
760            state
761        })
762    }
763}