Detailed changes
@@ -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<EntityId>,
+ active_item: Option<Box<dyn ItemHandle>>,
+}
+
+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<AtomicUsize> = 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<ToolbarItemEvent> for MultibufferHint {}
+
+impl ToolbarItemView for MultibufferHint {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) -> 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()
+ }
+}
@@ -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();
@@ -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<AppState>, cx: &mut AppContext) {
fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
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);
@@ -17,6 +17,7 @@
# Using Zed
+- [Multibuffers](./multibuffers.md)
- [Assistant Panel](./assistant-panel.md)
- [Channels](./channels.md)
- [Collaboration](./collaboration.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.