Reuse existing windows when opening files in remote workspaces (#48891)

Closes https://github.com/zed-industries/zed/issues/42366

- [ ] Tests or screenshots needed?
- [X] Code Reviewed
- [X] Manual QA

Release Notes:

- Opening files with the Zed CLI will reuse existing windows for remote
workspaces.
This commit is contained in:
John Tur 2026-02-10 17:48:54 -05:00 committed by GitHub
parent aba74e6ca3
commit d2c922b815
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 395 additions and 238 deletions

View file

@ -2325,14 +2325,12 @@ impl Project {
pub fn visibility_for_paths(
&self,
paths: &[PathBuf],
metadatas: &[Metadata],
exclude_sub_dirs: bool,
cx: &App,
) -> Option<bool> {
paths
.iter()
.zip(metadatas)
.map(|(path, metadata)| self.visibility_for_path(path, metadata, exclude_sub_dirs, cx))
.map(|path| self.visibility_for_path(path, exclude_sub_dirs, cx))
.max()
.flatten()
}
@ -2340,17 +2338,26 @@ impl Project {
pub fn visibility_for_path(
&self,
path: &Path,
metadata: &Metadata,
exclude_sub_dirs: bool,
cx: &App,
) -> Option<bool> {
let path = SanitizedPath::new(path).as_path();
let path_style = self.path_style(cx);
self.worktrees(cx)
.filter_map(|worktree| {
let worktree = worktree.read(cx);
let abs_path = worktree.as_local()?.abs_path();
let contains = path == abs_path.as_ref()
|| (path.starts_with(abs_path) && (!exclude_sub_dirs || !metadata.is_dir));
let abs_path = worktree.abs_path();
let relative_path = path_style.strip_prefix(path, abs_path.as_ref());
let is_dir = relative_path
.as_ref()
.and_then(|p| worktree.entry_for_path(p))
.is_some_and(|e| e.is_dir());
// Don't exclude the worktree root itself, only actual subdirectories
let is_subdir = relative_path
.as_ref()
.is_some_and(|p| !p.as_ref().as_unix_str().is_empty());
let contains =
relative_path.is_some() && (!exclude_sub_dirs || !is_dir || !is_subdir);
contains.then(|| worktree.is_visible())
})
.max()

View file

@ -223,8 +223,8 @@ impl WorktreeStore {
let abs_path = SanitizedPath::new(abs_path.as_ref());
for tree in self.worktrees() {
let path_style = tree.read(cx).path_style();
if let Ok(relative_path) = abs_path.as_ref().strip_prefix(tree.read(cx).abs_path())
&& let Ok(relative_path) = RelPath::new(relative_path, path_style)
if let Some(relative_path) =
path_style.strip_prefix(abs_path.as_ref(), tree.read(cx).abs_path().as_ref())
{
return Some((tree.clone(), relative_path.into_arc()));
}

View file

@ -11,7 +11,7 @@ mod wsl_picker;
use remote::RemoteConnectionOptions;
pub use remote_connection::{RemoteConnectionModal, connect};
pub use remote_connections::open_remote_project;
pub use remote_connections::{navigate_to_positions, open_remote_project};
use disconnected_overlay::DisconnectedOverlay;
use fuzzy::{StringMatch, StringMatchCandidate};

View file

@ -8,7 +8,7 @@ use askpass::EncryptedPassword;
use editor::Editor;
use extension_host::ExtensionStore;
use futures::{FutureExt as _, channel::oneshot, select};
use gpui::{AppContext, AsyncApp, PromptLevel};
use gpui::{AppContext, AsyncApp, PromptLevel, WindowHandle};
use language::Point;
use project::trusted_worktrees;
@ -19,7 +19,9 @@ use remote::{
pub use settings::SshConnection;
use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
use util::paths::PathWithPosition;
use workspace::{AppState, Workspace};
use workspace::{
AppState, OpenOptions, SerializedWorkspaceLocation, Workspace, find_existing_workspace,
};
pub use remote_connection::{
RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,
@ -131,6 +133,62 @@ pub async fn open_remote_project(
cx: &mut AsyncApp,
) -> Result<()> {
let created_new_window = open_options.replace_window.is_none();
let (existing, open_visible) = find_existing_workspace(
&paths,
&open_options,
&SerializedWorkspaceLocation::Remote(connection_options.clone()),
cx,
)
.await;
if let Some(existing) = existing {
let remote_connection = existing
.update(cx, |workspace, _, cx| {
workspace
.project()
.read(cx)
.remote_client()
.and_then(|client| client.read(cx).remote_connection())
})?
.ok_or_else(|| anyhow::anyhow!("no remote connection for existing remote workspace"))?;
let (resolved_paths, paths_with_positions) =
determine_paths_with_positions(&remote_connection, paths).await;
let open_results = existing
.update(cx, |workspace, window, cx| {
window.activate_window();
workspace.open_paths(
resolved_paths,
OpenOptions {
visible: Some(open_visible),
..Default::default()
},
None,
window,
cx,
)
})?
.await;
_ = existing.update(cx, |workspace, _, cx| {
for item in open_results.iter().flatten() {
if let Err(e) = item {
workspace.show_error(&e, cx);
}
}
});
let items = open_results
.into_iter()
.map(|r| r.and_then(|r| r.ok()))
.collect::<Vec<_>>();
navigate_to_positions(&existing, items, &paths_with_positions, cx);
return Ok(());
}
let window = if let Some(window) = open_options.replace_window {
window
} else {
@ -337,29 +395,7 @@ pub async fn open_remote_project(
}
Ok(items) => {
for (item, path) in items.into_iter().zip(paths_with_positions) {
let Some(item) = item else {
continue;
};
let Some(row) = path.row else {
continue;
};
if let Some(active_editor) = item.downcast::<Editor>() {
window
.update(cx, |_, window, cx| {
active_editor.update(cx, |editor, cx| {
let row = row.saturating_sub(1);
let col = path.column.unwrap_or(0).saturating_sub(1);
editor.go_to_singleton_buffer_point(
Point::new(row, col),
window,
cx,
);
});
})
.ok();
}
}
navigate_to_positions(&window, items, &paths_with_positions, cx);
}
}
@ -379,6 +415,33 @@ pub async fn open_remote_project(
Ok(())
}
pub fn navigate_to_positions(
window: &WindowHandle<Workspace>,
items: impl IntoIterator<Item = Option<Box<dyn workspace::item::ItemHandle>>>,
positions: &[PathWithPosition],
cx: &mut AsyncApp,
) {
for (item, path) in items.into_iter().zip(positions) {
let Some(item) = item else {
continue;
};
let Some(row) = path.row else {
continue;
};
if let Some(active_editor) = item.downcast::<Editor>() {
window
.update(cx, |_, window, cx| {
active_editor.update(cx, |editor, cx| {
let row = row.saturating_sub(1);
let col = path.column.unwrap_or(0).saturating_sub(1);
editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx);
});
})
.ok();
}
}
}
pub(crate) async fn determine_paths_with_positions(
remote_connection: &Arc<dyn RemoteConnection>,
mut paths: Vec<PathBuf>,

View file

@ -1132,7 +1132,7 @@ impl RemoteClient {
.unwrap()
}
fn remote_connection(&self) -> Option<Arc<dyn RemoteConnection>> {
pub fn remote_connection(&self) -> Option<Arc<dyn RemoteConnection>> {
self.state
.as_ref()
.and_then(|state| state.remote_connection())

View file

@ -1128,7 +1128,7 @@ pub enum Event {
ModalOpened,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum OpenVisible {
All,
None,
@ -5872,7 +5872,7 @@ impl Workspace {
}
}
match self.serialize_workspace_location(cx) {
match self.workspace_location(cx) {
WorkspaceLocation::Location(location, paths) => {
let breakpoints = self.project.update(cx, |project, cx| {
project
@ -5944,7 +5944,7 @@ impl Workspace {
self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
}
fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation {
fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
let paths = PathList::new(&self.root_paths(cx));
if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
@ -5959,6 +5959,16 @@ impl Workspace {
}
}
pub fn serialized_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
Some(SerializedWorkspaceLocation::Remote(connection))
} else if self.project.read(cx).is_local() && self.has_any_items_open(cx) {
Some(SerializedWorkspaceLocation::Local)
} else {
None
}
}
fn update_history(&self, cx: &mut App) {
let Some(id) = self.database_id() else {
return;
@ -8169,24 +8179,60 @@ fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Works
})
}
pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
pub fn workspace_windows_for_location(
serialized_location: &SerializedWorkspaceLocation,
cx: &App,
) -> Vec<WindowHandle<Workspace>> {
cx.windows()
.into_iter()
.filter_map(|window| window.downcast::<Workspace>())
.filter(|workspace| {
let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
(RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
(&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
}
(RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
// The WSL username is not consistently populated in the workspace location, so ignore it for now.
a.distro_name == b.distro_name
}
(RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
a.container_id == b.container_id
}
#[cfg(any(test, feature = "test-support"))]
(RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
a.id == b.id
}
_ => false,
};
workspace
.read(cx)
.is_ok_and(|workspace| workspace.project.read(cx).is_local())
.is_ok_and(|workspace| match workspace.workspace_location(cx) {
WorkspaceLocation::Location(location, _) => {
match (&location, serialized_location) {
(
SerializedWorkspaceLocation::Local,
SerializedWorkspaceLocation::Local,
) => true,
(
SerializedWorkspaceLocation::Remote(a),
SerializedWorkspaceLocation::Remote(b),
) => same_host(a, b),
_ => false,
}
}
_ => false,
})
})
.collect()
}
#[derive(Default)]
#[derive(Default, Clone)]
pub struct OpenOptions {
pub visible: Option<OpenVisible>,
pub focus: Option<bool>,
pub open_new_workspace: Option<bool>,
pub prefer_focused_window: bool,
pub wait: bool,
pub replace_window: Option<WindowHandle<Workspace>>,
pub env: Option<HashMap<String, String>>,
}
@ -8268,6 +8314,83 @@ pub fn open_workspace_by_id(
})
}
pub async fn find_existing_workspace(
abs_paths: &[PathBuf],
open_options: &OpenOptions,
location: &SerializedWorkspaceLocation,
cx: &mut AsyncApp,
) -> (Option<WindowHandle<Workspace>>, OpenVisible) {
let mut existing = None;
let mut open_visible = OpenVisible::All;
let mut best_match = None;
if open_options.open_new_workspace != Some(true) {
cx.update(|cx| {
for window in workspace_windows_for_location(location, cx) {
if let Ok(workspace) = window.read(cx) {
let project = workspace.project.read(cx);
let m = project.visibility_for_paths(
abs_paths,
open_options.open_new_workspace == None,
cx,
);
if m > best_match {
existing = Some(window);
best_match = m;
} else if best_match.is_none() && open_options.open_new_workspace == Some(false)
{
existing = Some(window)
}
}
}
});
let all_paths_are_files = existing
.and_then(|workspace| {
cx.update(|cx| {
workspace
.read(cx)
.map(|workspace| {
let project = workspace.project.read(cx);
let path_style = workspace.path_style(cx);
!abs_paths.iter().any(|path| {
project.worktrees(cx).any(|worktree| {
let worktree = worktree.read(cx);
let abs_path = worktree.abs_path();
path_style
.strip_prefix(path, abs_path.as_ref())
.and_then(|rel| worktree.entry_for_path(&rel))
.is_some_and(|e| e.is_dir())
})
})
})
.ok()
})
})
.unwrap_or(false);
if open_options.open_new_workspace.is_none()
&& existing.is_some()
&& open_options.wait
&& all_paths_are_files
{
cx.update(|cx| {
let windows = workspace_windows_for_location(location, cx);
let window = cx
.active_window()
.and_then(|window| window.downcast::<Workspace>())
.filter(|window| windows.contains(window))
.or_else(|| windows.into_iter().next());
if let Some(window) = window {
existing = Some(window);
open_visible = OpenVisible::None;
}
});
}
}
return (existing, open_visible);
}
#[allow(clippy::type_complexity)]
pub fn open_paths(
abs_paths: &[PathBuf],
@ -8281,16 +8404,17 @@ pub fn open_paths(
)>,
> {
let abs_paths = abs_paths.to_vec();
let mut existing = None;
let mut best_match = None;
let mut open_visible = OpenVisible::All;
#[cfg(target_os = "windows")]
let wsl_path = abs_paths
.iter()
.find_map(|p| util::paths::WslPath::from_path(p));
cx.spawn(async move |cx| {
if open_options.open_new_workspace != Some(true) {
let (mut existing, mut open_visible) = find_existing_workspace(&abs_paths, &open_options, &SerializedWorkspaceLocation::Local, cx).await;
// Fallback: if no workspace contains the paths and all paths are files,
// prefer an existing local workspace window (active window first).
if open_options.open_new_workspace.is_none() && existing.is_none() {
let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
let all_metadatas = futures::future::join_all(all_paths)
.await
@ -8298,54 +8422,17 @@ pub fn open_paths(
.filter_map(|result| result.ok().flatten())
.collect::<Vec<_>>();
cx.update(|cx| {
for window in local_workspace_windows(cx) {
if let Ok(workspace) = window.read(cx) {
let m = workspace.project.read(cx).visibility_for_paths(
&abs_paths,
&all_metadatas,
open_options.open_new_workspace == None,
cx,
);
if m > best_match {
existing = Some(window);
best_match = m;
} else if best_match.is_none()
&& open_options.open_new_workspace == Some(false)
{
existing = Some(window)
}
}
}
});
if open_options.open_new_workspace.is_none()
&& (existing.is_none() || open_options.prefer_focused_window)
&& all_metadatas.iter().all(|file| !file.is_dir)
{
if all_metadatas.iter().all(|file| !file.is_dir) {
cx.update(|cx| {
if let Some(window) = cx
let windows = workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx);
let window = cx
.active_window()
.and_then(|window| window.downcast::<Workspace>())
&& let Ok(workspace) = window.read(cx)
{
let project = workspace.project().read(cx);
if project.is_local() && !project.is_via_collab() {
existing = Some(window);
open_visible = OpenVisible::None;
return;
}
}
for window in local_workspace_windows(cx) {
if let Ok(workspace) = window.read(cx) {
let project = workspace.project().read(cx);
if project.is_via_collab() {
continue;
}
existing = Some(window);
open_visible = OpenVisible::None;
break;
}
.filter(|window| windows.contains(window))
.or_else(|| windows.into_iter().next());
if let Some(window) = window {
existing = Some(window);
open_visible = OpenVisible::None;
}
});
}

View file

@ -249,9 +249,9 @@ pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
semver.workspace = true
terminal_view = { workspace = true, features = ["test-support"] }
title_bar = { workspace = true, features = ["test-support"] }
tree-sitter-md.workspace = true
tree-sitter-rust.workspace = true
title_bar = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
image.workspace = true
agent_ui = { workspace = true, features = ["test-support"] }

View file

@ -37,7 +37,7 @@ use parking_lot::Mutex;
use project::{project_settings::ProjectSettings, trusted_worktrees};
use proto;
use recent_projects::{RemoteSettings, open_remote_project};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use release_channel::{AppCommitSha, AppVersion};
use session::{AppSession, Session};
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
use std::{
@ -322,7 +322,7 @@ fn main() {
let (open_listener, mut open_rx) = OpenListener::new();
let failed_single_instance_check = if *zed_env_vars::ZED_STATELESS
|| *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev
// || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev
{
false
} else {

View file

@ -2413,7 +2413,7 @@ mod tests {
.fs
.as_fake()
.insert_tree(
"/root",
path!("/root"),
json!({
"a": {
"aa": null,
@ -2441,7 +2441,10 @@ mod tests {
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
&[
PathBuf::from(path!("/root/a")),
PathBuf::from(path!("/root/b")),
],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
@ -2453,7 +2456,7 @@ mod tests {
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/a")],
&[PathBuf::from(path!("/root/a"))],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
@ -2482,7 +2485,10 @@ mod tests {
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
&[
PathBuf::from(path!("/root/c")),
PathBuf::from(path!("/root/d")),
],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
@ -2498,7 +2504,7 @@ mod tests {
.unwrap();
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/e")],
&[PathBuf::from(path!("/root/e"))],
app_state,
workspace::OpenOptions {
replace_window: Some(window),
@ -2521,7 +2527,7 @@ mod tests {
.worktrees(cx)
.map(|w| w.read(cx).abs_path())
.collect::<Vec<_>>(),
&[Path::new("/root/e").into()]
&[Path::new(path!("/root/e")).into()]
);
assert!(workspace.left_dock().read(cx).is_open());
assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
@ -5285,7 +5291,7 @@ mod tests {
cx.update(|cx| {
let open_options = OpenOptions {
prefer_focused_window: true,
wait: true,
..Default::default()
};

View file

@ -4,21 +4,19 @@ use anyhow::{Context as _, Result, anyhow};
use cli::{CliRequest, CliResponse, ipc::IpcSender};
use cli::{IpcHandshake, ipc};
use client::{ZedLink, parse_zed_link};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use fs::Fs;
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures::channel::{mpsc, oneshot};
use futures::future;
use futures::future::join_all;
use futures::{FutureExt, SinkExt, StreamExt};
use git_ui::{file_diff_view::FileDiffView, multi_diff_view::MultiDiffView};
use gpui::{App, AsyncApp, Global, WindowHandle};
use language::Point;
use onboarding::FIRST_OPEN;
use onboarding::show_onboarding_view;
use recent_projects::{RemoteSettings, open_remote_project};
use recent_projects::{RemoteSettings, navigate_to_positions, open_remote_project};
use remote::{RemoteConnectionOptions, WslConnectionOptions};
use settings::Settings;
use std::path::{Path, PathBuf};
@ -340,21 +338,9 @@ pub async fn open_paths_with_positions(
WindowHandle<Workspace>,
Vec<Option<Result<Box<dyn ItemHandle>>>>,
)> {
let mut caret_positions = HashMap::default();
let paths = path_positions
.iter()
.map(|path_with_position| {
let path = path_with_position.path.clone();
if let Some(row) = path_with_position.row
&& path.is_file()
{
let row = row.saturating_sub(1);
let col = path_with_position.column.unwrap_or(0).saturating_sub(1);
caret_positions.insert(path.clone(), Point::new(row, col));
}
path
})
.map(|path_with_position| path_with_position.path.clone())
.collect::<Vec<_>>();
let (workspace, mut items) = cx
@ -386,25 +372,15 @@ pub async fn open_paths_with_positions(
for (item, path) in items.iter_mut().zip(&paths) {
if let Some(Err(error)) = item {
*error = anyhow!("error opening {path:?}: {error}");
continue;
}
let Some(Ok(item)) = item else {
continue;
};
let Some(point) = caret_positions.remove(path) else {
continue;
};
if let Some(active_editor) = item.downcast::<Editor>() {
workspace
.update(cx, |_, window, cx| {
active_editor.update(cx, |editor, cx| {
editor.go_to_singleton_buffer_point(point, window, cx);
});
})
.log_err();
}
}
let items_for_navigation = items
.iter()
.map(|item| item.as_ref().and_then(|r| r.as_ref().ok()).cloned())
.collect::<Vec<_>>();
navigate_to_positions(&workspace, items_for_navigation, path_positions, cx);
Ok((workspace, items))
}
@ -527,64 +503,82 @@ async fn open_workspaces(
.detach();
});
}
} else {
// If there are paths to open, open a workspace for each grouping of paths
let mut errored = false;
return Ok(());
}
// If there are paths to open, open a workspace for each grouping of paths
let mut errored = false;
for (location, workspace_paths) in grouped_locations {
match location {
SerializedWorkspaceLocation::Local => {
let workspace_paths = workspace_paths
.paths()
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect();
for (location, workspace_paths) in grouped_locations {
// If reuse flag is passed, open a new workspace in an existing window.
let (open_new_workspace, replace_window) = if reuse {
(
Some(true),
cx.update(|cx| {
workspace::workspace_windows_for_location(&location, cx)
.into_iter()
.next()
}),
)
} else {
(open_new_workspace, None)
};
let open_options = workspace::OpenOptions {
open_new_workspace,
replace_window,
wait,
env: env.clone(),
..Default::default()
};
let workspace_failed_to_open = open_local_workspace(
workspace_paths,
diff_paths.clone(),
diff_all,
open_new_workspace,
reuse,
wait,
responses,
env.as_ref(),
&app_state,
cx,
)
.await;
match location {
SerializedWorkspaceLocation::Local => {
let workspace_paths = workspace_paths
.paths()
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect();
if workspace_failed_to_open {
errored = true
}
}
SerializedWorkspaceLocation::Remote(mut connection) => {
let app_state = app_state.clone();
if let RemoteConnectionOptions::Ssh(options) = &mut connection {
cx.update(|cx| {
RemoteSettings::get_global(cx)
.fill_connection_options_from_settings(options)
});
}
cx.spawn(async move |cx| {
open_remote_project(
connection,
workspace_paths.paths().to_vec(),
app_state,
OpenOptions::default(),
cx,
)
.await
.log_err();
})
.detach();
let workspace_failed_to_open = open_local_workspace(
workspace_paths,
diff_paths.clone(),
diff_all,
open_options,
responses,
&app_state,
cx,
)
.await;
if workspace_failed_to_open {
errored = true
}
}
SerializedWorkspaceLocation::Remote(mut connection) => {
let app_state = app_state.clone();
if let RemoteConnectionOptions::Ssh(options) = &mut connection {
cx.update(|cx| {
RemoteSettings::get_global(cx)
.fill_connection_options_from_settings(options)
});
}
cx.spawn(async move |cx| {
open_remote_project(
connection,
workspace_paths.paths().to_vec(),
app_state,
open_options,
cx,
)
.await
.log_err();
})
.detach();
}
}
anyhow::ensure!(!errored, "failed to open a workspace");
}
anyhow::ensure!(!errored, "failed to open a workspace");
Ok(())
}
@ -592,39 +586,20 @@ async fn open_local_workspace(
workspace_paths: Vec<String>,
diff_paths: Vec<[String; 2]>,
diff_all: bool,
open_new_workspace: Option<bool>,
reuse: bool,
wait: bool,
open_options: workspace::OpenOptions,
responses: &IpcSender<CliResponse>,
env: Option<&HashMap<String, String>>,
app_state: &Arc<AppState>,
cx: &mut AsyncApp,
) -> bool {
let paths_with_position =
derive_paths_with_position(app_state.fs.as_ref(), workspace_paths).await;
// If reuse flag is passed, open a new workspace in an existing window.
let (open_new_workspace, replace_window) = if reuse {
(
Some(true),
cx.update(|cx| workspace::local_workspace_windows(cx).into_iter().next()),
)
} else {
(open_new_workspace, None)
};
let (workspace, items) = match open_paths_with_positions(
&paths_with_position,
&diff_paths,
diff_all,
app_state.clone(),
workspace::OpenOptions {
open_new_workspace,
replace_window,
prefer_focused_window: wait,
env: env.cloned(),
..Default::default()
},
open_options.clone(),
cx,
)
.await
@ -643,10 +618,9 @@ async fn open_local_workspace(
let mut errored = false;
let mut item_release_futures = Vec::new();
let mut subscriptions = Vec::new();
// If --wait flag is used with no paths, or a directory, then wait until
// the entire workspace is closed.
if wait {
if open_options.wait {
let mut wait_for_window_close = paths_with_position.is_empty() && diff_paths.is_empty();
for path_with_position in &paths_with_position {
if app_state.fs.is_dir(&path_with_position.path).await {
@ -669,7 +643,7 @@ async fn open_local_workspace(
for item in items {
match item {
Some(Ok(item)) => {
if wait {
if open_options.wait {
let (release_tx, release_rx) = oneshot::channel();
item_release_futures.push(release_rx);
subscriptions.push(Ok(cx.update(|cx| {
@ -694,7 +668,7 @@ async fn open_local_workspace(
}
}
if wait {
if open_options.wait {
let wait = async move {
let _subscriptions = subscriptions;
let _ = future::try_join_all(item_release_futures).await;
@ -725,17 +699,30 @@ pub async fn derive_paths_with_position(
fs: &dyn Fs,
path_strings: impl IntoIterator<Item = impl AsRef<str>>,
) -> Vec<PathWithPosition> {
join_all(path_strings.into_iter().map(|path_str| async move {
let canonicalized = fs.canonicalize(Path::new(path_str.as_ref())).await;
(path_str, canonicalized)
}))
.await
.into_iter()
.map(|(original, canonicalized)| match canonicalized {
Ok(canonicalized) => PathWithPosition::from_path(canonicalized),
Err(_) => PathWithPosition::parse_str(original.as_ref()),
})
.collect()
let path_strings: Vec<_> = path_strings.into_iter().collect();
let mut result = Vec::with_capacity(path_strings.len());
for path_str in path_strings {
let original_path = Path::new(path_str.as_ref());
let mut parsed = PathWithPosition::parse_str(path_str.as_ref());
// If a the unparsed path string actually points to a file, use that file instead of parsing out the line/col number.
// Note: The colon syntax is also used to open NTFS alternate data streams (e.g., `file.txt:stream`), which would cause issues.
// However, the colon is not valid in NTFS file names, so we can just skip this logic.
if !cfg!(windows)
&& parsed.row.is_some()
&& parsed.path != original_path
&& fs.is_file(original_path).await
{
parsed = PathWithPosition::from_path(original_path.to_path_buf());
}
if let Ok(canonicalized) = fs.canonicalize(&parsed.path).await {
parsed.path = canonicalized;
}
result.push(parsed);
}
result
}
#[cfg(test)]
@ -755,7 +742,7 @@ mod tests {
use serde_json::json;
use std::{sync::Arc, task::Poll};
use util::path;
use workspace::{AppState, Workspace};
use workspace::{AppState, Workspace, find_existing_workspace};
#[gpui::test]
fn test_parse_ssh_url(cx: &mut TestAppContext) {
@ -957,11 +944,11 @@ mod tests {
workspace_paths,
vec![],
false,
None,
false,
true,
workspace::OpenOptions {
wait: true,
..Default::default()
},
&response_tx,
None,
&app_state,
&mut cx,
)
@ -1049,11 +1036,11 @@ mod tests {
workspace_paths,
vec![],
false,
open_new_workspace,
false,
false,
OpenOptions {
open_new_workspace,
..Default::default()
},
&response_tx,
None,
&app_state,
&mut cx,
)
@ -1123,11 +1110,8 @@ mod tests {
workspace_paths,
vec![],
false,
None,
false,
false,
workspace::OpenOptions::default(),
&response_tx,
None,
&app_state,
&mut cx,
)
@ -1138,6 +1122,16 @@ mod tests {
// Now test the reuse functionality - should replace the existing workspace
let workspace_paths_reuse = vec![file1_path.to_string()];
let paths: Vec<PathBuf> = workspace_paths_reuse.iter().map(PathBuf::from).collect();
let window_to_replace = find_existing_workspace(
&paths,
&workspace::OpenOptions::default(),
&workspace::SerializedWorkspaceLocation::Local,
&mut cx.to_async(),
)
.await
.0
.unwrap();
let errored_reuse = cx
.spawn({
@ -1148,11 +1142,11 @@ mod tests {
workspace_paths_reuse,
vec![],
false,
None, // open_new_workspace will be overridden by reuse logic
true, // reuse = true
false,
workspace::OpenOptions {
replace_window: Some(window_to_replace),
..Default::default()
},
&response_tx,
None,
&app_state,
&mut cx,
)