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