go_to_line.rs

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