Let LineColumn on StatusBar as clickable to open GoToLineColumn (#9002)

Jason Lee created

Release Notes:

- Added to let LineColumn on StatusBar as clickable to open
GoToLineColumn.
- Added placeholder to GoToLineColumn input, and show help message on
input changed.

## Screenshot


![go-to-line-column](https://github.com/zed-industries/zed/assets/5518/90a4f644-07d4-4208-8caa-5510e1537f37)

Change summary

crates/editor/src/items.rs               |  91 +----------------------
crates/go_to_line/src/cursor_position.rs | 100 ++++++++++++++++++++++++++
crates/go_to_line/src/go_to_line.rs      |  53 +++++++++----
crates/zed/src/zed.rs                    |   3 
4 files changed, 144 insertions(+), 103 deletions(-)

Detailed changes

crates/editor/src/items.rs 🔗

@@ -7,9 +7,9 @@ use anyhow::{anyhow, Context as _, Result};
 use collections::HashSet;
 use futures::future::try_join_all;
 use gpui::{
-    div, point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId,
-    EventEmitter, IntoElement, Model, ParentElement, Pixels, Render, SharedString, Styled,
-    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+    point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
+    IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use language::{
     proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
@@ -21,7 +21,6 @@ use rpc::proto::{self, update_view, PeerId};
 use settings::Settings;
 use workspace::item::ItemSettings;
 
-use std::fmt::Write;
 use std::{
     borrow::Cow,
     cmp::{self, Ordering},
@@ -33,11 +32,8 @@ use std::{
 use text::{BufferId, Selection};
 use theme::Theme;
 use ui::{h_flex, prelude::*, Label};
-use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
-use workspace::{
-    item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
-    StatusItemView,
-};
+use util::{paths::PathExt, ResultExt, TryFutureExt};
+use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
     searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -1199,83 +1195,6 @@ pub fn active_match_index(
     }
 }
 
-pub struct CursorPosition {
-    position: Option<Point>,
-    selected_count: usize,
-    _observe_active_editor: Option<Subscription>,
-}
-
-impl Default for CursorPosition {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl CursorPosition {
-    pub fn new() -> Self {
-        Self {
-            position: None,
-            selected_count: 0,
-            _observe_active_editor: None,
-        }
-    }
-
-    fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
-        let editor = editor.read(cx);
-        let buffer = editor.buffer().read(cx).snapshot(cx);
-
-        self.selected_count = 0;
-        let mut last_selection: Option<Selection<usize>> = None;
-        for selection in editor.selections.all::<usize>(cx) {
-            self.selected_count += selection.end - selection.start;
-            if last_selection
-                .as_ref()
-                .map_or(true, |last_selection| selection.id > last_selection.id)
-            {
-                last_selection = Some(selection);
-            }
-        }
-        self.position = last_selection.map(|s| s.head().to_point(&buffer));
-
-        cx.notify();
-    }
-}
-
-impl Render for CursorPosition {
-    fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
-        div().when_some(self.position, |el, position| {
-            let mut text = format!(
-                "{}{FILE_ROW_COLUMN_DELIMITER}{}",
-                position.row + 1,
-                position.column + 1
-            );
-            if self.selected_count > 0 {
-                write!(text, " ({} selected)", self.selected_count).unwrap();
-            }
-
-            el.child(Label::new(text).size(LabelSize::Small))
-        })
-    }
-}
-
-impl StatusItemView for CursorPosition {
-    fn set_active_pane_item(
-        &mut self,
-        active_pane_item: Option<&dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
-            self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
-            self.update_position(editor, cx);
-        } else {
-            self.position = None;
-            self._observe_active_editor = None;
-        }
-
-        cx.notify();
-    }
-}
-
 fn path_for_buffer<'a>(
     buffer: &Model<MultiBuffer>,
     height: usize,

crates/go_to_line/src/cursor_position.rs 🔗

@@ -0,0 +1,100 @@
+use editor::{Editor, ToPoint};
+use gpui::{Subscription, View, WeakView};
+use std::fmt::Write;
+use text::{Point, Selection};
+use ui::{
+    div, Button, ButtonCommon, Clickable, FluentBuilder, IntoElement, LabelSize, ParentElement,
+    Render, Tooltip, ViewContext,
+};
+use util::paths::FILE_ROW_COLUMN_DELIMITER;
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+pub struct CursorPosition {
+    position: Option<Point>,
+    selected_count: usize,
+    workspace: WeakView<Workspace>,
+    _observe_active_editor: Option<Subscription>,
+}
+
+impl CursorPosition {
+    pub fn new(workspace: &Workspace) -> Self {
+        Self {
+            position: None,
+            selected_count: 0,
+            workspace: workspace.weak_handle(),
+            _observe_active_editor: None,
+        }
+    }
+
+    fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+        let buffer = editor.buffer().read(cx).snapshot(cx);
+
+        self.selected_count = 0;
+        let mut last_selection: Option<Selection<usize>> = None;
+        for selection in editor.selections.all::<usize>(cx) {
+            self.selected_count += selection.end - selection.start;
+            if last_selection
+                .as_ref()
+                .map_or(true, |last_selection| selection.id > last_selection.id)
+            {
+                last_selection = Some(selection);
+            }
+        }
+        self.position = last_selection.map(|s| s.head().to_point(&buffer));
+
+        cx.notify();
+    }
+}
+
+impl Render for CursorPosition {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div().when_some(self.position, |el, position| {
+            let mut text = format!(
+                "{}{FILE_ROW_COLUMN_DELIMITER}{}",
+                position.row + 1,
+                position.column + 1
+            );
+            if self.selected_count > 0 {
+                write!(text, " ({} selected)", self.selected_count).unwrap();
+            }
+
+            el.child(
+                Button::new("go-to-line-column", text)
+                    .label_size(LabelSize::Small)
+                    .on_click(cx.listener(|this, _, cx| {
+                        if let Some(workspace) = this.workspace.upgrade() {
+                            workspace.update(cx, |workspace, cx| {
+                                if let Some(editor) = workspace
+                                    .active_item(cx)
+                                    .and_then(|item| item.act_as::<Editor>(cx))
+                                {
+                                    workspace
+                                        .toggle_modal(cx, |cx| crate::GoToLine::new(editor, cx))
+                                }
+                            });
+                        }
+                    }))
+                    .tooltip(|cx| Tooltip::for_action("Go to Line/Column", &crate::Toggle, cx)),
+            )
+        })
+    }
+}
+
+impl StatusItemView for CursorPosition {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+            self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
+            self.update_position(editor, cx);
+        } else {
+            self.position = None;
+            self._observe_active_editor = None;
+        }
+
+        cx.notify();
+    }
+}

