From a26b0667885a76f9eb2558e0581729fa9c222774 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 3 Nov 2021 17:27:51 +0100 Subject: [PATCH] Introduce a status bar and add the cursor position to it Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/editor/src/lib.rs | 15 ++++ crates/gpui/src/app.rs | 6 ++ crates/theme/src/lib.rs | 9 ++ crates/workspace/src/items.rs | 84 +++++++++++++++++- crates/workspace/src/lib.rs | 26 +++++- crates/workspace/src/status_bar.rs | 127 ++++++++++++++++++++++++++++ crates/zed/assets/themes/_base.toml | 8 +- 7 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 crates/workspace/src/status_bar.rs diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 56f93eb5af88bc827b576936103aa178fa88903b..1f03133c84d3c1f03751b3f397b65b70ab0a4021 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -2219,6 +2219,21 @@ impl Editor { .map(|(set_id, _)| *set_id) } + pub fn last_selection(&self, cx: &AppContext) -> Selection { + if let Some(pending_selection) = self.pending_selection.as_ref() { + pending_selection.clone() + } else { + let buffer = self.buffer.read(cx); + let last_selection = buffer + .selection_set(self.selection_set_id) + .unwrap() + .point_selections(buffer) + .max_by_key(|s| s.id) + .unwrap(); + last_selection + } + } + pub fn selections_in_range<'a>( &'a self, set_id: SelectionSetId, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index dc7e4d19b584a1650bd97a4da20b7844444ff927..a41ec8c96bc1d4aa2ba65e57bcd2a7a761f6068c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2456,6 +2456,12 @@ impl UpdateModel for RenderContext<'_, V> { } } +impl ReadView for RenderContext<'_, V> { + fn read_view(&self, handle: &ViewHandle) -> &T { + self.app.read_view(handle) + } +} + impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.cx diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 2a0abc4395011c5a954c1af1a827981a5800af39..eb60f6cf3a5d8f93da8bc11e6636c34510529d7e 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -35,6 +35,7 @@ pub struct Workspace { pub pane_divider: Border, pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, + pub status_bar: StatusBar, } #[derive(Clone, Deserialize, Default)] @@ -88,6 +89,14 @@ pub struct SidebarItem { pub height: f32, } +#[derive(Deserialize, Default)] +pub struct StatusBar { + #[serde(flatten)] + pub container: ContainerStyle, + pub height: f32, + pub cursor_position: TextStyle, +} + #[derive(Deserialize, Default)] pub struct ChatPanel { #[serde(flatten)] diff --git a/crates/workspace/src/items.rs b/crates/workspace/src/items.rs index 0b4b5f0d51719843d6ff61fc6ca5720784e8f196..8692b072d48884116f9574620db32bf27d2f7ca9 100644 --- a/crates/workspace/src/items.rs +++ b/crates/workspace/src/items.rs @@ -1,11 +1,16 @@ use super::{Item, ItemView}; -use crate::Settings; +use crate::{status_bar::StatusItemView, Settings}; use anyhow::Result; +use buffer::{Point, ToOffset}; use editor::{Editor, EditorSettings, Event}; -use gpui::{fonts::TextStyle, AppContext, ModelHandle, Task, ViewContext}; +use gpui::{ + elements::*, fonts::TextStyle, AppContext, Entity, ModelHandle, RenderContext, Subscription, + Task, View, ViewContext, ViewHandle, +}; use language::{Buffer, File as _}; use postage::watch; use project::{ProjectPath, Worktree}; +use std::fmt::Write; use std::path::Path; impl Item for Buffer { @@ -156,3 +161,78 @@ impl ItemView for Editor { self.buffer().read(cx).has_conflict() } } + +pub struct CursorPosition { + position: Option, + selected_count: usize, + settings: watch::Receiver, + _observe_active_editor: Option, +} + +impl CursorPosition { + pub fn new(settings: watch::Receiver) -> Self { + Self { + position: None, + selected_count: 0, + settings, + _observe_active_editor: None, + } + } + + fn update_position(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + let editor = editor.read(cx); + + let last_selection = editor.last_selection(cx); + self.position = Some(last_selection.head()); + if last_selection.is_empty() { + self.selected_count = 0; + } else { + let buffer = editor.buffer().read(cx); + let start = last_selection.start.to_offset(buffer); + let end = last_selection.end.to_offset(buffer); + self.selected_count = end - start; + } + cx.notify(); + } +} + +impl Entity for CursorPosition { + type Event = (); +} + +impl View for CursorPosition { + fn ui_name() -> &'static str { + "CursorPosition" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + if let Some(position) = self.position { + let theme = &self.settings.borrow().theme.workspace.status_bar; + let mut text = format!("{},{}", position.row + 1, position.column + 1); + if self.selected_count > 0 { + write!(text, " ({} selected)", self.selected_count).unwrap(); + } + Label::new(text, theme.cursor_position.clone()).boxed() + } else { + Empty::new().boxed() + } + } +} + +impl StatusItemView for CursorPosition { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemViewHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::()) { + 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/workspace/src/lib.rs b/crates/workspace/src/lib.rs index ec1f39e48019ab90fc1826b20cea777173b4b285..08d26e29a2bc1a589ec74fd10d22263ef1f85679 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -3,15 +3,16 @@ pub mod pane; pub mod pane_group; pub mod settings; pub mod sidebar; +mod status_bar; use anyhow::Result; -use language::{Buffer, LanguageRegistry}; use client::{Authenticate, ChannelList, Client, UserStore}; use gpui::{ action, elements::*, json::to_string_pretty, keymap::Binding, platform::CursorStyle, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; +use language::{Buffer, LanguageRegistry}; use log::error; pub use pane::*; pub use pane_group::*; @@ -26,6 +27,8 @@ use std::{ sync::Arc, }; +use crate::status_bar::StatusBar; + action!(OpenNew, WorkspaceParams); action!(Save); action!(DebugElements); @@ -311,6 +314,7 @@ pub struct Workspace { right_sidebar: Sidebar, panes: Vec>, active_pane: ViewHandle, + status_bar: ViewHandle, project: ModelHandle, items: Vec>, loading_items: HashMap< @@ -345,6 +349,13 @@ impl Workspace { .detach(); cx.focus(&pane); + let cursor_position = cx.add_view(|_| items::CursorPosition::new(params.settings.clone())); + let status_bar = cx.add_view(|cx| { + let mut status_bar = StatusBar::new(&pane, params.settings.clone(), cx); + status_bar.add_right_item(cursor_position, cx); + status_bar + }); + let mut current_user = params.user_store.read(cx).watch_current_user().clone(); let mut connection_status = params.client.status().clone(); let _observe_current_user = cx.spawn_weak(|this, mut cx| async move { @@ -367,6 +378,7 @@ impl Workspace { center: PaneGroup::new(pane.id()), panes: vec![pane.clone()], active_pane: pane.clone(), + status_bar, settings: params.settings.clone(), client: params.client.clone(), user_store: params.user_store.clone(), @@ -824,6 +836,9 @@ impl Workspace { fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { self.active_pane = pane; + self.status_bar.update(cx, |status_bar, cx| { + status_bar.set_active_pane(&self.active_pane, cx); + }); cx.focus(&self.active_pane); cx.notify(); } @@ -1017,7 +1032,14 @@ impl View for Workspace { content.add_child(Flexible::new(0.8, element).boxed()); } content.add_child( - Expanded::new(1.0, self.center.render(&settings.theme)).boxed(), + Flex::column() + .with_child( + Expanded::new(1.0, self.center.render(&settings.theme)) + .boxed(), + ) + .with_child(ChildView::new(self.status_bar.id()).boxed()) + .expanded(1.) + .boxed(), ); if let Some(element) = self.right_sidebar.render_active_item(&settings, cx) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs new file mode 100644 index 0000000000000000000000000000000000000000..970078dcb50dfa78a2913107a8029dd492230957 --- /dev/null +++ b/crates/workspace/src/status_bar.rs @@ -0,0 +1,127 @@ +use crate::{ItemViewHandle, Pane, Settings}; +use gpui::{ + elements::*, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View, + ViewContext, ViewHandle, +}; +use postage::watch; + +pub trait StatusItemView: View { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn crate::ItemViewHandle>, + cx: &mut ViewContext, + ); +} + +trait StatusItemViewHandle { + fn id(&self) -> usize; + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemViewHandle>, + cx: &mut MutableAppContext, + ); +} + +pub struct StatusBar { + left_items: Vec>, + right_items: Vec>, + active_pane: ViewHandle, + _observe_active_pane: Subscription, + settings: watch::Receiver, +} + +impl Entity for StatusBar { + type Event = (); +} + +impl View for StatusBar { + fn ui_name() -> &'static str { + "StatusBar" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme.workspace.status_bar; + Flex::row() + .with_children( + self.left_items + .iter() + .map(|i| ChildView::new(i.id()).aligned().boxed()), + ) + .with_child(Empty::new().expanded(1.).boxed()) + .with_children( + self.right_items + .iter() + .map(|i| ChildView::new(i.id()).aligned().boxed()), + ) + .contained() + .with_style(theme.container) + .constrained() + .with_height(theme.height) + .boxed() + } +} + +impl StatusBar { + pub fn new( + active_pane: &ViewHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + let mut this = Self { + left_items: Default::default(), + right_items: Default::default(), + active_pane: active_pane.clone(), + _observe_active_pane: cx + .observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)), + settings, + }; + this.update_active_pane_item(cx); + this + } + + pub fn add_left_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.left_items.push(Box::new(item)); + cx.notify(); + } + + pub fn add_right_item(&mut self, item: ViewHandle, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.right_items.push(Box::new(item)); + cx.notify(); + } + + pub fn set_active_pane(&mut self, active_pane: &ViewHandle, cx: &mut ViewContext) { + self.active_pane = active_pane.clone(); + self._observe_active_pane = + cx.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)); + self.update_active_pane_item(cx); + } + + fn update_active_pane_item(&mut self, cx: &mut ViewContext) { + let active_pane_item = self.active_pane.read(cx).active_item(); + for item in self.left_items.iter().chain(&self.right_items) { + item.set_active_pane_item(active_pane_item.as_deref(), cx); + } + } +} + +impl StatusItemViewHandle for ViewHandle { + fn id(&self) -> usize { + self.id() + } + + fn set_active_pane_item( + &self, + active_pane_item: Option<&dyn ItemViewHandle>, + cx: &mut MutableAppContext, + ) { + self.update(cx, |this, cx| { + this.set_active_pane_item(active_pane_item, cx) + }); + } +} diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index d384b85d68c93cb7c7bcce4fc477f73970926fd1..a1a2397f9a5c60159e014bb93c0226eed38f9f32 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -60,6 +60,11 @@ border = { width = 1, color = "$border.0", right = true } extends = "$workspace.sidebar" border = { width = 1, color = "$border.0", left = true } +[workspace.status_bar] +padding = { left = 6, right = 6 } +height = 24 +cursor_position = "$text.2" + [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } @@ -164,7 +169,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -226,7 +231,6 @@ line_number = "$text.2.color" line_number_active = "$text.0.color" selection = "$selection.host" guest_selections = "$selection.guests" - error_underline = "$status.bad" warning_underline = "$status.warn" info_underline = "$status.info"