Fix pane::RevealInProjectPanel to focus/open project panel for non-project buffers (#51246)

Update how `workspace::pane::Pane` handles the `RevealInProjectPanel`
action so as to display a notification when the user attempts to reveal
an unsaved buffer or a file that does not belong to any of the open
projects.

Closes #23967 

Release Notes:

- Update `pane: reveal in project panel` to display a notification when
the user attempts to use it with an unsaved buffer or a file that is not
part of the open projects

---------

Signed-off-by: Pratik Karki <pratik@prertik.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
Pratik Karki 2026-04-07 17:10:55 +05:45 committed by GitHub
parent 93438829c7
commit 1dc3bb90e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 203 additions and 9 deletions

View file

@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
use util::{path, paths::PathStyle, rel_path::rel_path};
use workspace::{
AppState, ItemHandle, MultiWorkspace, Pane, Workspace,
item::{Item, ProjectItem},
item::{Item, ProjectItem, test::TestItem},
register_project_item,
};
@ -6015,6 +6015,150 @@ async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_reveal_in_project_panel_notifications(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/workspace",
json!({
"README.md": ""
}),
)
.await;
let project = Project::test(fs.clone(), ["/workspace".as_ref()], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
// Ensure that, attempting to run `pane: reveal in project panel` without
// any active item does nothing, i.e., does not focus the project panel but
// it also does not show a notification.
cx.dispatch_action(workspace::RevealInProjectPanel::default());
cx.run_until_parked();
panel.update_in(cx, |panel, window, cx| {
assert!(
!panel.focus_handle(cx).is_focused(window),
"Project panel should not be focused after attempting to reveal an invisible worktree entry"
);
panel.workspace.update(cx, |workspace, cx| {
assert!(
workspace.active_item(cx).is_none(),
"Workspace should not have an active item"
);
assert_eq!(
workspace.notification_ids(),
vec![],
"No notification should be shown when there's no active item"
);
}).unwrap();
});
// Create a file in a different folder than the one in the project so we can
// later open it and ensure that, attempting to reveal it in the project
// panel shows a notification and does not focus the project panel.
fs.insert_tree(
"/external",
json!({
"file.txt": "External File",
}),
)
.await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/external/file.txt", false, cx)
})
.await
.unwrap();
workspace
.update_in(cx, |workspace, window, cx| {
let worktree_id = worktree.read(cx).id();
let path = rel_path("").into();
let project_path = ProjectPath { worktree_id, path };
workspace.open_path(project_path, None, true, window, cx)
})
.await
.unwrap();
cx.run_until_parked();
cx.dispatch_action(workspace::RevealInProjectPanel::default());
cx.run_until_parked();
panel.update_in(cx, |panel, window, cx| {
assert!(
!panel.focus_handle(cx).is_focused(window),
"Project panel should not be focused after attempting to reveal an invisible worktree entry"
);
panel.workspace.update(cx, |workspace, cx| {
assert!(
workspace.active_item(cx).is_some(),
"Workspace should have an active item"
);
let notification_ids = workspace.notification_ids();
assert_eq!(
notification_ids.len(),
1,
"A notification should be shown when trying to reveal an invisible worktree entry"
);
workspace.dismiss_notification(&notification_ids[0], cx);
assert_eq!(
workspace.notification_ids().len(),
0,
"No notifications should be left after dismissing"
);
}).unwrap();
});
// Create an empty buffer so we can ensure that, attempting to reveal it in
// the project panel shows a notification and does not focus the project
// panel.
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
pane.update_in(cx, |pane, window, cx| {
let item = cx.new(|cx| TestItem::new(cx).with_label("Unsaved buffer"));
pane.add_item(Box::new(item), false, false, None, window, cx);
});
cx.dispatch_action(workspace::RevealInProjectPanel::default());
cx.run_until_parked();
panel.update_in(cx, |panel, window, cx| {
assert!(
!panel.focus_handle(cx).is_focused(window),
"Project panel should not be focused after attempting to reveal an unsaved buffer"
);
panel
.workspace
.update(cx, |workspace, cx| {
assert!(
workspace.active_item(cx).is_some(),
"Workspace should have an active item"
);
let notification_ids = workspace.notification_ids();
assert_eq!(
notification_ids.len(),
1,
"A notification should be shown when trying to reveal an unsaved buffer"
);
})
.unwrap();
});
}
#[gpui::test]
async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
init_test(cx);

View file

@ -10,7 +10,10 @@ use crate::{
TabContentParams, TabTooltipContent, WeakItemHandle,
},
move_item,
notifications::NotifyResultExt,
notifications::{
NotificationId, NotifyResultExt, show_app_notification,
simple_message_notification::MessageNotification,
},
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, FocusFollowsMouse, TabBarSettings, WorkspaceSettings},
};
@ -4400,17 +4403,64 @@ impl Render for Pane {
))
.on_action(
cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
let Some(active_item) = pane.active_item() else {
return;
};
let entry_id = action
.entry_id
.map(ProjectEntryId::from_proto)
.or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
if let Some(entry_id) = entry_id {
pane.project
.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id))
})
.ok();
.or_else(|| active_item.project_entry_ids(cx).first().copied());
let show_reveal_error_toast = |display_name: &str, cx: &mut App| {
let notification_id = NotificationId::unique::<RevealInProjectPanel>();
let message = SharedString::from(format!(
"\"{display_name}\" is not part of any open projects."
));
show_app_notification(notification_id, cx, move |cx| {
let message = message.clone();
cx.new(|cx| MessageNotification::new(message, cx))
});
};
let Some(entry_id) = entry_id else {
// When working with an unsaved buffer, display a toast
// informing the user that the buffer is not present in
// any of the open projects and stop execution, as we
// don't want to open the project panel.
let display_name = active_item
.tab_tooltip_text(cx)
.unwrap_or_else(|| active_item.tab_content_text(0, cx));
return show_reveal_error_toast(&display_name, cx);
};
// We'll now check whether the entry belongs to a visible
// worktree and, if that's not the case, it means the user
// is interacting with a file that does not belong to any of
// the open projects, so we'll show a toast informing them
// of this and stop execution.
let display_name = pane
.project
.read_with(cx, |project, cx| {
project
.worktree_for_entry(entry_id, cx)
.filter(|worktree| !worktree.read(cx).is_visible())
.map(|worktree| worktree.read(cx).root_name_str().to_string())
})
.ok()
.flatten();
if let Some(display_name) = display_name {
return show_reveal_error_toast(&display_name, cx);
}
pane.project
.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id))
})
.log_err();
}),
)
.on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {