Detailed changes
@@ -4763,6 +4763,19 @@ impl Project {
});
}
+ pub fn remove_worktree_for_main_worktree_path(
+ &mut self,
+ path: impl AsRef<Path>,
+ cx: &mut Context<Self>,
+ ) {
+ let path = path.as_ref();
+ self.worktree_store.update(cx, |worktree_store, cx| {
+ if let Some(worktree) = worktree_store.worktree_for_main_worktree_path(path, cx) {
+ worktree_store.remove_worktree(worktree.read(cx).id(), cx);
+ }
+ });
+ }
+
fn add_worktree(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) {
self.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.add(worktree, cx);
@@ -850,6 +850,21 @@ impl WorktreeStore {
self.send_project_updates(cx);
}
+ pub fn worktree_for_main_worktree_path(
+ &self,
+ path: &Path,
+ cx: &App,
+ ) -> Option<Entity<Worktree>> {
+ self.visible_worktrees(cx).find(|worktree| {
+ let worktree = worktree.read(cx);
+ if let Some(common_dir) = worktree.root_repo_common_dir() {
+ common_dir.parent() == Some(path)
+ } else {
+ worktree.abs_path().as_ref() == path
+ }
+ })
+ }
+
pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool) {
self.worktrees_reordered = worktrees_reordered;
}
@@ -689,12 +689,9 @@ impl Sidebar {
return;
};
- let paths: Vec<std::path::PathBuf> =
- path_list.paths().iter().map(|p| p.to_path_buf()).collect();
-
multi_workspace
- .update(cx, |mw, cx| {
- mw.open_project(paths, workspace::OpenMode::Activate, window, cx)
+ .update(cx, |this, cx| {
+ this.find_or_create_local_workspace(path_list.clone(), window, cx)
})
.detach_and_log_err(cx);
}
@@ -1439,10 +1436,7 @@ impl Sidebar {
})
}),
)
- .child({
- let workspace_for_new_thread = workspace.clone();
- let path_list_for_new_thread = path_list.clone();
-
+ .child(
h_flex()
.when(self.project_header_menu_ix != Some(ix), |this| {
this.visible_on_hover(group_name)
@@ -1450,13 +1444,7 @@ impl Sidebar {
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
})
- .when_some(workspace, |this, workspace| {
- this.child(
- self.render_project_header_menu(
- ix, id_prefix, &workspace, &workspace, cx,
- ),
- )
- })
+ .child(self.render_project_header_menu(ix, id_prefix, key, cx))
.when(view_more_expanded && !is_collapsed, |this| {
this.child(
IconButton::new(
@@ -1478,12 +1466,10 @@ impl Sidebar {
})),
)
})
- .when(
- show_new_thread_button && workspace_for_new_thread.is_some(),
- |this| {
- let workspace_for_new_thread =
- workspace_for_new_thread.clone().unwrap();
- let path_list_for_new_thread = path_list_for_new_thread.clone();
+ .when_some(
+ workspace.filter(|_| show_new_thread_button),
+ |this, workspace| {
+ let path_list = path_list.clone();
this.child(
IconButton::new(
SharedString::from(format!(
@@ -1495,26 +1481,22 @@ impl Sidebar {
.tooltip(Tooltip::text("New Thread"))
.on_click(cx.listener(
move |this, _, window, cx| {
- this.collapsed_groups.remove(&path_list_for_new_thread);
+ this.collapsed_groups.remove(&path_list);
this.selection = None;
- this.create_new_thread(
- &workspace_for_new_thread,
- window,
- cx,
- );
+ this.create_new_thread(&workspace, window, cx);
},
)),
)
},
- )
- })
+ ),
+ )
.when(!is_active, |this| {
- let path_list_for_open = path_list.clone();
+ let path_list = path_list.clone();
this.cursor_pointer()
.hover(|s| s.bg(hover_color))
.tooltip(Tooltip::text("Open Workspace"))
.on_click(cx.listener(move |this, _, window, cx| {
- if let Some(workspace) = this.workspace_for_group(&path_list_for_open, cx) {
+ if let Some(workspace) = this.workspace_for_group(&path_list, cx) {
this.active_entry = Some(ActiveEntry::Draft(workspace.clone()));
if let Some(multi_workspace) = this.multi_workspace.upgrade() {
multi_workspace.update(cx, |multi_workspace, cx| {
@@ -1527,7 +1509,7 @@ impl Sidebar {
});
}
} else {
- this.open_workspace_for_group(&path_list_for_open, window, cx);
+ this.open_workspace_for_group(&path_list, window, cx);
}
}))
})
@@ -1538,14 +1520,12 @@ impl Sidebar {
&self,
ix: usize,
id_prefix: &str,
- workspace: &Entity<Workspace>,
- workspace_for_remove: &Entity<Workspace>,
+ project_group_key: &ProjectGroupKey,
cx: &mut Context<Self>,
) -> impl IntoElement {
- let workspace_for_menu = workspace.clone();
- let workspace_for_remove = workspace_for_remove.clone();
let multi_workspace = self.multi_workspace.clone();
let this = cx.weak_entity();
+ let project_group_key = project_group_key.clone();
PopoverMenu::new(format!("{id_prefix}project-header-menu-{ix}"))
.on_open(Rc::new({
@@ -1559,116 +1539,102 @@ impl Sidebar {
}
}))
.menu(move |window, cx| {
- let workspace = workspace_for_menu.clone();
- let workspace_for_remove = workspace_for_remove.clone();
let multi_workspace = multi_workspace.clone();
+ let project_group_key = project_group_key.clone();
let menu = ContextMenu::build_persistent(window, cx, move |menu, _window, cx| {
- let worktrees: Vec<_> = workspace
- .read(cx)
- .visible_worktrees(cx)
- .map(|worktree| {
- let worktree_read = worktree.read(cx);
- let id = worktree_read.id();
- let name: SharedString =
- worktree_read.root_name().as_unix_str().to_string().into();
- (id, name)
- })
- .collect();
-
- let worktree_count = worktrees.len();
-
let mut menu = menu
.header("Project Folders")
.end_slot_action(Box::new(menu::EndSlot));
- for (worktree_id, name) in &worktrees {
- let worktree_id = *worktree_id;
- let workspace_for_worktree = workspace.clone();
- let workspace_for_remove_worktree = workspace_for_remove.clone();
- let multi_workspace_for_worktree = multi_workspace.clone();
-
- let remove_handler = move |window: &mut Window, cx: &mut App| {
- if worktree_count <= 1 {
- if let Some(mw) = multi_workspace_for_worktree.upgrade() {
- let ws = workspace_for_remove_worktree.clone();
- mw.update(cx, |multi_workspace, cx| {
- multi_workspace.remove(&ws, window, cx);
- });
- }
- } else {
- workspace_for_worktree.update(cx, |workspace, cx| {
- workspace.project().update(cx, |project, cx| {
- project.remove_worktree(worktree_id, cx);
- });
- });
- }
+ for path in project_group_key.path_list().paths() {
+ let Some(name) = path.file_name() else {
+ continue;
};
-
+ let name: SharedString = name.to_string_lossy().into_owned().into();
+ let path = path.clone();
+ let project_group_key = project_group_key.clone();
+ let multi_workspace = multi_workspace.clone();
menu = menu.entry_with_end_slot_on_hover(
name.clone(),
None,
|_, _| {},
IconName::Close,
"Remove Folder".into(),
- remove_handler,
+ move |_window, cx| {
+ multi_workspace
+ .update(cx, |multi_workspace, cx| {
+ multi_workspace.remove_folder_from_project_group(
+ &project_group_key,
+ &path,
+ cx,
+ );
+ })
+ .ok();
+ },
);
}
- let workspace_for_add = workspace.clone();
- let multi_workspace_for_add = multi_workspace.clone();
let menu = menu.separator().entry(
"Add Folder to Project",
Some(Box::new(AddFolderToProject)),
- move |window, cx| {
- if let Some(mw) = multi_workspace_for_add.upgrade() {
- mw.update(cx, |mw, cx| {
- mw.activate(workspace_for_add.clone(), window, cx);
- });
+ {
+ let project_group_key = project_group_key.clone();
+ let multi_workspace = multi_workspace.clone();
+ move |window, cx| {
+ multi_workspace
+ .update(cx, |multi_workspace, cx| {
+ multi_workspace.prompt_to_add_folders_to_project_group(
+ &project_group_key,
+ window,
+ cx,
+ );
+ })
+ .ok();
}
- workspace_for_add.update(cx, |workspace, cx| {
- workspace.add_folder_to_project(&AddFolderToProject, window, cx);
- });
},
);
- let workspace_count = multi_workspace
+ let group_count = multi_workspace
.upgrade()
- .map_or(0, |mw| mw.read(cx).workspaces().len());
- let menu = if workspace_count > 1 {
- let workspace_for_move = workspace.clone();
- let multi_workspace_for_move = multi_workspace.clone();
+ .map_or(0, |mw| mw.read(cx).project_group_keys().count());
+ let menu = if group_count > 1 {
+ let project_group_key = project_group_key.clone();
+ let multi_workspace = multi_workspace.clone();
menu.entry(
"Move to New Window",
Some(Box::new(
zed_actions::agents_sidebar::MoveWorkspaceToNewWindow,
)),
move |window, cx| {
- if let Some(mw) = multi_workspace_for_move.upgrade() {
- mw.update(cx, |multi_workspace, cx| {
- multi_workspace.move_workspace_to_new_window(
- &workspace_for_move,
+ multi_workspace
+ .update(cx, |multi_workspace, cx| {
+ multi_workspace.move_project_group_to_new_window(
+ &project_group_key,
window,
cx,
);
- });
- }
+ })
+ .ok();
},
)
} else {
menu
};
- let workspace_for_remove = workspace_for_remove.clone();
- let multi_workspace_for_remove = multi_workspace.clone();
+ let project_group_key = project_group_key.clone();
+ let multi_workspace = multi_workspace.clone();
menu.separator()
.entry("Remove Project", None, move |window, cx| {
- if let Some(mw) = multi_workspace_for_remove.upgrade() {
- let ws = workspace_for_remove.clone();
- mw.update(cx, |multi_workspace, cx| {
- multi_workspace.remove(&ws, window, cx);
- });
- }
+ multi_workspace
+ .update(cx, |multi_workspace, cx| {
+ multi_workspace.remove_project_group(
+ &project_group_key,
+ window,
+ cx,
+ );
+ })
+ .ok();
})
});
@@ -2176,16 +2142,12 @@ impl Sidebar {
return;
};
- let paths: Vec<std::path::PathBuf> =
- path_list.paths().iter().map(|p| p.to_path_buf()).collect();
-
- let open_task = multi_workspace.update(cx, |mw, cx| {
- mw.open_project(paths, workspace::OpenMode::Activate, window, cx)
+ let open_task = multi_workspace.update(cx, |this, cx| {
+ this.find_or_create_local_workspace(path_list, window, cx)
});
cx.spawn_in(window, async move |this, cx| {
let workspace = open_task.await?;
-
this.update_in(cx, |this, window, cx| {
this.activate_thread(metadata, &workspace, window, cx);
})?;
@@ -65,6 +65,16 @@ impl PathList {
self.paths.is_empty()
}
+ /// Returns a new `PathList` with the given path removed.
+ pub fn without_path(&self, path_to_remove: &Path) -> PathList {
+ let paths: Vec<PathBuf> = self
+ .ordered_paths()
+ .filter(|p| p.as_path() != path_to_remove)
+ .cloned()
+ .collect();
+ PathList::new(&paths)
+ }
+
/// Get the paths in lexicographic order.
pub fn paths(&self) -> &[PathBuf] {
self.paths.as_ref()
@@ -1,5 +1,6 @@
use anyhow::Result;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
+use gpui::PathPromptOptions;
use gpui::{
AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
@@ -7,14 +8,16 @@ use gpui::{
};
#[cfg(any(test, feature = "test-support"))]
use project::Project;
-use project::{DisableAiSettings, ProjectGroupKey};
+use project::{DirectoryLister, DisableAiSettings, ProjectGroupKey};
use settings::Settings;
pub use settings::SidebarSide;
use std::future::Future;
+use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use ui::prelude::*;
use util::ResultExt;
+use util::path_list::PathList;
use zed_actions::agents_sidebar::{MoveWorkspaceToNewWindow, ToggleThreadSwitcher};
use agent_settings::AgentSettings;
@@ -23,6 +26,7 @@ use ui::{ContextMenu, right_click_menu};
const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
+use crate::AppState;
use crate::{
CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, OpenMode,
Panel, Workspace, WorkspaceId, client_side_decorations,
@@ -494,6 +498,176 @@ impl MultiWorkspace {
groups.into_iter()
}
+ pub fn workspaces_for_project_group(
+ &self,
+ project_group_key: &ProjectGroupKey,
+ cx: &App,
+ ) -> impl Iterator<Item = &Entity<Workspace>> {
+ self.workspaces
+ .iter()
+ .filter(move |ws| ws.read(cx).project_group_key(cx) == *project_group_key)
+ }
+
+ pub fn remove_folder_from_project_group(
+ &mut self,
+ project_group_key: &ProjectGroupKey,
+ path: &Path,
+ cx: &mut Context<Self>,
+ ) {
+ let new_path_list = project_group_key.path_list().without_path(path);
+ if new_path_list.is_empty() {
+ return;
+ }
+
+ let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
+
+ let workspaces: Vec<_> = self
+ .workspaces_for_project_group(project_group_key, cx)
+ .cloned()
+ .collect();
+
+ self.add_project_group_key(new_key);
+
+ for workspace in workspaces {
+ let project = workspace.read(cx).project().clone();
+ project.update(cx, |project, cx| {
+ project.remove_worktree_for_main_worktree_path(path, cx);
+ });
+ }
+
+ self.serialize(cx);
+ cx.notify();
+ }
+
+ pub fn prompt_to_add_folders_to_project_group(
+ &mut self,
+ key: &ProjectGroupKey,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let paths = self.workspace().update(cx, |workspace, cx| {
+ workspace.prompt_for_open_path(
+ PathPromptOptions {
+ files: false,
+ directories: true,
+ multiple: true,
+ prompt: None,
+ },
+ DirectoryLister::Project(workspace.project().clone()),
+ window,
+ cx,
+ )
+ });
+
+ let key = key.clone();
+ cx.spawn_in(window, async move |this, cx| {
+ if let Some(new_paths) = paths.await.ok().flatten() {
+ if !new_paths.is_empty() {
+ this.update(cx, |multi_workspace, cx| {
+ multi_workspace.add_folders_to_project_group(&key, new_paths, cx);
+ })?;
+ }
+ }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn add_folders_to_project_group(
+ &mut self,
+ project_group_key: &ProjectGroupKey,
+ new_paths: Vec<PathBuf>,
+ cx: &mut Context<Self>,
+ ) {
+ let mut all_paths: Vec<PathBuf> = project_group_key.path_list().paths().to_vec();
+ all_paths.extend(new_paths.iter().cloned());
+ let new_path_list = PathList::new(&all_paths);
+ let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
+
+ let workspaces: Vec<_> = self
+ .workspaces_for_project_group(project_group_key, cx)
+ .cloned()
+ .collect();
+
+ self.add_project_group_key(new_key);
+
+ for workspace in workspaces {
+ let project = workspace.read(cx).project().clone();
+ for path in &new_paths {
+ project
+ .update(cx, |project, cx| {
+ project.find_or_create_worktree(path, true, cx)
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ self.serialize(cx);
+ cx.notify();
+ }
+
+ pub fn remove_project_group(
+ &mut self,
+ key: &ProjectGroupKey,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.project_group_keys.retain(|k| k != key);
+
+ let workspaces: Vec<_> = self
+ .workspaces_for_project_group(key, cx)
+ .cloned()
+ .collect();
+ for workspace in workspaces {
+ self.remove(&workspace, window, cx);
+ }
+
+ self.serialize(cx);
+ cx.notify();
+ }
+
+ /// Finds an existing workspace in this multi-workspace whose paths match,
+ /// or creates a new one (deserializing its saved state from the database).
+ /// Never searches other windows or matches workspaces with a superset of
+ /// the requested paths.
+ pub fn find_or_create_local_workspace(
+ &mut self,
+ path_list: PathList,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Entity<Workspace>>> {
+ if let Some(workspace) = self
+ .workspaces
+ .iter()
+ .find(|ws| ws.read(cx).project_group_key(cx).path_list() == &path_list)
+ .cloned()
+ {
+ self.activate(workspace.clone(), window, cx);
+ return Task::ready(Ok(workspace));
+ }
+
+ let paths = path_list.paths().to_vec();
+ let app_state = self.workspace().read(cx).app_state().clone();
+ let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
+
+ cx.spawn(async move |_this, cx| {
+ let result = cx
+ .update(|cx| {
+ Workspace::new_local(
+ paths,
+ app_state,
+ requesting_window,
+ None,
+ None,
+ OpenMode::Activate,
+ cx,
+ )
+ })
+ .await?;
+ Ok(result.workspace)
+ })
+ }
+
pub fn workspace(&self) -> &Entity<Workspace> {
&self.workspaces[self.active_workspace_index]
}
@@ -892,7 +1066,7 @@ impl MultiWorkspace {
return;
}
- let app_state: Arc<crate::AppState> = workspace.read(cx).app_state().clone();
+ let app_state: Arc<AppState> = workspace.read(cx).app_state().clone();
cx.defer(move |cx| {
let options = (app_state.build_window_options)(None, cx);
@@ -909,7 +1083,58 @@ impl MultiWorkspace {
});
}
- // TODO: Move group to a new window?
+ pub fn move_project_group_to_new_window(
+ &mut self,
+ key: &ProjectGroupKey,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let workspaces: Vec<_> = self
+ .workspaces_for_project_group(key, cx)
+ .cloned()
+ .collect();
+ if workspaces.is_empty() {
+ return;
+ }
+
+ self.project_group_keys.retain(|k| k != key);
+
+ let mut removed = Vec::new();
+ for workspace in &workspaces {
+ if self.remove(workspace, window, cx) {
+ removed.push(workspace.clone());
+ }
+ }
+
+ if removed.is_empty() {
+ return;
+ }
+
+ let app_state = removed[0].read(cx).app_state().clone();
+
+ cx.defer(move |cx| {
+ let options = (app_state.build_window_options)(None, cx);
+
+ let first = removed[0].clone();
+ let rest = removed[1..].to_vec();
+
+ let Ok(new_window) = cx.open_window(options, |window, cx| {
+ cx.new(|cx| MultiWorkspace::new(first, window, cx))
+ }) else {
+ return;
+ };
+
+ new_window
+ .update(cx, |mw, window, cx| {
+ for workspace in rest {
+ mw.activate(workspace, window, cx);
+ }
+ window.activate_window();
+ })
+ .log_err();
+ });
+ }
+
fn move_active_workspace_to_new_window(
&mut self,
_: &MoveWorkspaceToNewWindow,
@@ -927,16 +1152,10 @@ impl MultiWorkspace {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Workspace>>> {
- let workspace = self.workspace().clone();
-
- let needs_close_prompt = !self.multi_workspace_enabled(cx);
- let open_mode = if self.multi_workspace_enabled(cx) {
- open_mode
+ if self.multi_workspace_enabled(cx) {
+ self.find_or_create_local_workspace(PathList::new(&paths), window, cx)
} else {
- OpenMode::Activate
- };
-
- if needs_close_prompt {
+ let workspace = self.workspace().clone();
cx.spawn_in(window, async move |_this, cx| {
let should_continue = workspace
.update_in(cx, |workspace, window, cx| {
@@ -953,10 +1172,6 @@ impl MultiWorkspace {
Ok(workspace)
}
})
- } else {
- workspace.update(cx, |workspace, cx| {
- workspace.open_workspace_for_paths(open_mode, paths, window, cx)
- })
}
}
}