diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index bef45baa5d8..d6ec9ce81e1 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -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, diff --git a/crates/git_ui/src/diff_multibuffer.rs b/crates/git_ui/src/diff_multibuffer.rs index b20ed284035..dc519737594 100644 --- a/crates/git_ui/src/diff_multibuffer.rs +++ b/crates/git_ui/src/diff_multibuffer.rs @@ -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 { 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), diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 11b74d9d5a8..7f10a4a8829 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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::(cx).is_some()); + let staged_diff = workspace + .active_item_as::(cx) + .expect("StagedDiff should be active"); + assert_eq!(staged_diff.project_path(cx), Some(project_path.clone())); assert_eq!(workspace.items_of_type::(cx).count(), 1); assert_eq!(workspace.items_of_type::(cx).count(), 0); assert_eq!(workspace.items_of_type::(cx).count(), 0); @@ -11440,6 +11448,7 @@ mod tests { let solo_diff = workspace .active_item_as::(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::(cx).is_some()); + let unstaged_diff = workspace + .active_item_as::(cx) + .expect("UnstagedDiff should be active"); + assert_eq!(unstaged_diff.project_path(cx), Some(project_path)); assert_eq!(workspace.items_of_type::(cx).count(), 1); assert_eq!(workspace.items_of_type::(cx).count(), 1); assert_eq!(workspace.items_of_type::(cx).count(), 0); diff --git a/crates/git_ui/src/multi_diff_view.rs b/crates/git_ui/src/multi_diff_view.rs index 92129897ae2..71042ba1b1d 100644 --- a/crates/git_ui/src/multi_diff_view.rs +++ b/crates/git_ui/src/multi_diff_view.rs @@ -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, 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 { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 42c0896a315..5ef3c1a808b 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -70,6 +70,7 @@ pub struct ProjectDiff { project: Entity, workspace: WeakEntity, diff: Entity, + _diff_event_subscription: Subscription, _diff_observation: Subscription, } @@ -266,12 +267,18 @@ impl ProjectDiff { workspace: Entity, cx: &mut Context, ) -> 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::(|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] diff --git a/crates/git_ui/src/solo_diff_view.rs b/crates/git_ui/src/solo_diff_view.rs index bc54d6e4ac0..6d4696efe05 100644 --- a/crates/git_ui/src/solo_diff_view.rs +++ b/crates/git_ui/src/solo_diff_view.rs @@ -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 { + self.editor.read(cx).active_project_path(cx) + } + fn set_nav_history( &mut self, nav_history: ItemNavHistory, diff --git a/crates/git_ui/src/staged_diff.rs b/crates/git_ui/src/staged_diff.rs index 77bf62d100c..d86034961b3 100644 --- a/crates/git_ui/src/staged_diff.rs +++ b/crates/git_ui/src/staged_diff.rs @@ -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 { + self.diff.read(cx).active_project_path(cx) + } + fn set_nav_history( &mut self, nav_history: ItemNavHistory, diff --git a/crates/git_ui/src/unstaged_diff.rs b/crates/git_ui/src/unstaged_diff.rs index b51271fb2c9..bc6e1d3096c 100644 --- a/crates/git_ui/src/unstaged_diff.rs +++ b/crates/git_ui/src/unstaged_diff.rs @@ -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 { + self.diff.read(cx).active_project_path(cx) + } + fn set_nav_history( &mut self, nav_history: ItemNavHistory, diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 1eb129e53e1..8d298b30daa 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -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 diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 56338cbaa0f..25771a8ee1f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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)