go_to_line.rs

  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}