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, 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}