crates/go_to_line/src/go_to_line.rs 🔗

@@ -1,3 +1,5 @@
+pub mod cursor_position;
+
 use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Editor};
 use gpui::{
     actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
@@ -49,20 +51,24 @@ impl GoToLine {
     }
 
     pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
-        let line_editor = cx.new_view(|cx| Editor::single_line(cx));
+        let editor = active_editor.read(cx);
+        let cursor = editor.selections.last::<Point>(cx).head();
+
+        let line = cursor.row + 1;
+        let column = cursor.column + 1;
+
+        let line_editor = cx.new_view(|cx| {
+            let mut editor = Editor::single_line(cx);
+            editor.set_placeholder_text(format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"), cx);
+            editor
+        });
         let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
 
         let editor = active_editor.read(cx);
-        let cursor = editor.selections.last::<Point>(cx).head();
         let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row;
         let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx));
 
-        let current_text = format!(
-            "line {} of {} (column {})",
-            cursor.row + 1,
-            last_line + 1,
-            cursor.column + 1,
-        );
+        let current_text = format!("line {} of {} (column {})", line, last_line + 1, column);
 
         Self {
             line_editor,
@@ -116,17 +122,22 @@ impl GoToLine {
     }
 
     fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
-        let line_editor = self.line_editor.read(cx).text(cx);
-        let mut components = line_editor
+        let (row, column) = self.line_column_from_query(cx);
+        Some(Point::new(
+            row?.saturating_sub(1),
+            column.unwrap_or(0).saturating_sub(1),
+        ))
+    }
+
+    fn line_column_from_query(&self, cx: &ViewContext<Self>) -> (Option<u32>, Option<u32>) {
+        let input = self.line_editor.read(cx).text(cx);
+        let mut components = input
             .splitn(2, FILE_ROW_COLUMN_DELIMITER)
             .map(str::trim)
             .fuse();
-        let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
+        let row = components.next().and_then(|row| row.parse::<u32>().ok());
         let column = components.next().and_then(|col| col.parse::<u32>().ok());
-        Some(Point::new(
-            row.saturating_sub(1),
-            column.unwrap_or(0).saturating_sub(1),
-        ))
+        (row, column)
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
@@ -153,6 +164,16 @@ impl GoToLine {
 
 impl Render for GoToLine {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let mut help_text = self.current_text.clone();
+        let query = self.line_column_from_query(cx);
+        if let Some(line) = query.0 {
+            if let Some(column) = query.1 {
+                help_text = format!("Go to line {line}, column {column}").into();
+            } else {
+                help_text = format!("Go to line {line}").into();
+            }
+        }
+
         div()
             .elevation_2(cx)
             .key_context("GoToLine")
@@ -181,7 +202,7 @@ impl Render for GoToLine {
                             .justify_between()
                             .px_2()
                             .py_1()
-                            .child(Label::new(self.current_text.clone()).color(Color::Muted)),
+                            .child(Label::new(help_text).color(Color::Muted)),
                     ),
             )
     }

crates/zed/src/zed.rs 🔗

@@ -132,7 +132,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         let vim_mode_indicator = cx.new_view(|cx| vim::ModeIndicator::new(cx));
         let feedback_button =
             cx.new_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
-        let cursor_position = cx.new_view(|_| editor::items::CursorPosition::new());
+        let cursor_position =
+            cx.new_view(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
         workspace.status_bar().update(cx, |status_bar, cx| {
             status_bar.add_left_item(diagnostic_summary, cx);
             status_bar.add_left_item(activity_indicator, cx);