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