Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:21:56.386Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

go_to_line.rs

1065 lines · 38.6 KB · rust
1pub mod cursor_position;
2
3use cursor_position::UserCaretPosition;
4use editor::{
5    Anchor, Editor, RowHighlightOptions, SelectionEffects, ToPoint,
6    actions::Tab,
7    scroll::{Autoscroll, ScrollOffset},
8};
9use gpui::{
10    App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
11    Subscription, div, prelude::*,
12};
13use language::Buffer;
14use text::{Bias, Point};
15use theme::ActiveTheme;
16use ui::prelude::*;
17use util::paths::FILE_ROW_COLUMN_DELIMITER;
18use workspace::{DismissDecision, ModalView};
19
20pub fn init(cx: &mut App) {
21    cx.observe_new(GoToLine::register).detach();
22}
23
24pub struct GoToLine {
25    line_editor: Entity<Editor>,
26    active_editor: Entity<Editor>,
27    active_buffer: Entity<Buffer>,
28    current_text: SharedString,
29    prev_scroll_position: Option<gpui::Point<ScrollOffset>>,
30    current_line: u32,
31    _subscriptions: Vec<Subscription>,
32}
33
34impl ModalView for GoToLine {
35    fn on_before_dismiss(
36        &mut self,
37        _window: &mut Window,
38        _cx: &mut Context<Self>,
39    ) -> DismissDecision {
40        self.prev_scroll_position.take();
41        DismissDecision::Dismiss(true)
42    }
43}
44
45impl Focusable for GoToLine {
46    fn focus_handle(&self, cx: &App) -> FocusHandle {
47        self.line_editor.focus_handle(cx)
48    }
49}
50impl EventEmitter<DismissEvent> for GoToLine {}
51
52enum GoToLineRowHighlights {}
53
54impl GoToLine {
55    fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<Editor>) {
56        let handle = cx.entity().downgrade();
57        editor
58            .register_action(move |_: &editor::actions::ToggleGoToLine, window, cx| {
59                let Some(editor_handle) = handle.upgrade() else {
60                    return;
61                };
62                let Some(workspace) = editor_handle.read(cx).workspace() else {
63                    return;
64                };
65                let editor = editor_handle.read(cx);
66                let Some(buffer) = editor.active_buffer(cx) else {
67                    return;
68                };
69                workspace.update(cx, |workspace, cx| {
70                    workspace.toggle_modal(window, cx, move |window, cx| {
71                        GoToLine::new(editor_handle, buffer, window, cx)
72                    });
73                })
74            })
75            .detach();
76    }
77
78    pub fn new(
79        active_editor: Entity<Editor>,
80        active_buffer: Entity<Buffer>,
81        window: &mut Window,
82        cx: &mut Context<Self>,
83    ) -> Self {
84        let (user_caret, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
85            let user_caret = UserCaretPosition::at_selection_end(
86                &editor
87                    .selections
88                    .last::<Point>(&editor.display_snapshot(cx)),
89                &editor.buffer().read(cx).snapshot(cx),
90            );
91
92            let snapshot = active_buffer.read(cx).snapshot();
93            let last_line = editor
94                .buffer()
95                .read(cx)
96                .snapshot(cx)
97                .excerpts_for_buffer(snapshot.remote_id())
98                .map(move |range| text::ToPoint::to_point(&range.context.end, &snapshot).row)
99                .max()
100                .unwrap_or(0);
101
102            (user_caret, last_line, editor.scroll_position(cx))
103        });
104
105        let line = user_caret.line.get();
106        let column = user_caret.character.get();
107
108        let line_editor = cx.new(|cx| {
109            let mut editor = Editor::single_line(window, cx);
110            let editor_handle = cx.entity().downgrade();
111            editor
112                .register_action::<Tab>({
113                    move |_, window, cx| {
114                        let Some(editor) = editor_handle.upgrade() else {
115                            return;
116                        };
117                        editor.update(cx, |editor, cx| {
118                            if let Some(placeholder_text) = editor.placeholder_text(cx)
119                                && editor.text(cx).is_empty()
120                            {
121                                editor.set_text(placeholder_text, window, cx);
122                            }
123                        });
124                    }
125                })
126                .detach();
127            editor.set_placeholder_text(
128                &format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"),
129                window,
130                cx,
131            );
132            editor
133        });
134        let line_editor_change = cx.subscribe_in(&line_editor, window, Self::on_line_editor_event);
135
136        let current_text = format!(
137            "Current Line: {} of {} (column {})",
138            line,
139            last_line + 1,
140            column
141        );
142
143        Self {
144            line_editor,
145            active_editor,
146            active_buffer,
147            current_text: current_text.into(),
148            prev_scroll_position: Some(scroll_position),
149            current_line: line,
150            _subscriptions: vec![line_editor_change, cx.on_release_in(window, Self::release)],
151        }
152    }
153
154    fn release(&mut self, window: &mut Window, cx: &mut App) {
155        let scroll_position = self.prev_scroll_position.take();
156        self.active_editor.update(cx, |editor, cx| {
157            editor.clear_row_highlights::<GoToLineRowHighlights>();
158            if let Some(scroll_position) = scroll_position {
159                editor.set_scroll_position(scroll_position, window, cx);
160            }
161            cx.notify();
162        })
163    }
164
165    fn on_line_editor_event(
166        &mut self,
167        _: &Entity<Editor>,
168        event: &editor::EditorEvent,
169        _window: &mut Window,
170        cx: &mut Context<Self>,
171    ) {
172        match event {
173            editor::EditorEvent::Blurred => {
174                self.prev_scroll_position.take();
175                cx.emit(DismissEvent)
176            }
177            editor::EditorEvent::BufferEdited => self.highlight_current_line(cx),
178            _ => {}
179        }
180    }
181
182    fn highlight_current_line(&mut self, cx: &mut Context<Self>) {
183        self.active_editor.update(cx, |editor, cx| {
184            editor.clear_row_highlights::<GoToLineRowHighlights>();
185            let snapshot = editor.buffer().read(cx).snapshot(cx);
186            let Some(start) = self.anchor_from_query(editor, cx) else {
187                return;
188            };
189            let mut start_point = start.to_point(&snapshot);
190            start_point.column = 0;
191            // Force non-empty range to ensure the line is highlighted.
192            let mut end_point = snapshot.clip_point(start_point + Point::new(0, 1), Bias::Left);
193            if start_point == end_point {
194                end_point = snapshot.clip_point(start_point + Point::new(1, 0), Bias::Left);
195            }
196
197            let end = snapshot.anchor_after(end_point);
198            editor.highlight_rows::<GoToLineRowHighlights>(
199                start..end,
200                |cx| cx.theme().colors().editor_highlighted_line_background,
201                RowHighlightOptions {
202                    autoscroll: true,
203                    ..Default::default()
204                },
205                cx,
206            );
207            editor.request_autoscroll(Autoscroll::center(), cx);
208        });
209        cx.notify();
210    }
211
212    fn anchor_from_query(&self, editor: &Editor, cx: &Context<Editor>) -> Option<Anchor> {
213        let (query_row, query_char) = if let Some(offset) = self.relative_line_from_query(cx) {
214            let target = if offset >= 0 {
215                self.current_line.saturating_add(offset as u32)
216            } else {
217                self.current_line.saturating_sub(offset.unsigned_abs())
218            };
219            (target, None)
220        } else {
221            self.line_and_char_from_query(cx)?
222        };
223
224        let row = query_row.saturating_sub(1);
225        let character = query_char.unwrap_or(0).saturating_sub(1);
226        let target_point = {
227            let buffer_snapshot = self.active_buffer.read(cx).snapshot();
228            let row = row.min(buffer_snapshot.max_point().row);
229            buffer_snapshot.point_from_external_input(row, character)
230        };
231
232        editor
233            .buffer()
234            .read(cx)
235            .buffer_point_to_anchor(&self.active_buffer, target_point, cx)
236    }
237
238    fn relative_line_from_query(&self, cx: &App) -> Option<i32> {
239        let input = self.line_editor.read(cx).text(cx);
240        let trimmed = input.trim();
241
242        let mut last_direction_char: Option<char> = None;
243        let mut number_start_index = 0;
244
245        for (i, c) in trimmed.char_indices() {
246            match c {
247                '+' | 'f' | 'F' | '-' | 'b' | 'B' => {
248                    last_direction_char = Some(c);
249                    number_start_index = i + c.len_utf8();
250                }
251                _ => break,
252            }
253        }
254
255        let direction = last_direction_char?;
256
257        let number_part = &trimmed[number_start_index..];
258        let line_part = number_part
259            .split(FILE_ROW_COLUMN_DELIMITER)
260            .next()
261            .unwrap_or(number_part)
262            .trim();
263
264        let value = line_part.parse::<u32>().ok()?;
265
266        match direction {
267            '+' | 'f' | 'F' => Some(value as i32),
268            '-' | 'b' | 'B' => Some(-(value as i32)),
269            _ => None,
270        }
271    }
272
273    fn line_and_char_from_query(&self, cx: &App) -> Option<(u32, Option<u32>)> {
274        let input = self.line_editor.read(cx).text(cx);
275        let mut components = input
276            .splitn(2, FILE_ROW_COLUMN_DELIMITER)
277            .map(str::trim)
278            .fuse();
279        let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
280        let column = components.next().and_then(|col| col.parse::<u32>().ok());
281        Some((row, column))
282    }
283
284    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
285        cx.emit(DismissEvent);
286    }
287
288    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
289        self.active_editor.update(cx, |editor, cx| {
290            let Some(start) = self.anchor_from_query(editor, cx) else {
291                return;
292            };
293            editor.change_selections(
294                SelectionEffects::scroll(Autoscroll::center()),
295                window,
296                cx,
297                |s| s.select_anchor_ranges([start..start]),
298            );
299            editor.focus_handle(cx).focus(window, cx);
300            cx.notify()
301        });
302        self.prev_scroll_position.take();
303
304        cx.emit(DismissEvent);
305    }
306}
307
308impl Render for GoToLine {
309    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
310        let help_text = if let Some(offset) = self.relative_line_from_query(cx) {
311            let target_line = if offset >= 0 {
312                self.current_line.saturating_add(offset as u32)
313            } else {
314                self.current_line.saturating_sub(offset.unsigned_abs())
315            };
316            format!("Go to line {target_line} ({offset:+} from current)").into()
317        } else {
318            match self.line_and_char_from_query(cx) {
319                Some((line, Some(character))) => {
320                    format!("Go to line {line}, character {character}").into()
321                }
322                Some((line, None)) => format!("Go to line {line}").into(),
323                None => self.current_text.clone(),
324            }
325        };
326
327        v_flex()
328            .w(rems(24.))
329            .elevation_2(cx)
330            .key_context("GoToLine")
331            .on_action(cx.listener(Self::cancel))
332            .on_action(cx.listener(Self::confirm))
333            .child(
334                div()
335                    .border_b_1()
336                    .border_color(cx.theme().colors().border_variant)
337                    .px_2()
338                    .py_1()
339                    .child(self.line_editor.clone()),
340            )
341            .child(
342                h_flex()
343                    .px_2()
344                    .py_1()
345                    .gap_1()
346                    .child(Label::new(help_text).color(Color::Muted)),
347            )
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use cursor_position::{CursorPosition, SelectionStats, UserCaretPosition};
355    use editor::actions::{MoveRight, MoveToBeginning, SelectAll};
356    use gpui::{TestAppContext, VisualTestContext};
357    use indoc::indoc;
358    use language::Capability;
359    use multi_buffer::{MultiBuffer, PathKey};
360    use project::{FakeFs, Project};
361    use serde_json::json;
362    use std::{num::NonZeroU32, sync::Arc, time::Duration};
363    use util::{path, rel_path::rel_path};
364    use workspace::{AppState, MultiWorkspace, Workspace};
365
366    #[gpui::test]
367    async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
368        init_test(cx);
369        let fs = FakeFs::new(cx.executor());
370        fs.insert_tree(
371            path!("/dir"),
372            json!({
373                "a.rs": indoc!{"
374                    struct SingleLine; // display line 0
375                                       // display line 1
376                    struct MultiLine { // display line 2
377                        field_1: i32,  // display line 3
378                        field_2: i32,  // display line 4
379                    }                  // display line 5
380                                       // display line 6
381                    struct Another {   // display line 7
382                        field_1: i32,  // display line 8
383                        field_2: i32,  // display line 9
384                        field_3: i32,  // display line 10
385                        field_4: i32,  // display line 11
386                    }                  // display line 12
387                "}
388            }),
389        )
390        .await;
391
392        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
393        let (multi_workspace, cx) =
394            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
395        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
396        let worktree_id = workspace.update(cx, |workspace, cx| {
397            workspace.project().update(cx, |project, cx| {
398                project.worktrees(cx).next().unwrap().read(cx).id()
399            })
400        });
401        let _buffer = project
402            .update(cx, |project, cx| {
403                project.open_local_buffer(path!("/dir/a.rs"), cx)
404            })
405            .await
406            .unwrap();
407        let editor = workspace
408            .update_in(cx, |workspace, window, cx| {
409                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
410            })
411            .await
412            .unwrap()
413            .downcast::<Editor>()
414            .unwrap();
415
416        let go_to_line_view = open_go_to_line_view(&workspace, cx);
417        assert_eq!(
418            highlighted_display_rows(&editor, cx),
419            Vec::<u32>::new(),
420            "Initially opened go to line modal should not highlight any rows"
421        );
422        assert_single_caret_at_row(&editor, 0, cx);
423
424        cx.simulate_input("1");
425        assert_eq!(
426            highlighted_display_rows(&editor, cx),
427            vec![0],
428            "Go to line modal should highlight a row, corresponding to the query"
429        );
430        assert_single_caret_at_row(&editor, 0, cx);
431
432        cx.simulate_input("8");
433        assert_eq!(
434            highlighted_display_rows(&editor, cx),
435            vec![13],
436            "If the query is too large, the last row should be highlighted"
437        );
438        assert_single_caret_at_row(&editor, 0, cx);
439
440        cx.dispatch_action(menu::Cancel);
441        drop(go_to_line_view);
442        editor.update(cx, |_, _| {});
443        assert_eq!(
444            highlighted_display_rows(&editor, cx),
445            Vec::<u32>::new(),
446            "After cancelling and closing the modal, no rows should be highlighted"
447        );
448        assert_single_caret_at_row(&editor, 0, cx);
449
450        let go_to_line_view = open_go_to_line_view(&workspace, cx);
451        assert_eq!(
452            highlighted_display_rows(&editor, cx),
453            Vec::<u32>::new(),
454            "Reopened modal should not highlight any rows"
455        );
456        assert_single_caret_at_row(&editor, 0, cx);
457
458        let expected_highlighted_row = 4;
459        cx.simulate_input("5");
460        assert_eq!(
461            highlighted_display_rows(&editor, cx),
462            vec![expected_highlighted_row]
463        );
464        assert_single_caret_at_row(&editor, 0, cx);
465        cx.dispatch_action(menu::Confirm);
466        drop(go_to_line_view);
467        editor.update(cx, |_, _| {});
468        assert_eq!(
469            highlighted_display_rows(&editor, cx),
470            Vec::<u32>::new(),
471            "After confirming and closing the modal, no rows should be highlighted"
472        );
473        // On confirm, should place the caret on the highlighted row.
474        assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
475    }
476
477    #[gpui::test]
478    async fn test_go_to_line_uses_buffer_rows_in_multibuffers(cx: &mut TestAppContext) {
479        init_test(cx);
480        let cx = cx.add_empty_window();
481        let file_content = (1..=60)
482            .map(|line| format!("line {line}"))
483            .collect::<Vec<_>>()
484            .join("\n");
485        let buffer = cx.new(|cx| Buffer::local(file_content, cx));
486        let multibuffer = cx.new(|cx| {
487            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
488            multibuffer.set_excerpts_for_path(
489                PathKey::for_buffer(&buffer, cx),
490                buffer.clone(),
491                [
492                    Point::new(10, 0)..Point::new(13, 0),
493                    Point::new(50, 0)..Point::new(53, 0),
494                ],
495                0,
496                cx,
497            );
498            multibuffer
499        });
500        let editor = cx.new_window_entity(|window, cx| {
501            Editor::for_multibuffer(multibuffer.clone(), None, window, cx)
502        });
503        let go_to_line_view = cx.new_window_entity(|window, cx| {
504            GoToLine::new(editor.clone(), buffer.clone(), window, cx)
505        });
506
507        go_to_line_view.update_in(cx, |go_to_line_view, window, cx| {
508            go_to_line_view.line_editor.update(cx, |line_editor, cx| {
509                line_editor.set_text("52", window, cx);
510            });
511            go_to_line_view.confirm(&menu::Confirm, window, cx);
512        });
513        assert_single_caret_at_buffer_row(&editor, 51, cx);
514
515        go_to_line_view.update_in(cx, |go_to_line_view, window, cx| {
516            go_to_line_view.line_editor.update(cx, |line_editor, cx| {
517                line_editor.set_text("30", window, cx);
518            });
519            go_to_line_view.confirm(&menu::Confirm, window, cx);
520        });
521        assert_single_caret_at_buffer_row(&editor, 50, cx);
522    }
523
524    #[gpui::test]
525    async fn test_unicode_characters_selection(cx: &mut TestAppContext) {
526        init_test(cx);
527
528        let fs = FakeFs::new(cx.executor());
529        fs.insert_tree(
530            path!("/dir"),
531            json!({
532                "a.rs": "ēlo"
533            }),
534        )
535        .await;
536
537        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
538        let (multi_workspace, cx) =
539            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
540        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
541        workspace.update_in(cx, |workspace, window, cx| {
542            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
543            workspace.status_bar().update(cx, |status_bar, cx| {
544                status_bar.add_right_item(cursor_position, window, cx);
545            });
546        });
547
548        let worktree_id = workspace.update(cx, |workspace, cx| {
549            workspace.project().update(cx, |project, cx| {
550                project.worktrees(cx).next().unwrap().read(cx).id()
551            })
552        });
553        let _buffer = project
554            .update(cx, |project, cx| {
555                project.open_local_buffer(path!("/dir/a.rs"), cx)
556            })
557            .await
558            .unwrap();
559        let editor = workspace
560            .update_in(cx, |workspace, window, cx| {
561                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
562            })
563            .await
564            .unwrap()
565            .downcast::<Editor>()
566            .unwrap();
567
568        cx.executor().advance_clock(Duration::from_millis(200));
569        workspace.update(cx, |workspace, cx| {
570            assert_eq!(
571                &SelectionStats {
572                    lines: 0,
573                    characters: 0,
574                    selections: 1,
575                },
576                workspace
577                    .status_bar()
578                    .read(cx)
579                    .item_of_type::<CursorPosition>()
580                    .expect("missing cursor position item")
581                    .read(cx)
582                    .selection_stats(),
583                "No selections should be initially"
584            );
585        });
586        editor.update_in(cx, |editor, window, cx| {
587            editor.select_all(&SelectAll, window, cx)
588        });
589        cx.executor().advance_clock(Duration::from_millis(200));
590        workspace.update(cx, |workspace, cx| {
591            assert_eq!(
592                &SelectionStats {
593                    lines: 1,
594                    characters: 3,
595                    selections: 1,
596                },
597                workspace
598                    .status_bar()
599                    .read(cx)
600                    .item_of_type::<CursorPosition>()
601                    .expect("missing cursor position item")
602                    .read(cx)
603                    .selection_stats(),
604                "After selecting a text with multibyte unicode characters, the character count should be correct"
605            );
606        });
607    }
608
609    #[gpui::test]
610    async fn test_unicode_line_numbers(cx: &mut TestAppContext) {
611        init_test(cx);
612
613        let text = "ēlo你好";
614        let fs = FakeFs::new(cx.executor());
615        fs.insert_tree(
616            path!("/dir"),
617            json!({
618                "a.rs": text
619            }),
620        )
621        .await;
622
623        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
624        let (multi_workspace, cx) =
625            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
626        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
627        workspace.update_in(cx, |workspace, window, cx| {
628            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
629            workspace.status_bar().update(cx, |status_bar, cx| {
630                status_bar.add_right_item(cursor_position, window, cx);
631            });
632        });
633
634        let worktree_id = workspace.update(cx, |workspace, cx| {
635            workspace.project().update(cx, |project, cx| {
636                project.worktrees(cx).next().unwrap().read(cx).id()
637            })
638        });
639        let _buffer = project
640            .update(cx, |project, cx| {
641                project.open_local_buffer(path!("/dir/a.rs"), cx)
642            })
643            .await
644            .unwrap();
645        let editor = workspace
646            .update_in(cx, |workspace, window, cx| {
647                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
648            })
649            .await
650            .unwrap()
651            .downcast::<Editor>()
652            .unwrap();
653
654        editor.update_in(cx, |editor, window, cx| {
655            editor.move_to_beginning(&MoveToBeginning, window, cx)
656        });
657        cx.executor().advance_clock(Duration::from_millis(200));
658        assert_eq!(
659            user_caret_position(1, 1),
660            current_position(&workspace, cx),
661            "Beginning of the line should be at first line, before any characters"
662        );
663
664        for (i, c) in text.chars().enumerate() {
665            let i = i as u32 + 1;
666            editor.update_in(cx, |editor, window, cx| {
667                editor.move_right(&MoveRight, window, cx)
668            });
669            cx.executor().advance_clock(Duration::from_millis(200));
670            assert_eq!(
671                user_caret_position(1, i + 1),
672                current_position(&workspace, cx),
673                "Wrong position for char '{c}' in string '{text}'",
674            );
675        }
676
677        editor.update_in(cx, |editor, window, cx| {
678            editor.move_right(&MoveRight, window, cx)
679        });
680        cx.executor().advance_clock(Duration::from_millis(200));
681        assert_eq!(
682            user_caret_position(1, text.chars().count() as u32 + 1),
683            current_position(&workspace, cx),
684            "After reaching the end of the text, position should not change when moving right"
685        );
686    }
687
688    #[gpui::test]
689    async fn test_go_into_unicode(cx: &mut TestAppContext) {
690        init_test(cx);
691
692        let text = "ēlo你好";
693        let fs = FakeFs::new(cx.executor());
694        fs.insert_tree(
695            path!("/dir"),
696            json!({
697                "a.rs": text
698            }),
699        )
700        .await;
701
702        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
703        let (multi_workspace, cx) =
704            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
705        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
706        workspace.update_in(cx, |workspace, window, cx| {
707            let cursor_position = cx.new(|_| CursorPosition::new(workspace));
708            workspace.status_bar().update(cx, |status_bar, cx| {
709                status_bar.add_right_item(cursor_position, window, cx);
710            });
711        });
712
713        let worktree_id = workspace.update(cx, |workspace, cx| {
714            workspace.project().update(cx, |project, cx| {
715                project.worktrees(cx).next().unwrap().read(cx).id()
716            })
717        });
718        let _buffer = project
719            .update(cx, |project, cx| {
720                project.open_local_buffer(path!("/dir/a.rs"), cx)
721            })
722            .await
723            .unwrap();
724        let editor = workspace
725            .update_in(cx, |workspace, window, cx| {
726                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
727            })
728            .await
729            .unwrap()
730            .downcast::<Editor>()
731            .unwrap();
732
733        editor.update_in(cx, |editor, window, cx| {
734            editor.move_to_beginning(&MoveToBeginning, window, cx)
735        });
736        cx.executor().advance_clock(Duration::from_millis(200));
737        assert_eq!(user_caret_position(1, 1), current_position(&workspace, cx));
738
739        for (i, c) in text.chars().enumerate() {
740            let i = i as u32 + 1;
741            let point = user_caret_position(1, i + 1);
742            go_to_point(point, user_caret_position(1, i), &workspace, cx);
743            cx.executor().advance_clock(Duration::from_millis(200));
744            assert_eq!(
745                point,
746                current_position(&workspace, cx),
747                "When going to {point:?}, expecting the cursor to be at char '{c}' in string '{text}'",
748            );
749        }
750
751        go_to_point(
752            user_caret_position(111, 222),
753            user_caret_position(1, text.chars().count() as u32 + 1),
754            &workspace,
755            cx,
756        );
757        cx.executor().advance_clock(Duration::from_millis(200));
758        assert_eq!(
759            user_caret_position(1, text.chars().count() as u32 + 1),
760            current_position(&workspace, cx),
761            "When going into too large point, should go to the end of the text"
762        );
763    }
764
765    fn current_position(
766        workspace: &Entity<Workspace>,
767        cx: &mut VisualTestContext,
768    ) -> UserCaretPosition {
769        workspace.update(cx, |workspace, cx| {
770            workspace
771                .status_bar()
772                .read(cx)
773                .item_of_type::<CursorPosition>()
774                .expect("missing cursor position item")
775                .read(cx)
776                .position()
777                .expect("No position found")
778        })
779    }
780
781    fn user_caret_position(line: u32, character: u32) -> UserCaretPosition {
782        UserCaretPosition {
783            line: NonZeroU32::new(line).unwrap(),
784            character: NonZeroU32::new(character).unwrap(),
785        }
786    }
787
788    fn go_to_point(
789        new_point: UserCaretPosition,
790        expected_placeholder: UserCaretPosition,
791        workspace: &Entity<Workspace>,
792        cx: &mut VisualTestContext,
793    ) {
794        let go_to_line_view = open_go_to_line_view(workspace, cx);
795        go_to_line_view.update(cx, |go_to_line_view, cx| {
796            assert_eq!(
797                go_to_line_view.line_editor.update(cx, |line_editor, cx| {
798                    line_editor
799                        .placeholder_text(cx)
800                        .expect("No placeholder text")
801                }),
802                format!(
803                    "{}:{}",
804                    expected_placeholder.line, expected_placeholder.character
805                )
806            );
807        });
808        cx.simulate_input(&format!("{}:{}", new_point.line, new_point.character));
809        cx.dispatch_action(menu::Confirm);
810    }
811
812    fn open_go_to_line_view(
813        workspace: &Entity<Workspace>,
814        cx: &mut VisualTestContext,
815    ) -> Entity<GoToLine> {
816        cx.dispatch_action(editor::actions::ToggleGoToLine);
817        workspace.update(cx, |workspace, cx| {
818            workspace.active_modal::<GoToLine>(cx).unwrap()
819        })
820    }
821
822    fn highlighted_display_rows(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
823        editor.update_in(cx, |editor, window, cx| {
824            editor
825                .highlighted_display_rows(window, cx)
826                .into_keys()
827                .map(|r| r.0)
828                .collect()
829        })
830    }
831
832    #[track_caller]
833    fn assert_single_caret_at_row(
834        editor: &Entity<Editor>,
835        buffer_row: u32,
836        cx: &mut VisualTestContext,
837    ) {
838        let selection = single_caret_selection(editor, cx);
839        assert_eq!(selection.start.row, buffer_row);
840    }
841
842    #[track_caller]
843    fn assert_single_caret_at_buffer_row(
844        editor: &Entity<Editor>,
845        buffer_row: u32,
846        cx: &mut VisualTestContext,
847    ) {
848        let selection = single_caret_selection(editor, cx);
849        let buffer_point = editor.update(cx, |editor, cx| {
850            let snapshot = editor.buffer().read(cx).snapshot(cx);
851            snapshot
852                .point_to_buffer_point(selection.start)
853                .map(|(_, buffer_point)| buffer_point)
854        });
855
856        assert_eq!(buffer_point.map(|point| point.row), Some(buffer_row));
857    }
858
859    fn single_caret_selection(
860        editor: &Entity<Editor>,
861        cx: &mut VisualTestContext,
862    ) -> std::ops::Range<rope::Point> {
863        let selections = editor.update(cx, |editor, cx| {
864            editor
865                .selections
866                .all::<rope::Point>(&editor.display_snapshot(cx))
867                .into_iter()
868                .map(|s| s.start..s.end)
869                .collect::<Vec<_>>()
870        });
871        assert!(
872            selections.len() == 1,
873            "Expected one caret selection but got: {selections:?}"
874        );
875        let selection = selections
876            .into_iter()
877            .next()
878            .expect("checked selection count");
879        assert!(
880            selection.start == selection.end,
881            "Expected a single caret selection, but got: {selection:?}"
882        );
883        selection
884    }
885
886    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
887        cx.update(|cx| {
888            let state = AppState::test(cx);
889            crate::init(cx);
890            editor::init(cx);
891            state
892        })
893    }
894
895    #[gpui::test]
896    async fn test_scroll_position_on_outside_click(cx: &mut TestAppContext) {
897        init_test(cx);
898
899        let fs = FakeFs::new(cx.executor());
900        let file_content = (0..100)
901            .map(|i| format!("struct Line{};", i))
902            .collect::<Vec<_>>()
903            .join("\n");
904        fs.insert_tree(path!("/dir"), json!({"a.rs": file_content}))
905            .await;
906
907        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
908        let (multi_workspace, cx) =
909            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
910        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
911        let worktree_id = workspace.update(cx, |workspace, cx| {
912            workspace.project().update(cx, |project, cx| {
913                project.worktrees(cx).next().unwrap().read(cx).id()
914            })
915        });
916        let _buffer = project
917            .update(cx, |project, cx| {
918                project.open_local_buffer(path!("/dir/a.rs"), cx)
919            })
920            .await
921            .unwrap();
922        let editor = workspace
923            .update_in(cx, |workspace, window, cx| {
924                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
925            })
926            .await
927            .unwrap()
928            .downcast::<Editor>()
929            .unwrap();
930        let go_to_line_view = open_go_to_line_view(&workspace, cx);
931
932        let scroll_position_before_input =
933            editor.update(cx, |editor, cx| editor.scroll_position(cx));
934        cx.simulate_input("47");
935        let scroll_position_after_input =
936            editor.update(cx, |editor, cx| editor.scroll_position(cx));
937        assert_ne!(scroll_position_before_input, scroll_position_after_input);
938
939        drop(go_to_line_view);
940        workspace.update_in(cx, |workspace, window, cx| {
941            workspace.hide_modal(window, cx);
942        });
943        cx.run_until_parked();
944
945        let scroll_position_after_auto_dismiss =
946            editor.update(cx, |editor, cx| editor.scroll_position(cx));
947        assert_eq!(
948            scroll_position_after_auto_dismiss, scroll_position_after_input,
949            "Dismissing via outside click should maintain new scroll position"
950        );
951    }
952
953    #[gpui::test]
954    async fn test_scroll_position_on_cancel(cx: &mut TestAppContext) {
955        init_test(cx);
956
957        let fs = FakeFs::new(cx.executor());
958        let file_content = (0..100)
959            .map(|i| format!("struct Line{};", i))
960            .collect::<Vec<_>>()
961            .join("\n");
962        fs.insert_tree(path!("/dir"), json!({"a.rs": file_content}))
963            .await;
964
965        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
966        let (multi_workspace, cx) =
967            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
968        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
969        let worktree_id = workspace.update(cx, |workspace, cx| {
970            workspace.project().update(cx, |project, cx| {
971                project.worktrees(cx).next().unwrap().read(cx).id()
972            })
973        });
974        let _buffer = project
975            .update(cx, |project, cx| {
976                project.open_local_buffer(path!("/dir/a.rs"), cx)
977            })
978            .await
979            .unwrap();
980        let editor = workspace
981            .update_in(cx, |workspace, window, cx| {
982                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
983            })
984            .await
985            .unwrap()
986            .downcast::<Editor>()
987            .unwrap();
988        let go_to_line_view = open_go_to_line_view(&workspace, cx);
989
990        let scroll_position_before_input =
991            editor.update(cx, |editor, cx| editor.scroll_position(cx));
992        cx.simulate_input("47");
993        let scroll_position_after_input =
994            editor.update(cx, |editor, cx| editor.scroll_position(cx));
995        assert_ne!(scroll_position_before_input, scroll_position_after_input);
996
997        cx.dispatch_action(menu::Cancel);
998        drop(go_to_line_view);
999        cx.run_until_parked();
1000
1001        let scroll_position_after_cancel =
1002            editor.update(cx, |editor, cx| editor.scroll_position(cx));
1003        assert_eq!(
1004            scroll_position_after_cancel, scroll_position_after_input,
1005            "Cancel should maintain new scroll position"
1006        );
1007    }
1008
1009    #[gpui::test]
1010    async fn test_scroll_position_on_confirm(cx: &mut TestAppContext) {
1011        init_test(cx);
1012
1013        let fs = FakeFs::new(cx.executor());
1014        let file_content = (0..100)
1015            .map(|i| format!("struct Line{};", i))
1016            .collect::<Vec<_>>()
1017            .join("\n");
1018        fs.insert_tree(path!("/dir"), json!({"a.rs": file_content}))
1019            .await;
1020
1021        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1022        let (multi_workspace, cx) =
1023            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1024        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1025        let worktree_id = workspace.update(cx, |workspace, cx| {
1026            workspace.project().update(cx, |project, cx| {
1027                project.worktrees(cx).next().unwrap().read(cx).id()
1028            })
1029        });
1030        let _buffer = project
1031            .update(cx, |project, cx| {
1032                project.open_local_buffer(path!("/dir/a.rs"), cx)
1033            })
1034            .await
1035            .unwrap();
1036        let editor = workspace
1037            .update_in(cx, |workspace, window, cx| {
1038                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
1039            })
1040            .await
1041            .unwrap()
1042            .downcast::<Editor>()
1043            .unwrap();
1044        let go_to_line_view = open_go_to_line_view(&workspace, cx);
1045
1046        let scroll_position_before_input =
1047            editor.update(cx, |editor, cx| editor.scroll_position(cx));
1048        cx.simulate_input("47");
1049        let scroll_position_after_input =
1050            editor.update(cx, |editor, cx| editor.scroll_position(cx));
1051        assert_ne!(scroll_position_before_input, scroll_position_after_input);
1052
1053        cx.dispatch_action(menu::Confirm);
1054        drop(go_to_line_view);
1055        cx.run_until_parked();
1056
1057        let scroll_position_after_confirm =
1058            editor.update(cx, |editor, cx| editor.scroll_position(cx));
1059        assert_eq!(
1060            scroll_position_after_confirm, scroll_position_after_input,
1061            "Confirm should maintain new scroll position"
1062        );
1063    }
1064}
1065
Served at tenant.openagents/omega Member data and write actions are omitted.