From 5abcc1c3c533bc90010271e0af89d175f1a557bb Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Sat, 9 Mar 2024 05:11:17 +0800 Subject: [PATCH] Let LineColumn on StatusBar as clickable to open GoToLineColumn (#9002) 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) --- 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(-) create mode 100644 crates/go_to_line/src/cursor_position.rs diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 298e242dd9..2078baf5aa 100644 --- a/crates/editor/src/items.rs +++ b/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, - selected_count: usize, - _observe_active_editor: Option, -} - -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, cx: &mut ViewContext) { - let editor = editor.read(cx); - let buffer = editor.buffer().read(cx).snapshot(cx); - - self.selected_count = 0; - let mut last_selection: Option> = None; - for selection in editor.selections.all::(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) -> 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, - ) { - if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(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, height: usize, diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs new file mode 100644 index 0000000000..55193a63c4 --- /dev/null +++ b/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, + selected_count: usize, + workspace: WeakView, + _observe_active_editor: Option, +} + +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, cx: &mut ViewContext) { + let editor = editor.read(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); + + self.selected_count = 0; + let mut last_selection: Option> = None; + for selection in editor.selections.all::(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) -> 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::(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, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(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(); + } +} diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 0a74f1ac03..5957d77b87 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/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, cx: &mut ViewContext) -> Self { - let line_editor = cx.new_view(|cx| Editor::single_line(cx)); + let editor = active_editor.read(cx); + let cursor = editor.selections.last::(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::(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) -> Option { - 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) -> (Option, Option) { + 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::().ok())?; + let row = components.next().and_then(|row| row.parse::().ok()); let column = components.next().and_then(|col| col.parse::().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) { @@ -153,6 +164,16 @@ impl GoToLine { impl Render for GoToLine { fn render(&mut self, cx: &mut ViewContext) -> 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)), ), ) } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4d32b40a19..9ae5c2646f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -132,7 +132,8 @@ pub fn initialize_workspace(app_state: Arc, 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);