mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
gpui: Add support for NSWindow's representedFilename (#48029)
This sets the accessibility document property (AXDocument) of the window, which other apps can use to understand what file the current window represents. Document-based apps on macOS are generally expected to set this property. The document path is set via `Window::set_document_path()` which calls `setRepresentedFilename:` on the underlying NSWindow. The workspace updates this path in `update_window_title()` whenever the active item or project structure changes. For tests, instead of trying to somehow assemble a proper NSWindow, we store the document path on the window mock used for testing and check its value. Motivation: I am the developer of Timing, an automatic time tracking app for Mac. Timing uses the `AXDocument` property (a standard property of most document-based app windows on macOS) to understand what document the user is working on. With this change, Timing is able to understand which directory the user is working in. Without this, Timing would only record the window title, i.e. the filename without information about the containing directory. I've had several users ask for better support for Zed. Here's a screenshot of the macOS Accessibility Inspector showing the `AXDocument` property with this change. The UI of Zed itself does not change. However, in my dev build of Zed, the traffic lights are a bit too large and misaligned. However, this happens even when building `main`, so I assume it’s unrelated to my changes. <img width="1370" height="1162" alt="Screenshot 2026-01-30 at 16 18 25" src="https://github.com/user-attachments/assets/dc260252-91fb-41e1-97a9-e6fb843c6a70" /> Release Notes: - Set the represented filename property of windows on macOS --------- Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
parent
9adb4ea63e
commit
385f6134bb
6 changed files with 135 additions and 2 deletions
|
|
@ -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<std::path::PathBuf> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<dyn FnMut()>) {}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub(crate) struct TestWindowState {
|
|||
display: Rc<dyn PlatformDisplay>,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) edited: bool,
|
||||
pub(crate) document_path: Option<std::path::PathBuf>,
|
||||
platform: Weak<TestPlatform>,
|
||||
// TODO: Replace with `Rc`
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Rc<dyn PlatformDisplay>> {
|
||||
cx.platform
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<TestProjectItem> {
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue