mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Cherry-pick of #55715 to preview ---- * Perform grouping even for repositories that have no main worktree * Enable grouping for remote projects * Delete entire project groups when deleting via the recent project picker Release Notes: - Fixed a bug where each linked worktree appeared as its own entry in recent projects for repositories without main worktrees - Fixed a bug where deleting projects from the recent projects sometimes appeared to have no effect. Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
a9c2c08899
commit
81498612ec
13 changed files with 800 additions and 344 deletions
|
|
@ -200,27 +200,30 @@ fn migrate_thread_remote_connections(cx: &mut App, migration_task: Task<anyhow::
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let recent_workspaces = workspace_db.recent_project_workspaces(fs.as_ref()).await?;
|
||||
let recent_workspaces = workspace_db
|
||||
.recent_project_workspaces_ungrouped(fs.as_ref())
|
||||
.await?;
|
||||
|
||||
let mut local_path_lists = HashSet::<PathList>::default();
|
||||
let mut remote_path_lists = HashMap::<PathList, RemoteConnectionOptions>::default();
|
||||
|
||||
recent_workspaces
|
||||
.iter()
|
||||
.filter(|(_, location, path_list, _)| {
|
||||
!path_list.is_empty() && matches!(location, &SerializedWorkspaceLocation::Local)
|
||||
.filter(|workspace| {
|
||||
!workspace.paths.is_empty()
|
||||
&& matches!(workspace.location, SerializedWorkspaceLocation::Local)
|
||||
})
|
||||
.for_each(|(_, _, path_list, _)| {
|
||||
local_path_lists.insert(path_list.clone());
|
||||
.for_each(|workspace| {
|
||||
local_path_lists.insert(workspace.paths.clone());
|
||||
});
|
||||
|
||||
for (_, location, path_list, _) in recent_workspaces {
|
||||
match location {
|
||||
for workspace in recent_workspaces {
|
||||
match workspace.location {
|
||||
SerializedWorkspaceLocation::Remote(remote_connection)
|
||||
if !local_path_lists.contains(&path_list) =>
|
||||
if !local_path_lists.contains(&workspace.paths) =>
|
||||
{
|
||||
remote_path_lists
|
||||
.entry(path_list)
|
||||
.entry(workspace.paths)
|
||||
.or_insert(remote_connection);
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ use ui_input::ErasedEditor;
|
|||
use util::ResultExt;
|
||||
use util::paths::PathExt;
|
||||
use workspace::{
|
||||
CloseWindow, ModalView, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceDb,
|
||||
WorkspaceId, resolve_worktree_workspaces,
|
||||
CloseWindow, ModalView, PathList, RecentWorkspace, SerializedWorkspaceLocation, Workspace,
|
||||
WorkspaceDb, WorkspaceId,
|
||||
};
|
||||
|
||||
use zed_actions::agents_sidebar::FocusSidebarFilter;
|
||||
|
|
@ -1127,7 +1127,6 @@ impl ProjectPickerModal {
|
|||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let workspaces = resolve_worktree_workspaces(workspaces, fs.as_ref()).await;
|
||||
this.update_in(cx, move |this, window, cx| {
|
||||
this.picker.update(cx, move |picker, cx| {
|
||||
picker.delegate.workspaces = workspaces;
|
||||
|
|
@ -1182,12 +1181,7 @@ struct ProjectPickerDelegate {
|
|||
archive_view: WeakEntity<ThreadsArchiveView>,
|
||||
current_workspace_id: Option<WorkspaceId>,
|
||||
sibling_workspace_ids: HashSet<WorkspaceId>,
|
||||
workspaces: Vec<(
|
||||
WorkspaceId,
|
||||
SerializedWorkspaceLocation,
|
||||
PathList,
|
||||
DateTime<Utc>,
|
||||
)>,
|
||||
workspaces: Vec<RecentWorkspace>,
|
||||
filtered_entries: Vec<ProjectPickerEntry>,
|
||||
selected_index: usize,
|
||||
focus_handle: FocusHandle,
|
||||
|
|
@ -1332,9 +1326,10 @@ impl PickerDelegate for ProjectPickerDelegate {
|
|||
.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (id, _, _, _))| self.is_sibling_workspace(*id))
|
||||
.map(|(id, (_, _, paths, _))| {
|
||||
let combined_string = paths
|
||||
.filter(|(_, workspace)| self.is_sibling_workspace(workspace.workspace_id))
|
||||
.map(|(id, workspace)| {
|
||||
let combined_string = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
|
|
@ -1364,11 +1359,13 @@ impl PickerDelegate for ProjectPickerDelegate {
|
|||
.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (id, _, _, _))| {
|
||||
!self.is_current_workspace(*id) && !self.is_sibling_workspace(*id)
|
||||
.filter(|(_, workspace)| {
|
||||
!self.is_current_workspace(workspace.workspace_id)
|
||||
&& !self.is_sibling_workspace(workspace.workspace_id)
|
||||
})
|
||||
.map(|(id, (_, _, paths, _))| {
|
||||
let combined_string = paths
|
||||
.map(|(id, workspace)| {
|
||||
let combined_string = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
|
|
@ -1406,8 +1403,8 @@ impl PickerDelegate for ProjectPickerDelegate {
|
|||
entries.push(ProjectPickerEntry::Header("This Window".into()));
|
||||
|
||||
if is_empty_query {
|
||||
for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() {
|
||||
if self.is_sibling_workspace(*workspace_id) {
|
||||
for (id, workspace) in self.workspaces.iter().enumerate() {
|
||||
if self.is_sibling_workspace(workspace.workspace_id) {
|
||||
entries.push(ProjectPickerEntry::Workspace(StringMatch {
|
||||
candidate_id: id,
|
||||
score: 0.0,
|
||||
|
|
@ -1433,9 +1430,9 @@ impl PickerDelegate for ProjectPickerDelegate {
|
|||
entries.push(ProjectPickerEntry::Header("Recent Projects".into()));
|
||||
|
||||
if is_empty_query {
|
||||
for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() {
|
||||
if !self.is_current_workspace(*workspace_id)
|
||||
&& !self.is_sibling_workspace(*workspace_id)
|
||||
for (id, workspace) in self.workspaces.iter().enumerate() {
|
||||
if !self.is_current_workspace(workspace.workspace_id)
|
||||
&& !self.is_sibling_workspace(workspace.workspace_id)
|
||||
{
|
||||
entries.push(ProjectPickerEntry::Workspace(StringMatch {
|
||||
candidate_id: id,
|
||||
|
|
@ -1468,11 +1465,11 @@ impl PickerDelegate for ProjectPickerDelegate {
|
|||
Some(ProjectPickerEntry::Workspace(hit)) => hit.candidate_id,
|
||||
_ => return,
|
||||
};
|
||||
let Some((_workspace_id, _location, paths, _)) = self.workspaces.get(candidate_id) else {
|
||||
let Some(workspace) = self.workspaces.get(candidate_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.update_working_directories_and_unarchive(paths.clone(), window, cx);
|
||||
self.update_working_directories_and_unarchive(workspace.paths.clone(), window, cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
|
|
@ -1504,9 +1501,11 @@ impl PickerDelegate for ProjectPickerDelegate {
|
|||
.into_any_element(),
|
||||
),
|
||||
ProjectPickerEntry::Workspace(hit) => {
|
||||
let (_, location, paths, _) = self.workspaces.get(hit.candidate_id)?;
|
||||
let workspace = self.workspaces.get(hit.candidate_id)?;
|
||||
let location = &workspace.location;
|
||||
|
||||
let ordered_paths: Vec<_> = paths
|
||||
let ordered_paths: Vec<_> = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|p| p.compact().to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
|
@ -1514,7 +1513,8 @@ impl PickerDelegate for ProjectPickerDelegate {
|
|||
let tooltip_path: SharedString = ordered_paths.join("\n").into();
|
||||
|
||||
let mut path_start_offset = 0;
|
||||
let match_labels: Vec<_> = paths
|
||||
let match_labels: Vec<_> = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|p| p.compact())
|
||||
.map(|path| {
|
||||
|
|
|
|||
|
|
@ -60,26 +60,6 @@ pub const GRAPH_CHUNK_SIZE: usize = 1000;
|
|||
/// Default value for the `git.worktree_directory` setting.
|
||||
pub const DEFAULT_WORKTREE_DIRECTORY: &str = "../worktrees";
|
||||
|
||||
/// Determine the original (main) repository's working directory.
|
||||
///
|
||||
/// For linked worktrees, `common_dir` differs from `repository_dir` and
|
||||
/// points to the main repo's `.git` directory, so we can derive the main
|
||||
/// repo's working directory from it. For normal repos and submodules,
|
||||
/// `common_dir` equals `repository_dir`, and the original repo is simply
|
||||
/// `work_directory` itself.
|
||||
pub fn original_repo_path(
|
||||
work_directory: &Path,
|
||||
common_dir: &Path,
|
||||
repository_dir: &Path,
|
||||
) -> PathBuf {
|
||||
if common_dir != repository_dir {
|
||||
original_repo_path_from_common_dir(common_dir)
|
||||
.unwrap_or_else(|| work_directory.to_path_buf())
|
||||
} else {
|
||||
work_directory.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the git common directory (from `commondir()`), derive the original
|
||||
/// repository's working directory.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -7813,14 +7813,17 @@ impl Repository {
|
|||
}
|
||||
|
||||
/// If `path` is a git linked worktree checkout, resolves it to the main
|
||||
/// repository's working directory path. Returns `None` if `path` is a normal
|
||||
/// repository, not a git repo, or if resolution fails.
|
||||
/// repository's identity path. For regular linked worktrees this is the main
|
||||
/// repository's working directory; for linked worktrees backed by a bare repo
|
||||
/// such as `.bare`, this is the parent project directory users think of as the
|
||||
/// repository root. Returns `None` if `path` is a normal repository, not a git
|
||||
/// repo, or if resolution fails.
|
||||
///
|
||||
/// Resolution works by:
|
||||
/// 1. Reading the `.git` file to get the `gitdir:` pointer
|
||||
/// 2. Following that to the worktree-specific git directory
|
||||
/// 3. Reading the `commondir` file to find the shared `.git` directory
|
||||
/// 4. Deriving the main repo's working directory from the common dir
|
||||
/// 4. Deriving the main repo's identity path from the common dir
|
||||
pub async fn resolve_git_worktree_to_main_repo(fs: &dyn Fs, path: &Path) -> Option<PathBuf> {
|
||||
let dot_git = path.join(".git");
|
||||
let metadata = fs.metadata(&dot_git).await.ok()??;
|
||||
|
|
@ -7837,7 +7840,7 @@ pub async fn resolve_git_worktree_to_main_repo(fs: &dyn Fs, path: &Path) -> Opti
|
|||
.canonicalize(&gitdir_abs.join(commondir_content.trim()))
|
||||
.await
|
||||
.ok()?;
|
||||
git::repository::original_repo_path_from_common_dir(&common_dir)
|
||||
Some(repo_identity_path(&common_dir).to_path_buf())
|
||||
}
|
||||
|
||||
/// Validates that the resolved worktree directory is acceptable:
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ pub use prettier_store::PrettierStore;
|
|||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||
#[cfg(target_os = "windows")]
|
||||
use remote::wsl_path_to_windows_path;
|
||||
use remote::{RemoteClient, RemoteConnectionOptions};
|
||||
use remote::{RemoteClient, RemoteConnectionOptions, same_remote_connection_identity};
|
||||
use rpc::{
|
||||
AnyProtoClient, ErrorCode,
|
||||
proto::{LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID},
|
||||
|
|
@ -6226,6 +6226,11 @@ impl ProjectGroupKey {
|
|||
pub fn host(&self) -> Option<RemoteConnectionOptions> {
|
||||
self.host.clone()
|
||||
}
|
||||
|
||||
pub fn matches(&self, other: &ProjectGroupKey) -> bool {
|
||||
self.paths == other.paths
|
||||
&& same_remote_connection_identity(self.host.as_ref(), other.host.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_suffix(path: &Path, detail: usize) -> String {
|
||||
|
|
|
|||
|
|
@ -1666,6 +1666,35 @@ mod resolve_worktree_tests {
|
|||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_resolve_git_worktree_bare_repo_identity_path(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/monty/.bare",
|
||||
json!({
|
||||
"worktrees": {
|
||||
"feature-a": {
|
||||
"commondir": "../../",
|
||||
"HEAD": "ref: refs/heads/feature-a"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/monty/feature-a",
|
||||
json!({
|
||||
".git": "gitdir: /monty/.bare/worktrees/feature-a",
|
||||
"src": { "main.rs": "" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let result =
|
||||
resolve_git_worktree_to_main_repo(fs.as_ref(), Path::new("/monty/feature-a")).await;
|
||||
assert_eq!(result, Some(PathBuf::from("/monty")));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_resolve_git_worktree_no_git_returns_none(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ use ui::{
|
|||
use util::{ResultExt, paths::PathExt};
|
||||
use workspace::{
|
||||
HistoryManager, ModalView, MultiWorkspace, OpenMode, OpenOptions, OpenVisible, PathList,
|
||||
SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId,
|
||||
RecentWorkspace, SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId,
|
||||
notifications::DetachAndPromptErr, with_active_or_new_workspace,
|
||||
};
|
||||
use zed_actions::{OpenDevContainer, OpenRecent, OpenRemote};
|
||||
|
|
@ -102,13 +102,13 @@ pub async fn get_recent_projects(
|
|||
|
||||
let filtered: Vec<_> = workspaces
|
||||
.into_iter()
|
||||
.filter(|(id, _, _, _)| Some(*id) != current_workspace_id)
|
||||
.filter(|(_, location, _, _)| matches!(location, SerializedWorkspaceLocation::Local))
|
||||
.filter(|workspace| Some(workspace.workspace_id) != current_workspace_id)
|
||||
.filter(|workspace| matches!(workspace.location, SerializedWorkspaceLocation::Local))
|
||||
.collect();
|
||||
|
||||
let mut all_paths: Vec<PathBuf> = filtered
|
||||
.iter()
|
||||
.flat_map(|(_, _, path_list, _)| path_list.paths().iter().cloned())
|
||||
.flat_map(|workspace| workspace.identity_paths.paths().iter().cloned())
|
||||
.collect();
|
||||
all_paths.sort();
|
||||
all_paths.dedup();
|
||||
|
|
@ -121,9 +121,9 @@ pub async fn get_recent_projects(
|
|||
|
||||
let entries: Vec<RecentProjectEntry> = filtered
|
||||
.into_iter()
|
||||
.map(|(workspace_id, _, path_list, timestamp)| {
|
||||
let paths: Vec<PathBuf> = path_list.paths().to_vec();
|
||||
let ordered_paths: Vec<&PathBuf> = path_list.ordered_paths().collect();
|
||||
.map(|workspace| {
|
||||
let paths: Vec<PathBuf> = workspace.paths.paths().to_vec();
|
||||
let ordered_paths: Vec<&PathBuf> = workspace.identity_paths.ordered_paths().collect();
|
||||
|
||||
let name = ordered_paths
|
||||
.iter()
|
||||
|
|
@ -145,8 +145,8 @@ pub async fn get_recent_projects(
|
|||
name: SharedString::from(name),
|
||||
full_path: SharedString::from(full_path),
|
||||
paths,
|
||||
workspace_id,
|
||||
timestamp,
|
||||
workspace_id: workspace.workspace_id,
|
||||
timestamp: workspace.timestamp,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -614,7 +614,6 @@ impl RecentProjects {
|
|||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let workspaces = workspace::resolve_worktree_workspaces(workspaces, fs.as_ref()).await;
|
||||
this.update_in(cx, move |this, window, cx| {
|
||||
this.picker.update(cx, move |picker, cx| {
|
||||
picker.delegate.set_workspaces(workspaces);
|
||||
|
|
@ -773,11 +772,9 @@ impl RecentProjects {
|
|||
if let Some(ProjectPickerEntry::RecentProject(hit)) =
|
||||
picker.delegate.filtered_entries.get(ix)
|
||||
{
|
||||
if let Some((_, location, paths, _)) =
|
||||
picker.delegate.workspaces.get(hit.candidate_id)
|
||||
{
|
||||
if matches!(location, SerializedWorkspaceLocation::Local) {
|
||||
let paths_to_add = paths.paths().to_vec();
|
||||
if let Some(workspace) = picker.delegate.workspaces.get(hit.candidate_id) {
|
||||
if matches!(workspace.location, SerializedWorkspaceLocation::Local) {
|
||||
let paths_to_add = workspace.paths.paths().to_vec();
|
||||
picker
|
||||
.delegate
|
||||
.add_paths_to_project(paths_to_add, window, cx);
|
||||
|
|
@ -812,12 +809,7 @@ pub struct RecentProjectsDelegate {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
open_folders: Vec<OpenFolderEntry>,
|
||||
window_project_groups: Vec<ProjectGroupKey>,
|
||||
workspaces: Vec<(
|
||||
WorkspaceId,
|
||||
SerializedWorkspaceLocation,
|
||||
PathList,
|
||||
DateTime<Utc>,
|
||||
)>,
|
||||
workspaces: Vec<RecentWorkspace>,
|
||||
filtered_entries: Vec<ProjectPickerEntry>,
|
||||
selected_index: usize,
|
||||
render_paths: bool,
|
||||
|
|
@ -860,20 +852,12 @@ impl RecentProjectsDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_workspaces(
|
||||
&mut self,
|
||||
workspaces: Vec<(
|
||||
WorkspaceId,
|
||||
SerializedWorkspaceLocation,
|
||||
PathList,
|
||||
DateTime<Utc>,
|
||||
)>,
|
||||
) {
|
||||
pub fn set_workspaces(&mut self, workspaces: Vec<RecentWorkspace>) {
|
||||
self.workspaces = workspaces;
|
||||
let has_non_local_recent = !self
|
||||
.workspaces
|
||||
.iter()
|
||||
.all(|(_, location, _, _)| matches!(location, SerializedWorkspaceLocation::Local));
|
||||
.all(|workspace| matches!(workspace.location, SerializedWorkspaceLocation::Local));
|
||||
self.has_any_non_local_projects =
|
||||
self.project_connection_options.is_some() || has_non_local_recent;
|
||||
}
|
||||
|
|
@ -987,9 +971,10 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (id, _, paths, _))| self.is_valid_recent_candidate(*id, paths, cx))
|
||||
.map(|(id, (_, _, paths, _))| {
|
||||
let combined_string = paths
|
||||
.filter(|(_, workspace)| self.is_valid_recent_candidate(workspace, cx))
|
||||
.map(|(id, workspace)| {
|
||||
let combined_string = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
|
|
@ -1063,8 +1048,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
entries.push(ProjectPickerEntry::Header("Recent Projects".into()));
|
||||
|
||||
if is_empty_query {
|
||||
for (id, (workspace_id, _, paths, _)) in self.workspaces.iter().enumerate() {
|
||||
if self.is_valid_recent_candidate(*workspace_id, paths, cx) {
|
||||
for (id, workspace) in self.workspaces.iter().enumerate() {
|
||||
if self.is_valid_recent_candidate(workspace, cx) {
|
||||
entries.push(ProjectPickerEntry::RecentProject(StringMatch {
|
||||
candidate_id: id,
|
||||
score: 0.0,
|
||||
|
|
@ -1143,20 +1128,15 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some((
|
||||
candidate_workspace_id,
|
||||
candidate_workspace_location,
|
||||
candidate_workspace_paths,
|
||||
_,
|
||||
)) = self.workspaces.get(selected_match.candidate_id)
|
||||
let Some(candidate_workspace) = self.workspaces.get(selected_match.candidate_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let replace_current_window = self.create_new_window == secondary;
|
||||
let candidate_workspace_id = *candidate_workspace_id;
|
||||
let candidate_workspace_location = candidate_workspace_location.clone();
|
||||
let candidate_workspace_paths = candidate_workspace_paths.clone();
|
||||
let candidate_workspace_id = candidate_workspace.workspace_id;
|
||||
let candidate_workspace_location = candidate_workspace.location.clone();
|
||||
let candidate_workspace_paths = candidate_workspace.paths.clone();
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if workspace.database_id() == Some(candidate_workspace_id) {
|
||||
|
|
@ -1458,10 +1438,13 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
)
|
||||
}
|
||||
ProjectPickerEntry::RecentProject(hit) => {
|
||||
let (_, location, paths, _) = self.workspaces.get(hit.candidate_id)?;
|
||||
let workspace = self.workspaces.get(hit.candidate_id)?;
|
||||
let location = &workspace.location;
|
||||
let raw_paths = &workspace.paths;
|
||||
let identity_paths = &workspace.identity_paths;
|
||||
let is_local = matches!(location, SerializedWorkspaceLocation::Local);
|
||||
let paths_to_add = paths.paths().to_vec();
|
||||
let ordered_paths: Vec<_> = paths
|
||||
let paths_to_add = raw_paths.paths().to_vec();
|
||||
let ordered_paths: Vec<_> = identity_paths
|
||||
.ordered_paths()
|
||||
.map(|p| p.compact().to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
|
@ -1478,7 +1461,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
};
|
||||
|
||||
let mut path_start_offset = 0;
|
||||
let (match_labels, paths): (Vec<_>, Vec<_>) = paths
|
||||
let (match_labels, paths): (Vec<_>, Vec<_>) = identity_paths
|
||||
.ordered_paths()
|
||||
.map(|p| p.compact())
|
||||
.map(|path| {
|
||||
|
|
@ -1816,8 +1799,11 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
Some(ProjectPickerEntry::RecentProject(hit)) => self
|
||||
.workspaces
|
||||
.get(hit.candidate_id)
|
||||
.map(|(_, loc, ..)| {
|
||||
matches!(loc, SerializedWorkspaceLocation::Local)
|
||||
.map(|workspace| {
|
||||
matches!(
|
||||
workspace.location,
|
||||
SerializedWorkspaceLocation::Local
|
||||
)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
_ => false,
|
||||
|
|
@ -2038,22 +2024,23 @@ impl RecentProjectsDelegate {
|
|||
if let Some(ProjectPickerEntry::RecentProject(selected_match)) =
|
||||
self.filtered_entries.get(ix)
|
||||
{
|
||||
let (workspace_id, _, _, _) = &self.workspaces[selected_match.candidate_id];
|
||||
let workspace_id = *workspace_id;
|
||||
let recent_workspace = self.workspaces[selected_match.candidate_id].clone();
|
||||
let fs = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.map(|ws| ws.read(cx).app_state().fs.clone());
|
||||
let db = WorkspaceDb::global(cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
db.delete_workspace_by_id(workspace_id).await.log_err();
|
||||
let Some(fs) = fs else { return };
|
||||
let deleted_workspace_ids = db
|
||||
.delete_recent_workspace_group(&recent_workspace)
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let workspaces = db
|
||||
.recent_project_workspaces(fs.as_ref())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let workspaces =
|
||||
workspace::resolve_worktree_workspaces(workspaces, fs.as_ref()).await;
|
||||
this.update_in(cx, move |picker, window, cx| {
|
||||
picker.delegate.set_workspaces(workspaces);
|
||||
picker
|
||||
|
|
@ -2064,8 +2051,11 @@ impl RecentProjectsDelegate {
|
|||
// After deleting a project, we want to update the history manager to reflect the change.
|
||||
// But we do not emit a update event when user opens a project, because it's handled in `workspace::load_workspace`.
|
||||
if let Some(history_manager) = HistoryManager::global(cx) {
|
||||
history_manager
|
||||
.update(cx, |this, cx| this.delete_history(workspace_id, cx));
|
||||
history_manager.update(cx, |this, cx| {
|
||||
for workspace_id in &deleted_workspace_ids {
|
||||
this.delete_history(*workspace_id, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
|
@ -2118,10 +2108,10 @@ impl RecentProjectsDelegate {
|
|||
false
|
||||
}
|
||||
|
||||
fn is_in_current_window_groups(&self, paths: &PathList) -> bool {
|
||||
fn is_in_current_window_groups(&self, workspace: &RecentWorkspace) -> bool {
|
||||
self.window_project_groups
|
||||
.iter()
|
||||
.any(|key| key.path_list() == paths)
|
||||
.any(|key| key.matches(&workspace.project_group_key()))
|
||||
}
|
||||
|
||||
fn is_open_folder(&self, paths: &PathList) -> bool {
|
||||
|
|
@ -2142,13 +2132,12 @@ impl RecentProjectsDelegate {
|
|||
|
||||
fn is_valid_recent_candidate(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
paths: &PathList,
|
||||
workspace: &RecentWorkspace,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> bool {
|
||||
!self.is_current_workspace(workspace_id, cx)
|
||||
&& !self.is_in_current_window_groups(paths)
|
||||
&& !self.is_open_folder(paths)
|
||||
!self.is_current_workspace(workspace.workspace_id, cx)
|
||||
&& !self.is_in_current_window_groups(workspace)
|
||||
&& !self.is_open_folder(&workspace.paths)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use fuzzy_nucleo::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
|
|
@ -16,8 +15,8 @@ use ui::{ButtonLike, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}
|
|||
use ui_input::ErasedEditor;
|
||||
use util::{ResultExt, paths::PathExt};
|
||||
use workspace::{
|
||||
MultiWorkspace, OpenMode, OpenOptions, PathList, ProjectGroupKey, SerializedWorkspaceLocation,
|
||||
Workspace, WorkspaceDb, WorkspaceId, notifications::DetachAndPromptErr,
|
||||
MultiWorkspace, OpenMode, OpenOptions, ProjectGroupKey, RecentWorkspace,
|
||||
SerializedWorkspaceLocation, Workspace, WorkspaceDb, notifications::DetachAndPromptErr,
|
||||
};
|
||||
|
||||
use zed_actions::OpenRemote;
|
||||
|
|
@ -74,8 +73,6 @@ impl SidebarRecentProjects {
|
|||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let workspaces =
|
||||
workspace::resolve_worktree_workspaces(workspaces, fs.as_ref()).await;
|
||||
this.update_in(cx, move |this, window, cx| {
|
||||
this.picker.update(cx, move |picker, cx| {
|
||||
picker.delegate.set_workspaces(workspaces);
|
||||
|
|
@ -116,12 +113,7 @@ impl Render for SidebarRecentProjects {
|
|||
pub struct SidebarRecentProjectsDelegate {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window_project_groups: Vec<ProjectGroupKey>,
|
||||
workspaces: Vec<(
|
||||
WorkspaceId,
|
||||
SerializedWorkspaceLocation,
|
||||
PathList,
|
||||
DateTime<Utc>,
|
||||
)>,
|
||||
workspaces: Vec<RecentWorkspace>,
|
||||
filtered_workspaces: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
has_any_non_local_projects: bool,
|
||||
|
|
@ -129,18 +121,10 @@ pub struct SidebarRecentProjectsDelegate {
|
|||
}
|
||||
|
||||
impl SidebarRecentProjectsDelegate {
|
||||
pub fn set_workspaces(
|
||||
&mut self,
|
||||
workspaces: Vec<(
|
||||
WorkspaceId,
|
||||
SerializedWorkspaceLocation,
|
||||
PathList,
|
||||
DateTime<Utc>,
|
||||
)>,
|
||||
) {
|
||||
pub fn set_workspaces(&mut self, workspaces: Vec<RecentWorkspace>) {
|
||||
self.has_any_non_local_projects = workspaces
|
||||
.iter()
|
||||
.any(|(_, location, _, _)| !matches!(location, SerializedWorkspaceLocation::Local));
|
||||
.any(|workspace| !matches!(workspace.location, SerializedWorkspaceLocation::Local));
|
||||
self.workspaces = workspaces;
|
||||
}
|
||||
}
|
||||
|
|
@ -206,15 +190,16 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (id, _, paths, _))| {
|
||||
Some(*id) != current_workspace_id
|
||||
.filter(|(_, workspace)| {
|
||||
Some(workspace.workspace_id) != current_workspace_id
|
||||
&& !self
|
||||
.window_project_groups
|
||||
.iter()
|
||||
.any(|key| key.path_list() == paths)
|
||||
.any(|key| key.matches(&workspace.project_group_key()))
|
||||
})
|
||||
.map(|(id, (_, _, paths, _))| {
|
||||
let combined_string = paths
|
||||
.map(|(id, workspace)| {
|
||||
let combined_string = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
|
|
@ -251,9 +236,7 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
let Some(hit) = self.filtered_workspaces.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
let Some((_, location, candidate_workspace_paths, _)) =
|
||||
self.workspaces.get(hit.candidate_id)
|
||||
else {
|
||||
let Some(recent_workspace) = self.workspaces.get(hit.candidate_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
@ -261,10 +244,10 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
return;
|
||||
};
|
||||
|
||||
match location {
|
||||
match &recent_workspace.location {
|
||||
SerializedWorkspaceLocation::Local => {
|
||||
if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {
|
||||
let paths = candidate_workspace_paths.paths().to_vec();
|
||||
let paths = recent_workspace.paths.paths().to_vec();
|
||||
cx.defer(move |cx| {
|
||||
if let Some(task) = handle
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
|
|
@ -290,7 +273,7 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
crate::RemoteSettings::get_global(cx)
|
||||
.fill_connection_options_from_settings(connection);
|
||||
};
|
||||
let paths = candidate_workspace_paths.paths().to_vec();
|
||||
let paths = recent_workspace.paths.paths().to_vec();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
open_remote_project(connection.clone(), paths, app_state, open_options, cx)
|
||||
.await
|
||||
|
|
@ -326,14 +309,15 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let hit = self.filtered_workspaces.get(ix)?;
|
||||
let (_, location, paths, _) = self.workspaces.get(hit.candidate_id)?;
|
||||
let workspace = self.workspaces.get(hit.candidate_id)?;
|
||||
|
||||
let ordered_paths: Vec<_> = paths
|
||||
let ordered_paths: Vec<_> = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|p| p.compact().to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
let tooltip_path: SharedString = match &location {
|
||||
let tooltip_path: SharedString = match &workspace.location {
|
||||
SerializedWorkspaceLocation::Remote(options) => {
|
||||
let host = options.display_name();
|
||||
if ordered_paths.len() == 1 {
|
||||
|
|
@ -346,7 +330,8 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
};
|
||||
|
||||
let mut path_start_offset = 0;
|
||||
let match_labels: Vec<_> = paths
|
||||
let match_labels: Vec<_> = workspace
|
||||
.identity_paths
|
||||
.ordered_paths()
|
||||
.map(|p| p.compact())
|
||||
.map(|path| {
|
||||
|
|
@ -357,7 +342,7 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let prefix = match &location {
|
||||
let prefix = match &workspace.location {
|
||||
SerializedWorkspaceLocation::Remote(options) => {
|
||||
Some(SharedString::from(options.display_name()))
|
||||
}
|
||||
|
|
@ -371,7 +356,7 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
|
|||
active: false,
|
||||
};
|
||||
|
||||
let icon = icon_for_remote_connection(match location {
|
||||
let icon = icon_for_remote_connection(match &workspace.location {
|
||||
SerializedWorkspaceLocation::Local => None,
|
||||
SerializedWorkspaceLocation::Remote(options) => Some(options),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,9 +49,12 @@ impl HistoryManager {
|
|||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.filter_map(|(id, location, paths, _timestamp)| {
|
||||
if matches!(location, SerializedWorkspaceLocation::Local) {
|
||||
Some(HistoryManagerEntry::new(id, &paths))
|
||||
.filter_map(|workspace| {
|
||||
if matches!(workspace.location, SerializedWorkspaceLocation::Local) {
|
||||
Some(HistoryManagerEntry::new(
|
||||
workspace.workspace_id,
|
||||
&workspace.paths,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -130,6 +130,13 @@ pub(crate) struct SerializedWorkspace {
|
|||
pub(crate) id: WorkspaceId,
|
||||
pub(crate) location: SerializedWorkspaceLocation,
|
||||
pub(crate) paths: PathList,
|
||||
/// The workspace's main worktree paths at the time this workspace was saved.
|
||||
///
|
||||
/// These paths are used for grouping, deduping, and display in recent-workspace
|
||||
/// UIs. They are not authoritative for reopening the workspace, because they may
|
||||
/// become stale if the repository layout changes after the save. Use `paths` when
|
||||
/// reopening the workspace.
|
||||
pub(crate) identity_paths: Option<PathList>,
|
||||
pub(crate) center_group: SerializedPaneGroup,
|
||||
pub(crate) window_bounds: Option<SerializedWindowBounds>,
|
||||
pub(crate) centered_layout: bool,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use crate::{
|
||||
NewFile, Open, OpenMode, PathList, SerializedWorkspaceLocation, ToggleWorkspaceSidebar,
|
||||
Workspace, WorkspaceId,
|
||||
NewFile, Open, OpenMode, PathList, RecentWorkspace, SerializedWorkspaceLocation,
|
||||
ToggleWorkspaceSidebar, Workspace,
|
||||
item::{Item, ItemEvent},
|
||||
persistence::WorkspaceDb,
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use chrono::{DateTime, Utc};
|
||||
use git::Clone as GitClone;
|
||||
use gpui::{
|
||||
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
|
|
@ -242,14 +241,7 @@ pub struct WelcomePage {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
fallback_to_recent_projects: bool,
|
||||
recent_workspaces: Option<
|
||||
Vec<(
|
||||
WorkspaceId,
|
||||
SerializedWorkspaceLocation,
|
||||
PathList,
|
||||
DateTime<Utc>,
|
||||
)>,
|
||||
>,
|
||||
recent_workspaces: Option<Vec<RecentWorkspace>>,
|
||||
}
|
||||
|
||||
impl WelcomePage {
|
||||
|
|
@ -310,14 +302,11 @@ impl WelcomePage {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(recent_workspaces) = &self.recent_workspaces {
|
||||
if let Some((_workspace_id, location, paths, _timestamp)) =
|
||||
recent_workspaces.get(action.index)
|
||||
{
|
||||
let is_local = matches!(location, SerializedWorkspaceLocation::Local);
|
||||
if let Some(workspace) = recent_workspaces.get(action.index) {
|
||||
let is_local = matches!(workspace.location, SerializedWorkspaceLocation::Local);
|
||||
|
||||
if is_local {
|
||||
let paths = paths.clone();
|
||||
let paths = paths.paths().to_vec();
|
||||
let paths = workspace.paths.paths().to_vec();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
|
|
@ -433,8 +422,13 @@ impl Render for WelcomePage {
|
|||
.flatten()
|
||||
.take(5)
|
||||
.enumerate()
|
||||
.map(|(index, (_, loc, paths, _))| {
|
||||
self.render_recent_project(index, first_section_entries + index, loc, paths)
|
||||
.map(|(index, workspace)| {
|
||||
self.render_recent_project(
|
||||
index,
|
||||
first_section_entries + index,
|
||||
&workspace.location,
|
||||
&workspace.identity_paths,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
|
|
|||
|
|
@ -84,15 +84,15 @@ pub use pane_group::{
|
|||
ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
|
||||
SplitDirection,
|
||||
};
|
||||
use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
|
||||
pub use persistence::{
|
||||
WorkspaceDb, delete_unloaded_items,
|
||||
RecentWorkspace, WorkspaceDb, delete_unloaded_items,
|
||||
model::{
|
||||
DockData, DockStructure, ItemId, MultiWorkspaceState, SerializedMultiWorkspace,
|
||||
SerializedProjectGroup, SerializedWorkspaceLocation, SessionWorkspace,
|
||||
},
|
||||
read_serialized_multi_workspaces, resolve_worktree_workspaces,
|
||||
read_serialized_multi_workspaces,
|
||||
};
|
||||
use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
|
||||
use postage::stream::Stream;
|
||||
use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
|
|
@ -6743,11 +6743,13 @@ impl Workspace {
|
|||
let center_group = build_serialized_pane_group(&self.center.root, window, cx);
|
||||
let docks = build_serialized_docks(self, window, cx);
|
||||
let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
|
||||
let identity_paths_hint = self.project_group_key(cx).path_list().clone();
|
||||
|
||||
let serialized_workspace = SerializedWorkspace {
|
||||
id: database_id,
|
||||
location,
|
||||
paths,
|
||||
identity_paths: Some(identity_paths_hint),
|
||||
center_group,
|
||||
window_bounds,
|
||||
display: Default::default(),
|
||||
|
|
@ -8829,7 +8831,7 @@ pub async fn last_opened_workspace_location(
|
|||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
.map(|(id, location, paths, _timestamp)| (id, location, paths))
|
||||
.map(|workspace| (workspace.workspace_id, workspace.location, workspace.paths))
|
||||
}
|
||||
|
||||
pub async fn last_session_workspace_locations(
|
||||
|
|
@ -8950,7 +8952,7 @@ pub async fn apply_restored_multiworkspace_state(
|
|||
&& let Some(common_dir) =
|
||||
project::discover_root_repo_common_dir(path, fs.as_ref()).await
|
||||
{
|
||||
let main_path = common_dir.parent().unwrap_or(&common_dir);
|
||||
let main_path = project::repo_identity_path(&common_dir);
|
||||
resolved_paths.push(main_path.to_path_buf());
|
||||
} else {
|
||||
resolved_paths.push(path.to_path_buf());
|
||||
|
|
|
|||
Loading…
Reference in a new issue