workspace: Treat remote roots as directories (#54733)

## Summary

- Add a regression test covering reopening an already-open remote
workspace root inside the reused workspace path
- Use the resolved worktree entry instead of the local filesystem when
`Workspace::open_paths` decides whether a reused path is a directory
- This fixes the follow-on case where reopening an already-open SSH
workspace root could show `Error: opening project path ...` because the
remote root was being treated like a local file open

I kept this fix in `Workspace::open_paths` rather than adding another
remote-specific branch higher up, because the bug was specifically in
the generic reused-workspace directory check.

## Testing

- `cargo test -p recent_projects
test_reopen_existing_remote_root_treats_root_as_directory --
--nocapture`
- `cargo test -p recent_projects
test_reuse_existing_remote_workspace_window_with_tilde_path --
--nocapture`
- `cargo test -p recent_projects
test_reuse_existing_remote_workspace_window -- --nocapture`

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] Tests cover the new/changed behaviour
- [x] Performance impact has been considered and is acceptable

Related to #30829

Release Notes:

- Fixed reopening an already-open SSH workspace root showing an `opening
project path` error instead of treating the root as a directory
This commit is contained in:
Jake Nelson authored and GitHub committed 2026-06-01 07:13:03 +00:00
1 parent f87b2446d8
commit fd7b2866c5
2 files changed
+112 -2

No files matched your search

@@ -734,6 +734,101 @@ mod tests {
);
}
#[gpui::test]
async fn test_reopen_existing_remote_root_treats_root_as_directory(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let app_state = init_test(cx);
let executor = cx.executor();
cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
server_cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
let remote_home = paths::home_dir();
let canonical_project_path = remote_home.join("remote-reopen-root-project");
remote_fs
.insert_tree(
&canonical_project_path,
json!({
"src": {
"main.rs": "fn main() {}",
},
"README.md": "# Test Project",
}),
)
.await;
server_cx.update(HeadlessProject::init);
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor()));
let proxy = Arc::new(ExtensionHostProxy::new());
let _headless = server_cx.new(|cx| {
HeadlessProject::new(
HeadlessAppState {
session: server_session,
fs: remote_fs.clone(),
http_client,
node_runtime,
languages,
extension_host_proxy: proxy,
startup_time: std::time::Instant::now(),
},
false,
cx,
)
});
drop(connect_guard);
let mut async_cx = cx.to_async();
let window = open_remote_project(
opts,
vec![canonical_project_path.clone()],
app_state,
workspace::OpenOptions::default(),
&mut async_cx,
)
.await
.expect("initial open_remote_project should succeed");
executor.run_until_parked();
let open_results = window
.update(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
workspace.open_paths(
vec![canonical_project_path.clone()],
workspace::OpenOptions {
visible: Some(workspace::OpenVisible::All),
..Default::default()
},
None,
window,
cx,
)
})
})
.unwrap()
.await;
assert_eq!(open_results.len(), 1, "should return one open result");
assert!(
open_results[0].is_none(),
"reopening a remote root directory should not try to open it as a file"
);
}
#[gpui::test]
async fn test_reconnect_when_server_not_running(
cx: &mut TestAppContext,
+17 -2
View File
@@ -3672,8 +3672,23 @@ impl Workspace {
let fs = fs.clone();
let pane = pane.clone();
let task = cx.spawn(async move |cx| {
let (_worktree, project_path) = project_path?;
if fs.is_dir(&abs_path).await {
let (worktree, project_path) = project_path?;
let (entry_is_directory, worktree_is_local) =
worktree.read_with(cx, |worktree, _| {
let entry = if project_path.path.as_unix_str().is_empty() {
worktree.root_entry()
} else {
worktree.entry_for_path(&project_path.path)
};
(entry.map(|entry| entry.is_dir()), worktree.is_local())
});
let is_directory = match entry_is_directory {
Some(is_directory) => is_directory,
None if worktree_is_local => fs.is_dir(&abs_path).await,
None => false,
};
if is_directory {
// Opening a directory should not race to update the active entry.
// We'll select/reveal a deterministic final entry after all paths finish opening.
None