Reveal in all multi buffers (#64154)

Closes https://github.com/zed-industries/zed/issues/62909
Closes https://github.com/zed-industries/zed/issues/57396

Release Notes:

- Fixed project panel reveal not working in git diff-related multi
buffers
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-09-14 15:54:54 +00:00
1 parent f792c2d72d
commit 47b8ea5894
10 files changed
+145 -19

No files matched your search

+5
View File
@@ -118,6 +118,11 @@ impl AgentDiffPane {
cx.subscribe(&thread, |this, _thread, event, cx| {
this.handle_acp_thread_event(event, cx)
}),
cx.subscribe(&editor, |_, _, event: &EditorEvent, cx| {
if event == &(EditorEvent::SelectionsChanged { local: true }) {
cx.emit(event.clone())
}
}),
],
multibuffer,
editor,
+8 -2
View File
@@ -402,6 +402,7 @@ impl DiffMultibuffer {
if !editor.focus_handle(cx).contains_focused(window, cx) {
return;
}
cx.emit(event.clone());
let Some(project_path) = self.active_project_path(cx) else {
return;
};
@@ -720,12 +721,17 @@ impl DiffMultibuffer {
pub(crate) fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
let editor = self.editor.read(cx).focused_editor().read(cx);
let multibuffer = editor.buffer().read(cx);
let focused_multibuffer = editor.buffer().read(cx);
let position = editor.selections.newest_anchor().head();
let focused_snapshot = focused_multibuffer.snapshot(cx);
let (focused_anchor, _) = focused_snapshot.anchor_to_buffer_anchor(position)?;
let path = focused_snapshot.path_for_buffer(focused_anchor.buffer_id)?;
let multibuffer = self.multibuffer.read(cx);
let position = multibuffer.location_for_path(path, cx)?;
let snapshot = multibuffer.snapshot(cx);
let (text_anchor, _) = snapshot.anchor_to_buffer_anchor(position)?;
let buffer = multibuffer.buffer(text_anchor.buffer_id)?;
let file = buffer.read(cx).file()?;
Some(ProjectPath {
worktree_id: file.worktree_id(cx),
+16 -4
View File
@@ -9723,8 +9723,8 @@ mod tests {
use util::rel_path::rel_path;
use workspace::{
ActivatePaneLeft, ActivatePaneRight, MultiWorkspace, ToolbarItemEvent, ToolbarItemLocation,
item::test::TestItem,
ActivatePaneLeft, ActivatePaneRight, ItemHandle as _, MultiWorkspace, ToolbarItemEvent,
ToolbarItemLocation, item::test::TestItem,
};
use super::*;
@@ -11380,6 +11380,11 @@ mod tests {
.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
.unwrap();
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
let project_path = project.read_with(&cx, |project, cx| {
project
.find_project_path(path!("/project/partial.rs"), cx)
.expect("partial.rs should have a project path")
});
cx.update(|_window, cx| {
SettingsStore::update_global(cx, |store, cx| {
@@ -11415,7 +11420,10 @@ mod tests {
cx.run_until_parked();
workspace.read_with(&cx, |workspace, cx| {
assert!(workspace.active_item_as::<StagedDiff>(cx).is_some());
let staged_diff = workspace
.active_item_as::<StagedDiff>(cx)
.expect("StagedDiff should be active");
assert_eq!(staged_diff.project_path(cx), Some(project_path.clone()));
assert_eq!(workspace.items_of_type::<StagedDiff>(cx).count(), 1);
assert_eq!(workspace.items_of_type::<UnstagedDiff>(cx).count(), 0);
assert_eq!(workspace.items_of_type::<ProjectDiff>(cx).count(), 0);
@@ -11440,6 +11448,7 @@ mod tests {
let solo_diff = workspace
.active_item_as::<SoloDiffView>(cx)
.expect("SoloDiffView should be active");
assert_eq!(solo_diff.project_path(cx), Some(project_path.clone()));
let searchable = solo_diff
.read(cx)
.as_searchable(&solo_diff, cx)
@@ -11484,7 +11493,10 @@ mod tests {
cx.run_until_parked();
workspace.read_with(&cx, |workspace, cx| {
assert!(workspace.active_item_as::<UnstagedDiff>(cx).is_some());
let unstaged_diff = workspace
.active_item_as::<UnstagedDiff>(cx)
.expect("UnstagedDiff should be active");
assert_eq!(unstaged_diff.project_path(cx), Some(project_path));
assert_eq!(workspace.items_of_type::<StagedDiff>(cx).count(), 1);
assert_eq!(workspace.items_of_type::<UnstagedDiff>(cx).count(), 1);
assert_eq!(workspace.items_of_type::<ProjectDiff>(cx).count(), 0);
+13 -2
View File
@@ -6,7 +6,7 @@ use editor::{
use git_ui_core::file_diff_view::build_buffer_diff;
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Font,
IntoElement, Render, SharedString, Task, Window,
IntoElement, Render, SharedString, Subscription, Task, Window,
};
use language::{Buffer, Capability, HighlightedText, OffsetRangeExt};
use multi_buffer::PathKey;
@@ -28,6 +28,7 @@ use workspace::{
pub struct MultiDiffView {
editor: Entity<Editor>,
file_count: usize,
_editor_event_subscription: Subscription,
}
struct Entry {
@@ -204,7 +205,17 @@ impl MultiDiffView {
editor
});
Self { editor, file_count }
let editor_event_subscription = cx.subscribe(&editor, |_, _, event: &EditorEvent, cx| {
if event == &(EditorEvent::SelectionsChanged { local: true }) {
cx.emit(event.clone())
}
});
Self {
editor,
file_count,
_editor_event_subscription: editor_event_subscription,
}
}
fn title(&self) -> SharedString {
+66 -3
View File
@@ -70,6 +70,7 @@ pub struct ProjectDiff {
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
diff: Entity<DiffMultibuffer>,
_diff_event_subscription: Subscription,
_diff_observation: Subscription,
}
@@ -266,12 +267,18 @@ impl ProjectDiff {
workspace: Entity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let observation = cx.observe(&diff, |_, _, cx| cx.notify());
let diff_event_subscription = cx.subscribe(&diff, |_, _, event: &EditorEvent, cx| {
if event == &(EditorEvent::SelectionsChanged { local: true }) {
cx.emit(event.clone())
}
});
let diff_observation = cx.observe(&diff, |_, _, cx| cx.notify());
Self {
project,
workspace: workspace.downgrade(),
diff,
_diff_observation: observation,
_diff_event_subscription: diff_event_subscription,
_diff_observation: diff_observation,
}
}
@@ -1560,8 +1567,20 @@ mod tests {
);
let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
let (created_entry_id, changed_entry_id) = project.read_with(cx, |project, cx| {
let entry_id = |path| {
let project_path = project
.find_project_path(path, cx)
.expect("diff path should resolve");
project
.entry_for_path(&project_path, cx)
.expect("diff path should have a project entry")
.id
};
(entry_id(path!("/a/a.txt")), entry_id(path!("/a/b.txt")))
});
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.run_until_parked();
@@ -1591,6 +1610,10 @@ mod tests {
ˇcreated
"
));
assert_eq!(
project.read_with(&cx.cx, |project, _| project.active_entry()),
Some(created_entry_id)
);
cx.dispatch_action(editor::actions::GoToPreviousHunk);
@@ -1605,6 +1628,10 @@ mod tests {
created
"
));
assert_eq!(
project.read_with(&cx.cx, |project, _| project.active_entry()),
None
);
cx.dispatch_action(editor::actions::GoToPreviousHunk);
@@ -1619,6 +1646,42 @@ mod tests {
created
"
));
assert_eq!(
project.read_with(&cx.cx, |project, _| project.active_entry()),
Some(changed_entry_id)
);
cx.cx.update(|_, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings.editor.diff_view_style = Some(DiffViewStyle::Split);
});
});
});
cx.cx.run_until_parked();
let lhs_editor = item.read_with(&cx.cx, |item, cx| {
item.editor(cx)
.read(cx)
.lhs_editor()
.cloned()
.expect("split diff should have a left editor")
});
let mut lhs_cx = EditorTestContext::for_editor_in(lhs_editor, &mut cx.cx).await;
lhs_cx.update_editor(|editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([multi_buffer::Anchor::Max..multi_buffer::Anchor::Max]);
});
});
lhs_cx.update_editor(|editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([multi_buffer::Anchor::Min..multi_buffer::Anchor::Min]);
});
});
assert_eq!(
project.read_with(&lhs_cx.cx, |project, _| project.active_entry()),
Some(changed_entry_id)
);
}
#[gpui::test]
+5 -1
View File
@@ -18,7 +18,7 @@ use gpui::{
use language::{Anchor, Buffer, HighlightedText, OffsetRangeExt as _, Point};
use multi_buffer::{MultiBuffer, PathKey, excerpt_context_lines};
use project::{
Project,
Project, ProjectPath,
git_store::{Repository, RepositoryId},
};
use settings::{Settings, SettingsStore, StatusStyle};
@@ -453,6 +453,10 @@ impl Item for SoloDiffView {
self.editor.for_each_project_item(cx, f)
}
fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
self.editor.read(cx).active_project_path(cx)
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
+4
View File
@@ -326,6 +326,10 @@ impl Item for StagedDiff {
self.diff.read(cx).for_each_project_item(cx, f);
}
fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
self.diff.read(cx).active_project_path(cx)
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
+4
View File
@@ -370,6 +370,10 @@ impl Item for UnstagedDiff {
self.diff.read(cx).for_each_project_item(cx, f);
}
fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
self.diff.read(cx).active_project_path(cx)
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
@@ -6475,6 +6475,21 @@ async fn test_autoreveal_follows_multibuffer_selection(cx: &mut gpui::TestAppCon
"Moving the cursor into a different excerpt buffer should reveal that buffer's entry"
);
cx.dispatch_action(workspace::RevealInProjectPanel::default());
cx.run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"v project_root",
" v dir_1",
" file_1.py",
" v dir_2",
" file_2.py <== selected <== marked",
],
"Explicit reveal should use the project path for the excerpt under the cursor"
);
// Wrappers re-emit inner-editor events through `to_item_events`, so a
// benign `TitleChanged` (e.g. diagnostic summary updates) ultimately
// reaches `Workspace::active_item_path_changed`. The active path should be
+9 -7
View File
@@ -4554,16 +4554,18 @@ impl Render for Pane {
))
.on_action(cx.listener(
|pane: &mut Self, action: &RevealInProjectPanel, _window, cx| {
let active_item = pane.active_item();
let entry_id = active_item.as_ref().and_then(|item| {
action
.entry_id
.map(ProjectEntryId::from_proto)
.or_else(|| item.project_entry_ids(cx).first().copied())
});
let entry_id = action.entry_id.map(ProjectEntryId::from_proto);
let active_project_path =
pane.active_item().and_then(|item| item.project_path(cx));
pane.project
.update(cx, |project, cx| {
let entry_id = entry_id.or_else(|| {
active_project_path
.as_ref()
.and_then(|path| project.entry_for_path(path, cx))
.map(|entry| entry.id)
});
if let Some(entry_id) = entry_id
&& project
.worktree_for_entry(entry_id, cx)