go_to_line.rs

  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}