Introducing multibuffers (#14668)

Co-Authored-By: Marshall <marshall@zed.dev>

Release Notes:

- Added a hint the first few times you open a multibuffer to explain
what is going on.

Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Conrad Irwin 2024-07-17 11:54:52 -06:00 committed by GitHub
parent 53bcc3649a
commit 4852e170ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 180 additions and 4 deletions

View File

@ -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()
}
}

View File

@ -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();

View File

@ -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);

View File

@ -17,6 +17,7 @@
# Using Zed
- [Multibuffers](./multibuffers.md)
- [Assistant Panel](./assistant-panel.md)
- [Channels](./channels.md)
- [Collaboration](./collaboration.md)

25
docs/src/multibuffers.md Normal file
View File

@ -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.