1pub mod cursor_position;
2
3use editor::{scroll::Autoscroll, Editor};
4use gpui::{
5 actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
6 FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
7};
8use text::{Bias, Point};
9use theme::ActiveTheme;
10use ui::{h_flex, prelude::*, v_flex, Label};
11use util::paths::FILE_ROW_COLUMN_DELIMITER;
12use workspace::ModalView;
13
14actions!(go_to_line, [Toggle]);
15
16pub fn init(cx: &mut AppContext) {
17 cx.observe_new_views(GoToLine::register).detach();
18}
19
20pub struct GoToLine {
21 line_editor: View<Editor>,
22 active_editor: View<Editor>,
23 current_text: SharedString,
24 prev_scroll_position: Option<gpui::Point<f32>>,
25 _subscriptions: Vec<Subscription>,
26}
27
28impl ModalView for GoToLine {}
29
30impl FocusableView for GoToLine {
31 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
32 self.line_editor.focus_handle(cx)
33 }
34}
35impl EventEmitter<DismissEvent> for GoToLine {}
36
37enum GoToLineRowHighlights {}
38
39impl GoToLine {
40 fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
41 let handle = cx.view().downgrade();
42 editor.register_action(move |_: &Toggle, cx| {
43 let Some(editor) = handle.upgrade() else {
44 return;
45 };
46 let Some(workspace) = editor.read(cx).workspace() else {
47 return;
48 };
49 workspace.update(cx, |workspace, cx| {
50 workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
51 })
52 });
53 }
54
55 pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
56 let editor = active_editor.read(cx);
57 let cursor = editor.selections.last::<Point>(cx).head();
58
59 let line = cursor.row + 1;
60 let column = cursor.column + 1;
61
62 let line_editor = cx.new_view(|cx| {
63 let mut editor = Editor::single_line(cx);
64 editor.set_placeholder_text(format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"), cx);
65 editor
66 });
67 let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
68
69 let editor = active_editor.read(cx);
70 let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row;
71 let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx));
72
73 let current_text = format!("line {} of {} (column {})", line, last_line + 1, column);
74
75 Self {
76 line_editor,
77 active_editor,
78 current_text: current_text.into(),
79 prev_scroll_position: Some(scroll_position),
80 _subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
81 }
82 }
83
84 fn release(&mut self, window: AnyWindowHandle, cx: &mut AppContext) {
85 window
86 .update(cx, |_, cx| {
87 let scroll_position = self.prev_scroll_position.take();
88 self.active_editor.update(cx, |editor, cx| {
89 editor.clear_row_highlights::<GoToLineRowHighlights>();
90 if let Some(scroll_position) = scroll_position {
91 editor.set_scroll_position(scroll_position, cx);
92 }
93 cx.notify();
94 })
95 })
96 .ok();
97 }
98
99 fn on_line_editor_event(
100 &mut self,
101 _: View<Editor>,
102 event: &editor::EditorEvent,
103 cx: &mut ViewContext<Self>,
104 ) {
105 match event {
106 editor::EditorEvent::Blurred => cx.emit(DismissEvent),
107 editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
108 _ => {}
109 }
110 }
111
112 fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
113 if let Some(point) = self.point_from_query(cx) {
114 self.active_editor.update(cx, |active_editor, cx| {
115 let snapshot = active_editor.snapshot(cx).display_snapshot;
116 let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
117 let anchor = snapshot.buffer_snapshot.anchor_before(point);
118 active_editor.clear_row_highlights::<GoToLineRowHighlights>();
119 active_editor.highlight_rows::<GoToLineRowHighlights>(
120 anchor..anchor,
121 Some(cx.theme().colors().editor_highlighted_line_background),
122 cx,
123 );
124 active_editor.request_autoscroll(Autoscroll::center(), cx);
125 });
126 cx.notify();
127 }
128 }
129
130 fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
131 let (row, column) = self.line_column_from_query(cx);
132 Some(Point::new(
133 row?.saturating_sub(1),
134 column.unwrap_or(0).saturating_sub(1),
135 ))
136 }
137
138 fn line_column_from_query(&self, cx: &ViewContext<Self>) -> (Option<u32>, Option<u32>) {
139 let input = self.line_editor.read(cx).text(cx);
140 let mut components = input
141 .splitn(2, FILE_ROW_COLUMN_DELIMITER)
142 .map(str::trim)
143 .fuse();
144 let row = components.next().and_then(|row| row.parse::<u32>().ok());
145 let column = components.next().and_then(|col| col.parse::<u32>().ok());
146 (row, column)
147 }
148
149 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
150 cx.emit(DismissEvent);
151 }
152
153 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
154 if let Some(point) = self.point_from_query(cx) {
155 self.active_editor.update(cx, |editor, cx| {
156 let snapshot = editor.snapshot(cx).display_snapshot;
157 let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
158 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
159 s.select_ranges([point..point])
160 });
161 editor.focus(cx);
162 cx.notify();
163 });
164 self.prev_scroll_position.take();
165 }
166
167 cx.emit(DismissEvent);
168 }
169}
170
171impl Render for GoToLine {
172 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
173 let mut help_text = self.current_text.clone();
174 let query = self.line_column_from_query(cx);
175 if let Some(line) = query.0 {
176 if let Some(column) = query.1 {
177 help_text = format!("Go to line {line}, column {column}").into();
178 } else {
179 help_text = format!("Go to line {line}").into();
180 }
181 }
182
183 div()
184 .elevation_2(cx)
185 .key_context("GoToLine")
186 .on_action(cx.listener(Self::cancel))
187 .on_action(cx.listener(Self::confirm))
188 .w_96()
189 .child(
190 v_flex()
191 .px_1()
192 .pt_0p5()
193 .gap_px()
194 .child(
195 v_flex()
196 .py_0p5()
197 .px_1()
198 .child(div().px_1().py_0p5().child(self.line_editor.clone())),
199 )
200 .child(
201 div()
202 .h_px()
203 .w_full()
204 .bg(cx.theme().colors().element_background),
205 )
206 .child(
207 h_flex()
208 .justify_between()
209 .px_2()
210 .py_1()
211 .child(Label::new(help_text).color(Color::Muted)),
212 ),
213 )
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use std::sync::Arc;
220
221 use gpui::{TestAppContext, VisualTestContext};
222 use indoc::indoc;
223 use project::{FakeFs, Project};
224 use serde_json::json;
225 use workspace::{AppState, Workspace};
226
227 use super::*;
228
229 #[gpui::test]
230 async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
231 init_test(cx);
232 let fs = FakeFs::new(cx.executor());
233 fs.insert_tree(
234 "/dir",
235 json!({
236 "a.rs": indoc!{"
237 struct SingleLine; // display line 0
238 // display line 1
239 struct MultiLine { // display line 2
240 field_1: i32, // display line 3
241 field_2: i32, // display line 4
242 } // display line 5
243 // display line 7
244 struct Another { // display line 8
245 field_1: i32, // display line 9
246 field_2: i32, // display line 10
247 field_3: i32, // display line 11
248 field_4: i32, // display line 12
249 } // display line 13
250 "}
251 }),
252 )
253 .await;
254
255 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
256 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
257 let worktree_id = workspace.update(cx, |workspace, cx| {
258 workspace.project().update(cx, |project, cx| {
259 project.worktrees().next().unwrap().read(cx).id()
260 })
261 });
262 let _buffer = project
263 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
264 .await
265 .unwrap();
266 let editor = workspace
267 .update(cx, |workspace, cx| {
268 workspace.open_path((worktree_id, "a.rs"), None, true, cx)
269 })
270 .await
271 .unwrap()
272 .downcast::<Editor>()
273 .unwrap();
274
275 let go_to_line_view = open_go_to_line_view(&workspace, cx);
276 assert_eq!(
277 highlighted_display_rows(&editor, cx),
278 Vec::<u32>::new(),
279 "Initially opened go to line modal should not highlight any rows"
280 );
281 assert_single_caret_at_row(&editor, 0, cx);
282
283 cx.simulate_input("1");
284 assert_eq!(
285 highlighted_display_rows(&editor, cx),
286 vec![0],
287 "Go to line modal should highlight a row, corresponding to the query"
288 );
289 assert_single_caret_at_row(&editor, 0, cx);
290
291 cx.simulate_input("8");
292 assert_eq!(
293 highlighted_display_rows(&editor, cx),
294 vec![13],
295 "If the query is too large, the last row should be highlighted"
296 );
297 assert_single_caret_at_row(&editor, 0, cx);
298
299 cx.dispatch_action(menu::Cancel);
300 drop(go_to_line_view);
301 editor.update(cx, |_, _| {});
302 assert_eq!(
303 highlighted_display_rows(&editor, cx),
304 Vec::<u32>::new(),
305 "After cancelling and closing the modal, no rows should be highlighted"
306 );
307 assert_single_caret_at_row(&editor, 0, cx);
308
309 let go_to_line_view = open_go_to_line_view(&workspace, cx);
310 assert_eq!(
311 highlighted_display_rows(&editor, cx),
312 Vec::<u32>::new(),
313 "Reopened modal should not highlight any rows"
314 );
315 assert_single_caret_at_row(&editor, 0, cx);
316
317 let expected_highlighted_row = 4;
318 cx.simulate_input("5");
319 assert_eq!(
320 highlighted_display_rows(&editor, cx),
321 vec![expected_highlighted_row]
322 );
323 assert_single_caret_at_row(&editor, 0, cx);
324 cx.dispatch_action(menu::Confirm);
325 drop(go_to_line_view);
326 editor.update(cx, |_, _| {});
327 assert_eq!(
328 highlighted_display_rows(&editor, cx),
329 Vec::<u32>::new(),
330 "After confirming and closing the modal, no rows should be highlighted"
331 );
332 // On confirm, should place the caret on the highlighted row.
333 assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
334 }
335
336 fn open_go_to_line_view(
337 workspace: &View<Workspace>,
338 cx: &mut VisualTestContext,
339 ) -> View<GoToLine> {
340 cx.dispatch_action(Toggle);
341 workspace.update(cx, |workspace, cx| {
342 workspace.active_modal::<GoToLine>(cx).unwrap().clone()
343 })
344 }
345
346 fn highlighted_display_rows(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
347 editor.update(cx, |editor, cx| {
348 editor.highlighted_display_rows(cx).into_keys().collect()
349 })
350 }
351
352 #[track_caller]
353 fn assert_single_caret_at_row(
354 editor: &View<Editor>,
355 buffer_row: u32,
356 cx: &mut VisualTestContext,
357 ) {
358 let selections = editor.update(cx, |editor, cx| {
359 editor
360 .selections
361 .all::<rope::Point>(cx)
362 .into_iter()
363 .map(|s| s.start..s.end)
364 .collect::<Vec<_>>()
365 });
366 assert!(
367 selections.len() == 1,
368 "Expected one caret selection but got: {selections:?}"
369 );
370 let selection = &selections[0];
371 assert!(
372 selection.start == selection.end,
373 "Expected a single caret selection, but got: {selection:?}"
374 );
375 assert_eq!(selection.start.row, buffer_row);
376 }
377
378 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
379 cx.update(|cx| {
380 let state = AppState::test(cx);
381 language::init(cx);
382 crate::init(cx);
383 editor::init(cx);
384 workspace::init_settings(cx);
385 Project::init_settings(cx);
386 state
387 })
388 }
389}