From 4852e170ffd63532bc751da8ecc3cae05056586c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jul 2024 11:54:52 -0600 Subject: [PATCH] Introducing multibuffers (#14668) Co-Authored-By: Marshall Release Notes: - Added a hint the first few times you open a multibuffer to explain what is going on. Co-authored-by: Marshall --- crates/welcome/src/multibuffer_hint.rs | 142 +++++++++++++++++++++++++ crates/welcome/src/welcome.rs | 12 ++- crates/zed/src/zed.rs | 4 +- docs/src/SUMMARY.md | 1 + docs/src/multibuffers.md | 25 +++++ 5 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 crates/welcome/src/multibuffer_hint.rs create mode 100644 docs/src/multibuffers.md diff --git a/crates/welcome/src/multibuffer_hint.rs b/crates/welcome/src/multibuffer_hint.rs new file mode 100644 index 0000000000..0c74c5ce5d --- /dev/null +++ b/crates/welcome/src/multibuffer_hint.rs @@ -0,0 +1,142 @@ +use std::collections::HashSet; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::OnceLock; + +use db::kvp::KEY_VALUE_STORE; +use gpui::{AppContext, Empty, EntityId, EventEmitter}; +use ui::{prelude::*, ButtonLike, IconButtonShape, Tooltip}; +use workspace::item::ItemHandle; +use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; + +pub struct MultibufferHint { + shown_on: HashSet, + active_item: Option>, +} + +const NUMBER_OF_HINTS: usize = 10; + +const SHOWN_COUNT_KEY: &str = "MULTIBUFFER_HINT_SHOWN_COUNT"; + +impl MultibufferHint { + pub fn new() -> Self { + Self { + shown_on: Default::default(), + active_item: None, + } + } +} + +impl MultibufferHint { + fn counter() -> &'static AtomicUsize { + static SHOWN_COUNT: OnceLock = OnceLock::new(); + SHOWN_COUNT.get_or_init(|| { + let value: usize = KEY_VALUE_STORE + .read_kvp(SHOWN_COUNT_KEY) + .ok() + .flatten() + .and_then(|v| v.parse().ok()) + .unwrap_or(0); + + AtomicUsize::new(value) + }) + } + + fn shown_count() -> usize { + Self::counter().load(Ordering::Relaxed) + } + + fn increment_count(cx: &mut AppContext) { + Self::set_count(Self::shown_count() + 1, cx) + } + + pub(crate) fn set_count(count: usize, cx: &mut AppContext) { + Self::counter().store(count, Ordering::Relaxed); + + db::write_and_log(cx, move || { + KEY_VALUE_STORE.write_kvp(SHOWN_COUNT_KEY.to_string(), format!("{}", count)) + }); + } + + fn dismiss(&mut self, cx: &mut AppContext) { + Self::set_count(NUMBER_OF_HINTS, cx) + } +} + +impl EventEmitter for MultibufferHint {} + +impl ToolbarItemView for MultibufferHint { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + if Self::shown_count() > NUMBER_OF_HINTS { + return ToolbarItemLocation::Hidden; + } + + let Some(active_pane_item) = active_pane_item else { + return ToolbarItemLocation::Hidden; + }; + + if active_pane_item.is_singleton(cx) { + return ToolbarItemLocation::Hidden; + } + + if self.shown_on.insert(active_pane_item.item_id()) { + Self::increment_count(cx) + } + + self.active_item = Some(active_pane_item.boxed_clone()); + ToolbarItemLocation::Secondary + } +} + +impl Render for MultibufferHint { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Some(active_item) = self.active_item.as_ref() else { + return Empty.into_any_element(); + }; + + if active_item.breadcrumbs(cx.theme(), cx).is_none() { + return Empty.into_any_element(); + } + + h_flex() + .px_2() + .justify_between() + .bg(cx.theme().status().info_background) + .rounded_md() + .child( + h_flex() + .gap_2() + .child(Label::new("You can edit results inline in multibuffers!")) + .child( + ButtonLike::new("open_docs") + .style(ButtonStyle::Transparent) + .child( + h_flex() + .gap_1() + .child(Label::new("Read moreā€¦")) + .child(Icon::new(IconName::ArrowUpRight).size(IconSize::Small)), + ) + .on_click(move |_event, cx| { + cx.open_url("https://zed.dev/docs/multibuffers") + }), + ), + ) + .child( + IconButton::new("dismiss", IconName::Close) + .style(ButtonStyle::Transparent) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _event, cx| { + this.dismiss(cx); + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Hidden, + )) + })) + .tooltip(move |cx| Tooltip::text("Dismiss this hint", cx)), + ) + .into_any_element() + } +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 49264809db..98892eb29f 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,12 +1,13 @@ mod base_keymap_picker; mod base_keymap_setting; +mod multibuffer_hint; use client::{telemetry::Telemetry, TelemetrySettings}; use db::kvp::KEY_VALUE_STORE; use gpui::{ - svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - ParentElement, Render, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, + InteractiveElement, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use settings::{Settings, SettingsStore}; use std::sync::Arc; @@ -19,6 +20,9 @@ use workspace::{ }; pub use base_keymap_setting::BaseKeymap; +pub use multibuffer_hint::*; + +actions!(welcome, [ResetHints]); pub const FIRST_OPEN: &str = "first_open"; @@ -30,6 +34,8 @@ pub fn init(cx: &mut AppContext) { let welcome_page = WelcomePage::new(workspace, cx); workspace.add_item_to_active_pane(Box::new(welcome_page), None, cx) }); + workspace + .register_action(|_workspace, _: &ResetHints, cx| MultibufferHint::set_count(0, cx)); }) .detach(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2380dcf626..50481c5e55 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -41,7 +41,7 @@ use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{asset_str, ResultExt}; use uuid::Uuid; use vim::VimModeSetting; -use welcome::BaseKeymap; +use welcome::{BaseKeymap, MultibufferHint}; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, @@ -495,6 +495,8 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewContext) { pane.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { + let multibuffer_hint = cx.new_view(|_| MultibufferHint::new()); + toolbar.add_item(multibuffer_hint, cx); let breadcrumbs = cx.new_view(|_| Breadcrumbs::new()); toolbar.add_item(breadcrumbs, cx); let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 487503ba88..430b83aed4 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -17,6 +17,7 @@ # Using Zed +- [Multibuffers](./multibuffers.md) - [Assistant Panel](./assistant-panel.md) - [Channels](./channels.md) - [Collaboration](./collaboration.md) diff --git a/docs/src/multibuffers.md b/docs/src/multibuffers.md new file mode 100644 index 0000000000..c35f6bbd00 --- /dev/null +++ b/docs/src/multibuffers.md @@ -0,0 +1,25 @@ +# Multibuffers + +One of the superpowers Zed gives you is the ability to edit multiple files simultaneously. When combined with multiple cursors, this makes wide-ranging refactors significantly faster. + +## Editing in a multibuffer + +Editing a multibuffer is the same as editing a normal file. Changes you make will be reflected in the open copies of that file in the rest of the editor, and you can save all files with `editor: Save` (bound to `cmd-s` on macOS, `ctrl-s` on Windows/Linux, or `:w` in Vim mode). + +When in a multibuffer, it is often useful to use multiple cursors to edit every file simultaneously. If you want to edit a few instances, you can select them with the mouse (`option-click` on macOS, `alt-click` on Window/Linux) or the keyboard. `cmd-d` on macOS, `ctrl-d` on Windows/Linux, or `gl` in Vim mode will select the next match of the word under the cursor. + +When you want to edit all matches you can select them by running the `editor: Select All Matches` command (`cmd-shift-l` on macOS, `ctrl-shift-l` on Windows/Linux, or `g a` in Vim mode). + +## Project search + +To start a search run the `pane: Toggle Search` command (`cmd-shift-f` on macOS, `ctrl-shift-f` on Windows/Linux, or `g/` in Vim mode). After the search has completed, the results will be shown in a new multibuffer. There will be one excerpt for each matching line across the whole project. + +## Diagnostics + +If you have a language server installed, the diagnostics pane can show you all errors across your project. You can open it by clicking on the icon in the status bar, or running the `diagnostcs: Deploy` command` ('cmd-shift-m` on macOS, `ctrl-shift-m` on Windows/Linux, or `:clist` in Vim mode). + +## Find References + +If you have a language server installed, you can find all references to the symbol under the cursor with the `editor: Find References` command (`cmd-click` on macOS, `ctrl-click` on Windows/Linux, or `g A` in Vim mode. + +Depending on your language server, commands like `editor: Go To Definition` and `editor: Go To Type Definition` will also open a multibuffer if there are multiple possible definitions.