1use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
2use gpui::{
3 actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
4 FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
5 WindowContext,
6};
7use text::{Bias, Point};
8use theme::ActiveTheme;
9use ui::{h_stack, prelude::*, v_stack, Label};
10use util::paths::FILE_ROW_COLUMN_DELIMITER;
11use workspace::ModalView;
12
13actions!(go_to_line, [Toggle]);
14
15pub fn init(cx: &mut AppContext) {
16 cx.observe_new_views(GoToLine::register).detach();
17}
18
19pub struct GoToLine {
20 line_editor: View<Editor>,
21 active_editor: View<Editor>,
22 current_text: SharedString,
23 prev_scroll_position: Option<gpui::Point<f32>>,
24 _subscriptions: Vec<Subscription>,
25}
26
27impl ModalView for GoToLine {}
28
29impl FocusableView for GoToLine {
30 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
31 self.line_editor.focus_handle(cx)
32 }
33}
34impl EventEmitter<DismissEvent> for GoToLine {}
35
36impl GoToLine {
37 fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
38 let handle = cx.view().downgrade();
39 editor.register_action(move |_: &Toggle, cx| {
40 let Some(editor) = handle.upgrade() else {
41 return;
42 };
43 let Some(workspace) = editor.read(cx).workspace() else {
44 return;
45 };
46 workspace.update(cx, |workspace, cx| {
47 workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
48 })
49 });
50 }
51
52 pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
53 let line_editor = cx.build_view(|cx| Editor::single_line(cx));
54 let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
55
56 let editor = active_editor.read(cx);
57 let cursor = editor.selections.last::<Point>(cx).head();
58 let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row;
59 let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx));
60
61 let current_text = format!(
62 "line {} of {} (column {})",
63 cursor.row + 1,
64 last_line + 1,
65 cursor.column + 1,
66 );
67
68 Self {
69 line_editor,
70 active_editor,
71 current_text: current_text.into(),
72 prev_scroll_position: Some(scroll_position),
73 _subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
74 }
75 }
76
77 fn release(&mut self, cx: &mut WindowContext) {
78 let scroll_position = self.prev_scroll_position.take();
79 self.active_editor.update(cx, |editor, cx| {
80 editor.highlight_rows(None);
81 if let Some(scroll_position) = scroll_position {
82 editor.set_scroll_position(scroll_position, cx);
83 }
84 cx.notify();
85 })
86 }
87
88 fn on_line_editor_event(
89 &mut self,
90 _: View<Editor>,
91 event: &editor::EditorEvent,
92 cx: &mut ViewContext<Self>,
93 ) {
94 match event {
95 editor::EditorEvent::Blurred => cx.emit(DismissEvent),
96 editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
97 _ => {}
98 }
99 }
100
101 fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
102 if let Some(point) = self.point_from_query(cx) {
103 self.active_editor.update(cx, |active_editor, cx| {
104 let snapshot = active_editor.snapshot(cx).display_snapshot;
105 let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
106 let display_point = point.to_display_point(&snapshot);
107 let row = display_point.row();
108 active_editor.highlight_rows(Some(row..row + 1));
109 active_editor.request_autoscroll(Autoscroll::center(), cx);
110 });
111 cx.notify();
112 }
113 }
114
115 fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
116 let line_editor = self.line_editor.read(cx).text(cx);
117 let mut components = line_editor
118 .splitn(2, FILE_ROW_COLUMN_DELIMITER)
119 .map(str::trim)
120 .fuse();
121 let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
122 let column = components.next().and_then(|col| col.parse::<u32>().ok());
123 Some(Point::new(
124 row.saturating_sub(1),
125 column.unwrap_or(0).saturating_sub(1),
126 ))
127 }
128
129 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
130 cx.emit(DismissEvent);
131 }
132
133 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
134 if let Some(point) = self.point_from_query(cx) {
135 self.active_editor.update(cx, |editor, cx| {
136 let snapshot = editor.snapshot(cx).display_snapshot;
137 let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
138 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
139 s.select_ranges([point..point])
140 });
141 editor.focus(cx);
142 cx.notify();
143 });
144 self.prev_scroll_position.take();
145 }
146
147 cx.emit(DismissEvent);
148 }
149}
150
151impl Render for GoToLine {
152 type Element = Div;
153
154 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
155 div()
156 .elevation_2(cx)
157 .key_context("GoToLine")
158 .on_action(cx.listener(Self::cancel))
159 .on_action(cx.listener(Self::confirm))
160 .w_96()
161 .child(
162 v_stack()
163 .px_1()
164 .pt_0p5()
165 .gap_px()
166 .child(
167 v_stack()
168 .py_0p5()
169 .px_1()
170 .child(div().px_1().py_0p5().child(self.line_editor.clone())),
171 )
172 .child(
173 div()
174 .h_px()
175 .w_full()
176 .bg(cx.theme().colors().element_background),
177 )
178 .child(
179 h_stack()
180 .justify_between()
181 .px_2()
182 .py_1()
183 .child(Label::new(self.current_text.clone()).color(Color::Muted)),
184 ),
185 )
186 }
187}