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