diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index d8f459df3c..99a64b54e2 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -735,6 +735,16 @@ impl VisualTestContext { self.cx.test_window(self.window).0.lock().title.clone() } + /// Read the document path off the window (set by `Window#set_document_path`) + pub fn document_path(&mut self) -> Option { + self.cx + .test_window(self.window) + .0 + .lock() + .document_path + .clone() + } + /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")` /// Automatically runs until parked. pub fn simulate_keystrokes(&mut self, keystrokes: &str) { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 59957a9c6a..6cd8e9cd8d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -651,6 +651,7 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { false } fn set_edited(&mut self, _edited: bool) {} + fn set_document_path(&self, _path: Option<&std::path::Path>) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} fn on_move_tab_to_new_window(&self, _callback: Box) {} diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index ac98027b52..2b5399ca98 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -20,6 +20,7 @@ pub(crate) struct TestWindowState { display: Rc, pub(crate) title: Option, pub(crate) edited: bool, + pub(crate) document_path: Option, platform: Weak, // TODO: Replace with `Rc` sprite_atlas: Arc, @@ -75,6 +76,7 @@ impl TestWindow { renderer, title: Default::default(), edited: false, + document_path: None, should_close_handler: None, hit_test_window_control_callback: None, input_callback: None, @@ -230,6 +232,10 @@ impl PlatformWindow for TestWindow { self.0.lock().edited = edited; } + fn set_document_path(&self, path: Option<&std::path::Path>) { + self.0.lock().document_path = path.map(|p| p.to_path_buf()); + } + fn show_character_palette(&self) { unimplemented!() } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ab195c6294..49bdf2e65c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2214,6 +2214,12 @@ impl Window { self.platform_window.set_edited(edited); } + /// Set the path of the file this window represents. + /// On macOS, this sets the window's accessibility document property (AXDocument). + pub fn set_document_path(&self, path: Option<&std::path::Path>) { + self.platform_window.set_document_path(path); + } + /// Determine the display on which the window is visible. pub fn display(&self, cx: &App) -> Option> { cx.platform diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 7ce5badb2b..d6e1d296ac 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -1439,6 +1439,14 @@ impl PlatformWindow for MacWindow { self.0.lock().move_traffic_light(); } + fn set_document_path(&self, path: Option<&std::path::Path>) { + unsafe { + let window = self.0.lock().native_window; + let filename = path.map_or(ns_string(""), |p| ns_string(&p.to_string_lossy())); + let _: () = msg_send![window, setRepresentedFilename: filename]; + } + } + fn show_character_palette(&self) { let this = self.0.lock(); let window = this.native_window; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c88aec84e2..a6d966bb23 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5840,7 +5840,9 @@ impl Workspace { title = "empty project".to_string(); } - if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { + let active_project_path = self.active_item(cx).and_then(|item| item.project_path(cx)); + + if let Some(path) = active_project_path.as_ref() { let filename = path.path.file_name().or_else(|| { Some( project @@ -5862,6 +5864,11 @@ impl Workspace { title.push_str(" ↗"); } + let document_path = active_project_path + .as_ref() + .and_then(|path| project.absolute_path(path, cx)); + window.set_document_path(document_path.as_deref()); + if let Some(last_title) = self.last_window_title.as_ref() && &title == last_title { @@ -10880,7 +10887,7 @@ mod tests { DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext, UpdateGlobal, VisualTestContext, px, }; - use project::{Project, ProjectEntryId}; + use project::{Project, ProjectEntryId, WorktreeId}; use serde_json::json; use settings::SettingsStore; use util::path; @@ -11029,6 +11036,86 @@ mod tests { assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt")); } + #[gpui::test] + async fn test_document_path_updates_with_active_item(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/root", + json!({ + "one.txt": "", + "two.txt": "", + }), + ) + .await; + + let project = Project::test(fs, ["root".as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let item1 = cx.new(|cx| { + TestItem::new(cx).with_project_items(&[new_test_project_item( + 1, + "one.txt", + worktree_id, + cx, + )]) + }); + let item2 = cx.new(|cx| { + TestItem::new(cx).with_project_items(&[new_test_project_item( + 2, + "two.txt", + worktree_id, + cx, + )]) + }); + + // Initially no document path + assert_eq!(cx.document_path(), None); + + // Add an item - document path should be set + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx) + }); + assert_eq!( + cx.document_path(), + Some(std::path::PathBuf::from("root/one.txt")) + ); + + // Add a second item - document path should update + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx) + }); + assert_eq!( + cx.document_path(), + Some(std::path::PathBuf::from("root/two.txt")) + ); + + // Close the active item - document path should revert to first item + pane.update_in(cx, |pane, window, cx| { + pane.close_active_item(&Default::default(), window, cx) + }) + .await + .unwrap(); + assert_eq!( + cx.document_path(), + Some(std::path::PathBuf::from("root/one.txt")) + ); + + // Close all items - document path should be cleared + pane.update_in(cx, |pane, window, cx| { + pane.close_active_item(&Default::default(), window, cx) + }) + .await + .unwrap(); + assert_eq!(cx.document_path(), None); + } + #[gpui::test] async fn test_close_window(cx: &mut TestAppContext) { init_test(cx); @@ -15321,6 +15408,21 @@ mod tests { item } + fn new_test_project_item( + id: u64, + path: &str, + worktree_id: WorktreeId, + cx: &mut App, + ) -> Entity { + let item = TestProjectItem::new(id, path, cx); + item.update(cx, |item, _| { + if let Some(ref mut project_path) = item.project_path { + project_path.worktree_id = worktree_id; + } + }); + item + } + #[gpui::test] async fn test_zoomed_panel_without_pane_preserved_on_center_focus( cx: &mut gpui::TestAppContext,