From f0ad50b7fb47cf51964808f07a47cf9cfb694d84 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 11 Dec 2024 17:44:42 -0500 Subject: [PATCH 01/22] Add a test covering the git_status API we want for the git panel co-authored-by: Mikayla --- crates/git/src/repository.rs | 1 + crates/worktree/src/worktree.rs | 10 +++++ crates/worktree/src/worktree_tests.rs | 63 +++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index b37e517d43233..ccc72480c4ff0 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -394,6 +394,7 @@ pub enum GitFileStatus { Added, Modified, Conflict, + Deleted, } impl GitFileStatus { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 13f335334ae70..4a85b232d192d 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2202,6 +2202,10 @@ impl Snapshot { Some(removed_entry.path) } + pub fn git_status(&self, path: impl Into) -> impl Iterator { + todo!() + } + #[cfg(any(test, feature = "test-support"))] pub fn status_for_file(&self, path: impl Into) -> Option { let path = path.into(); @@ -3476,6 +3480,12 @@ pub struct Entry { pub is_fifo: bool, } +pub struct GitEntry { + pub path: Arc, + entry_id: Option, + git_status: GitFileStatus, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { UnloadedDir, diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 8b93396e24b95..e105203baaea3 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2523,6 +2523,69 @@ async fn test_git_status(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_git_status_git_panel(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let root = temp_tree(json!({ + "project": { + "a.txt": "a", // Modified + "b.txt": "bb", // Added + "c.txt": "ccc", // Unchanged + "d.txt": "dddd", // Deleted + }, + + })); + + let project_path = Path::new("project"); + + // Set up git repository before creating the worktree. + let work_dir = root.path().join("project"); + let mut repo = git_init(work_dir.as_path()); + git_add("a.txt", &repo); + git_add("c.txt", &repo); + git_add("d.txt", &repo); + git_commit("Initial commit", &repo); + std::fs::remove_file(work_dir.join("d.txt")).unwrap(); + std::fs::write(work_dir.join("a.txt"), "aa").unwrap(); + + let tree = Worktree::local( + root.path(), + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + tree.flush_fs_events(cx).await; + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.executor().run_until_parked(); + + // Check that the right git state is observed on startup + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (dir, _) = snapshot.repositories().next().unwrap(); + + // Takes a work directory, and returns all file entries with a git status. + let entries = snapshot.git_status(dir).collect::>(); + + assert_eq!(entries.len(), 3); + assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); + assert_eq!(entries[0].git_status, GitFileStatus::Modified); + assert!(entries[0].entry_id.is_some()); + assert_eq!(entries[1].path.as_ref(), Path::new("b.txt")); + assert_eq!(entries[1].git_status, GitFileStatus::Added); + assert!(entries[1].entry_id.is_some()); + assert_eq!(entries[2].path.as_ref(), Path::new("d.txt")); + assert_eq!(entries[2].git_status, GitFileStatus::Deleted); + assert!(entries[2].entry_id.is()); + }); +} + #[gpui::test] async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { init_test(cx); From 7b5a1e3f84da095236ff6c0c9ed693b702edde0d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 11 Dec 2024 16:03:57 -0800 Subject: [PATCH 02/22] Remove git statuses from the worktree entries, mark places that used to use it for refactoring co-authored-by: Cole --- crates/project/src/lsp_store.rs | 5 +- crates/project/src/project.rs | 5 +- crates/project_panel/src/project_panel.rs | 1 - crates/proto/proto/zed.proto | 3 +- crates/title_bar/src/title_bar.rs | 4 +- crates/worktree/src/worktree.rs | 308 +++++++++------------- crates/worktree/src/worktree_tests.rs | 9 +- 7 files changed, 137 insertions(+), 198 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 55abc6811e241..8522404da2586 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -87,9 +87,8 @@ pub use language::Location; #[cfg(any(test, feature = "test-support"))] pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use worktree::{ - Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry, - UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, - FS_WATCH_LATENCY, + Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet, + UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY, }; const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 50c9db8fb1aef..87c3ed1a7c7c6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -97,9 +97,8 @@ pub use task_inventory::{ BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind, }; pub use worktree::{ - Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry, - UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, - FS_WATCH_LATENCY, + Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet, + UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY, }; pub use buffer_store::ProjectTransaction; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 1f4fd50f41051..08561603efe1e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2374,7 +2374,6 @@ impl ProjectPanel { is_external: false, is_private: false, is_always_included: entry.is_always_included, - git_status: entry.git_status, canonical_path: entry.canonical_path.clone(), char_bag: entry.char_bag, is_fifo: entry.is_fifo, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 14dc999031236..9cad363109cca 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -1767,7 +1767,7 @@ message Entry { bool is_ignored = 7; bool is_external = 8; reserved 6; - optional GitStatus git_status = 9; + reserved 9; bool is_fifo = 10; optional uint64 size = 11; optional string canonical_path = 12; @@ -1787,6 +1787,7 @@ enum GitStatus { Added = 0; Modified = 1; Conflict = 2; + Deleted = 3; } message BufferState { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index b6e08e21262ef..39285ad7ec223 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -17,7 +17,7 @@ use gpui::{ Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; -use project::{Project, RepositoryEntry}; +use project::{Project, GitRepository}; use rpc::proto; use settings::Settings as _; use smallvec::SmallVec; @@ -432,7 +432,7 @@ impl TitleBar { let workspace = self.workspace.upgrade()?; let branch_name = entry .as_ref() - .and_then(RepositoryEntry::branch) + .and_then(GitRepository::branch) .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; Some( Button::new("project_branch_trigger", branch_name) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 4a85b232d192d..02794fd33df49 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -63,7 +63,7 @@ use std::{ }, time::{Duration, Instant}, }; -use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; +use sum_tree::{Bias, Edit, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ paths::{home_dir, PathMatcher, SanitizedPath}, @@ -171,6 +171,7 @@ pub struct Snapshot { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepositoryEntry { + pub(crate) git_entries_by_path: SumTree, pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, @@ -2202,16 +2203,21 @@ impl Snapshot { Some(removed_entry.path) } - pub fn git_status(&self, path: impl Into) -> impl Iterator { - todo!() + // TODO: revisit the vec + pub fn git_status<'a>(&'a self, work_dir: &'a impl AsRef) -> Option> { + let path = work_dir.as_ref(); + self.repository_for_path(&path) + .map(|repo| repo.git_entries_by_path.iter().cloned().collect()) } #[cfg(any(test, feature = "test-support"))] pub fn status_for_file(&self, path: impl Into) -> Option { let path = path.into(); - self.entries_by_path - .get(&PathKey(Arc::from(path)), &()) - .and_then(|entry| entry.git_status) + self.repository_for_path(&path).and_then(|repo| { + repo.git_entries_by_path + .get(&PathKey(Arc::from(path)), &()) + .map(|entry| entry.git_status) + }) } fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) { @@ -2294,6 +2300,7 @@ impl Snapshot { RepositoryEntry { work_directory: work_directory_entry, branch: repository.branch.map(Into::into), + git_entries_by_path: Default::default(), // When syncing repository entries from a peer, we don't need // the location_in_repo field, since git operations don't happen locally // anyway. @@ -2455,15 +2462,13 @@ impl Snapshot { /// Updates the `git_status` of the given entries such that files' /// statuses bubble up to their ancestor directories. pub fn propagate_git_statuses(&self, result: &mut [Entry]) { - let mut cursor = self - .entries_by_path - .cursor::<(TraversalProgress, GitStatuses)>(&()); - let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); + let mut cursor = self.entries_by_path.cursor::(&()); + let mut entry_stack = Vec::::new(); let mut result_ix = 0; loop { let next_entry = result.get(result_ix); - let containing_entry = entry_stack.last().map(|(ix, _)| &result[*ix]); + let containing_entry = entry_stack.last().map(|ix| &result[*ix]); let entry_to_finish = match (containing_entry, next_entry) { (Some(_), None) => entry_stack.pop(), @@ -2478,24 +2483,25 @@ impl Snapshot { (None, None) => break, }; - if let Some((entry_ix, prev_statuses)) = entry_to_finish { + if let Some(entry_ix) = entry_to_finish { cursor.seek_forward( &TraversalTarget::PathSuccessor(&result[entry_ix].path), Bias::Left, &(), ); - let statuses = cursor.start().1 - prev_statuses; - - result[entry_ix].git_status = if statuses.conflict > 0 { - Some(GitFileStatus::Conflict) - } else if statuses.modified > 0 { - Some(GitFileStatus::Modified) - } else if statuses.added > 0 { - Some(GitFileStatus::Added) - } else { - None - }; + // let statuses = cursor.start().1 - prev_statuses; + + // TODO: recreate this behavior, using the GitEntrySummary and the PathSuccessor target + // result[entry_ix].git_status = if statuses.conflict > 0 { + // Some(GitFileStatus::Conflict) + // } else if statuses.modified > 0 { + // Some(GitFileStatus::Modified) + // } else if statuses.added > 0 { + // Some(GitFileStatus::Added) + // } else { + // None + // }; } else { if result[result_ix].is_dir() { cursor.seek_forward( @@ -2503,7 +2509,7 @@ impl Snapshot { Bias::Left, &(), ); - entry_stack.push((result_ix, cursor.start().1)); + // entry_stack.push((result_ix, cursor.start().1)); } result_ix += 1; } @@ -3153,6 +3159,8 @@ impl BackgroundScannerState { RepositoryEntry { work_directory: work_dir_id.into(), branch: repository.branch_name().map(Into::into), + // TODO: Fill in this data structure + git_entries_by_path: Default::default(), location_in_repo, }, ); @@ -3471,7 +3479,7 @@ pub struct Entry { /// directory is expanded. External entries are treated like gitignored /// entries in that they are not included in searches. pub is_external: bool, - pub git_status: Option, + /// Whether this entry is considered to be a `.env` file. pub is_private: bool, /// The entry's size on disk, in bytes. @@ -3480,12 +3488,6 @@ pub struct Entry { pub is_fifo: bool, } -pub struct GitEntry { - pub path: Arc, - entry_id: Option, - git_status: GitFileStatus, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { UnloadedDir, @@ -3518,6 +3520,60 @@ pub struct GitRepositoryChange { pub type UpdatedEntriesSet = Arc<[(Arc, ProjectEntryId, PathChange)]>; pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GitEntry { + pub path: Arc, + entry_id: Option, + git_status: GitFileStatus, +} + +#[derive(Clone, Debug)] +pub struct GitEntrySummary { + max_path: Arc, +} + +impl sum_tree::Summary for GitEntrySummary { + type Context = (); + + fn zero(_cx: &Self::Context) -> Self { + GitEntrySummary { + max_path: Arc::from(Path::new("")), + } + } + + fn add_summary(&mut self, rhs: &Self, _: &Self::Context) { + self.max_path = rhs.max_path.clone(); + } +} + +impl sum_tree::Item for GitEntry { + type Summary = GitEntrySummary; + + fn summary(&self, _: &::Context) -> Self::Summary { + GitEntrySummary { + max_path: self.path.clone(), + } + } +} + +impl sum_tree::KeyedItem for GitEntry { + type Key = PathKey; + + fn key(&self) -> Self::Key { + PathKey(self.path.clone()) + } +} + +impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for PathKey { + fn zero(_cx: &()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a GitEntrySummary, _: &()) { + self.0 = summary.max_path.clone(); + } +} + impl Entry { fn new( path: Arc, @@ -3543,7 +3599,6 @@ impl Entry { is_always_included: false, is_external: false, is_private: false, - git_status: None, char_bag, is_fifo: metadata.is_fifo, } @@ -3560,10 +3615,6 @@ impl Entry { pub fn is_file(&self) -> bool { self.kind.is_file() } - - pub fn git_status(&self) -> Option { - self.git_status - } } impl EntryKind { @@ -3603,22 +3654,12 @@ impl sum_tree::Item for Entry { non_ignored_file_count = 0; } - let mut statuses = GitStatuses::default(); - if let Some(status) = self.git_status { - match status { - GitFileStatus::Added => statuses.added = 1, - GitFileStatus::Modified => statuses.modified = 1, - GitFileStatus::Conflict => statuses.conflict = 1, - } - } - EntrySummary { max_path: self.path.clone(), count: 1, non_ignored_count, file_count, non_ignored_file_count, - statuses, } } } @@ -3638,7 +3679,6 @@ pub struct EntrySummary { non_ignored_count: usize, file_count: usize, non_ignored_file_count: usize, - statuses: GitStatuses, } impl Default for EntrySummary { @@ -3649,7 +3689,6 @@ impl Default for EntrySummary { non_ignored_count: 0, file_count: 0, non_ignored_file_count: 0, - statuses: Default::default(), } } } @@ -3667,7 +3706,6 @@ impl sum_tree::Summary for EntrySummary { self.non_ignored_count += rhs.non_ignored_count; self.file_count += rhs.file_count; self.non_ignored_file_count += rhs.non_ignored_file_count; - self.statuses += rhs.statuses; } } @@ -4391,7 +4429,8 @@ impl BackgroundScanner { if let Some(repo) = &containing_repository { if let Ok(repo_path) = child_entry.path.strip_prefix(&repo.work_directory) { let repo_path = RepoPath(repo_path.into()); - child_entry.git_status = repo.statuses.get(&repo_path); + // TODO: Figure out what to do here + // child_entry.git_status = repo.statuses.get(&repo_path); } } } @@ -4496,30 +4535,31 @@ impl BackgroundScanner { } // Group all relative paths by their git repository. - let mut paths_by_git_repo = HashMap::default(); - for relative_path in relative_paths.iter() { - if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(relative_path) { - if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) { - paths_by_git_repo - .entry(repo.dot_git_dir_abs_path.clone()) - .or_insert_with(|| RepoPaths { - repo: repo.repo_ptr.clone(), - repo_paths: Vec::new(), - relative_paths: Vec::new(), - }) - .add_paths(relative_path, repo_path); - } - } - } - + // let mut paths_by_git_repo = HashMap::default(); + // for relative_path in relative_paths.iter() { + // if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(relative_path) { + // if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) { + // paths_by_git_repo + // .entry(repo.dot_git_dir_abs_path.clone()) + // .or_insert_with(|| RepoPaths { + // repo: repo.repo_ptr.clone(), + // repo_paths: Vec::new(), + // relative_paths: Vec::new(), + // }) + // .add_paths(relative_path, repo_path); + // } + // } + // } + + // TODO: Move this to where it should be for the new data structure // Now call `git status` once per repository and collect each file's git status. - let mut git_statuses_by_relative_path = - paths_by_git_repo - .into_values() - .fold(HashMap::default(), |mut map, repo_paths| { - map.extend(repo_paths.into_git_file_statuses()); - map - }); + // let mut git_statuses_by_relative_path = + // paths_by_git_repo + // .into_values() + // .fold(HashMap::default(), |mut map, repo_paths| { + // map.extend(repo_paths.into_git_file_statuses()); + // map + // }); for (path, metadata) in relative_paths.iter().zip(metadata.into_iter()) { let abs_path: Arc = root_abs_path.join(path).into(); @@ -4558,9 +4598,10 @@ impl BackgroundScanner { } } - if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external { - fs_entry.git_status = git_statuses_by_relative_path.remove(path); - } + // Old usage of git_statuses_by_relative_path + // if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external { + // fs_entry.git_status = git_statuses_by_relative_path.remove(path); + // } state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); } @@ -4739,7 +4780,8 @@ impl BackgroundScanner { .status(&[repo_path.0.clone()]) .ok() .and_then(|status| status.get(&repo_path)); - entry.git_status = status; + // TODO: figure out what to do here + // entry.git_status = status; } } } @@ -4872,7 +4914,8 @@ impl BackgroundScanner { .scoped(|scope| { scope.spawn(async { for repo_update in repo_updates { - self.update_git_statuses(repo_update); + // TODO: Switch this to calling the right API + // self.update_git_statuses(repo_update); } updates_done_tx.blocking_send(()).ok(); }); @@ -4896,65 +4939,6 @@ impl BackgroundScanner { .await; } - /// Update the git statuses for a given batch of entries. - fn update_git_statuses(&self, job: UpdateGitStatusesJob) { - log::trace!("updating git statuses for repo {:?}", job.work_directory.0); - let t0 = Instant::now(); - let Some(statuses) = job.repository.status(&[PathBuf::from("")]).log_err() else { - return; - }; - log::trace!( - "computed git statuses for repo {:?} in {:?}", - job.work_directory.0, - t0.elapsed() - ); - - let t0 = Instant::now(); - let mut changes = Vec::new(); - let snapshot = self.state.lock().snapshot.snapshot.clone(); - for file in snapshot.traverse_from_path(true, false, false, job.work_directory.0.as_ref()) { - let Ok(repo_path) = file.path.strip_prefix(&job.work_directory.0) else { - break; - }; - let git_status = if let Some(location) = &job.location_in_repo { - statuses.get(&location.join(repo_path)) - } else { - statuses.get(repo_path) - }; - if file.git_status != git_status { - let mut entry = file.clone(); - entry.git_status = git_status; - changes.push((entry.path, git_status)); - } - } - - let mut state = self.state.lock(); - let edits = changes - .iter() - .filter_map(|(path, git_status)| { - let entry = state.snapshot.entry_for_path(path)?.clone(); - Some(Edit::Insert(Entry { - git_status: *git_status, - ..entry.clone() - })) - }) - .collect(); - - // Apply the git status changes. - util::extend_sorted( - &mut state.changed_paths, - changes.iter().map(|p| p.0.clone()), - usize::MAX, - Ord::cmp, - ); - state.snapshot.entries_by_path.edit(edits, &()); - log::trace!( - "applied git status updates for repo {:?} in {:?}", - job.work_directory.0, - t0.elapsed(), - ); - } - fn build_change_set( &self, old_snapshot: &Snapshot, @@ -5344,43 +5328,6 @@ impl<'a> Default for TraversalProgress<'a> { } } -#[derive(Clone, Debug, Default, Copy)] -struct GitStatuses { - added: usize, - modified: usize, - conflict: usize, -} - -impl AddAssign for GitStatuses { - fn add_assign(&mut self, rhs: Self) { - self.added += rhs.added; - self.modified += rhs.modified; - self.conflict += rhs.conflict; - } -} - -impl Sub for GitStatuses { - type Output = GitStatuses; - - fn sub(self, rhs: Self) -> Self::Output { - GitStatuses { - added: self.added - rhs.added, - modified: self.modified - rhs.modified, - conflict: self.conflict - rhs.conflict, - } - } -} - -impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses { - fn zero(_cx: &()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { - *self += summary.statuses - } -} - pub struct Traversal<'a> { cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>, include_ignored: bool, @@ -5519,14 +5466,6 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa } } -impl<'a, 'b> SeekTarget<'a, EntrySummary, (TraversalProgress<'a>, GitStatuses)> - for TraversalTarget<'b> -{ - fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering { - self.cmp(&cursor_location.0, &()) - } -} - pub struct ChildEntriesIter<'a> { parent_path: &'a Path, traversal: Traversal<'a>, @@ -5556,7 +5495,6 @@ impl<'a> From<&'a Entry> for proto::Entry { mtime: entry.mtime.map(|time| time.into()), is_ignored: entry.is_ignored, is_external: entry.is_external, - git_status: entry.git_status.map(git_status_to_proto), is_fifo: entry.is_fifo, size: Some(entry.size), canonical_path: entry @@ -5593,7 +5531,7 @@ impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry { is_ignored: entry.is_ignored, is_always_included: always_included.is_match(path.as_ref()), is_external: entry.is_external, - git_status: git_status_from_proto(entry.git_status), + // git_status: git_status_from_proto(entry.git_status), is_private: false, char_bag, is_fifo: entry.is_fifo, @@ -5607,6 +5545,7 @@ fn git_status_from_proto(git_status: Option) -> Option { proto::GitStatus::Added => GitFileStatus::Added, proto::GitStatus::Modified => GitFileStatus::Modified, proto::GitStatus::Conflict => GitFileStatus::Conflict, + proto::GitStatus::Deleted => GitFileStatus::Deleted, }) }) } @@ -5616,6 +5555,7 @@ fn git_status_to_proto(status: GitFileStatus) -> i32 { GitFileStatus::Added => proto::GitStatus::Added as i32, GitFileStatus::Modified => proto::GitStatus::Modified as i32, GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, + GitFileStatus::Deleted => proto::GitStatus::Deleted as i32, } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index e105203baaea3..83a171d3b6a90 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2571,7 +2571,7 @@ async fn test_git_status_git_panel(cx: &mut TestAppContext) { let (dir, _) = snapshot.repositories().next().unwrap(); // Takes a work directory, and returns all file entries with a git status. - let entries = snapshot.git_status(dir).collect::>(); + let entries = snapshot.git_status(dir).unwrap(); assert_eq!(entries.len(), 3); assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); @@ -2582,7 +2582,7 @@ async fn test_git_status_git_panel(cx: &mut TestAppContext) { assert!(entries[1].entry_id.is_some()); assert_eq!(entries[2].path.as_ref(), Path::new("d.txt")); assert_eq!(entries[2].git_status, GitFileStatus::Deleted); - assert!(entries[2].entry_id.is()); + assert!(entries[2].entry_id.is_none()); }); } @@ -2811,7 +2811,7 @@ fn check_propagated_statuses( assert_eq!( entries .iter() - .map(|e| (e.path.as_ref(), e.git_status)) + .map(|e| (e.path.as_ref(), snapshot.status_for_file(e.path.as_ref()))) .collect::>(), expected_statuses ); @@ -2963,7 +2963,8 @@ fn assert_entry_git_state( ) { let entry = tree.entry_for_path(path).expect("entry {path} not found"); assert_eq!( - entry.git_status, git_status, + tree.status_for_file(Path::new(path)), + git_status, "expected {path} to have git status: {git_status:?}" ); assert_eq!( From f7452b44bd88dae954e651e2abf78f143d235007 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 16 Dec 2024 11:36:12 -0800 Subject: [PATCH 03/22] Get initial implementation of the new status checking code co-authored-by: Cole --- crates/git/src/git.rs | 1 + crates/git/src/repository.rs | 20 +-- crates/git/src/status.rs | 15 +-- crates/worktree/src/worktree.rs | 174 ++++++++++++++++++++------ crates/worktree/src/worktree_tests.rs | 4 +- 5 files changed, 153 insertions(+), 61 deletions(-) diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index cf07b74ac5d8d..c608c23cf357d 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -16,6 +16,7 @@ use std::sync::LazyLock; pub use crate::hosting_provider::*; pub use crate::remote::*; pub use git2 as libgit; +pub use repository::WORK_DIRECTORY_REPO_PATH; pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git")); pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies")); diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index ccc72480c4ff0..85145e2640a4b 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -7,6 +7,7 @@ use gpui::SharedString; use parking_lot::Mutex; use rope::Rope; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use std::{ cmp::Ordering, path::{Component, Path, PathBuf}, @@ -37,7 +38,7 @@ pub trait GitRepository: Send + Sync { /// Returns the SHA of the current HEAD. fn head_sha(&self) -> Option; - fn status(&self, path_prefixes: &[PathBuf]) -> Result; + fn status(&self, path_prefixes: &[RepoPath]) -> Result; fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; @@ -132,7 +133,7 @@ impl GitRepository for RealGitRepository { Some(self.repository.lock().head().ok()?.target()?.to_string()) } - fn status(&self, path_prefixes: &[PathBuf]) -> Result { + fn status(&self, path_prefixes: &[RepoPath]) -> Result { let working_directory = self .repository .lock() @@ -289,7 +290,7 @@ impl GitRepository for FakeGitRepository { state.dot_git_dir.clone() } - fn status(&self, path_prefixes: &[PathBuf]) -> Result { + fn status(&self, path_prefixes: &[RepoPath]) -> Result { let state = self.state.lock(); let mut entries = state .worktree_statuses @@ -422,20 +423,23 @@ impl GitFileStatus { } } +pub static WORK_DIRECTORY_REPO_PATH: LazyLock = + LazyLock::new(|| RepoPath(Path::new("").into())); + #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] -pub struct RepoPath(pub PathBuf); +pub struct RepoPath(pub Arc); impl RepoPath { pub fn new(path: PathBuf) -> Self { debug_assert!(path.is_relative(), "Repo paths must be relative"); - RepoPath(path) + RepoPath(path.into()) } } impl From<&Path> for RepoPath { fn from(value: &Path) -> Self { - RepoPath::new(value.to_path_buf()) + RepoPath::new(value.into()) } } @@ -447,7 +451,7 @@ impl From for RepoPath { impl Default for RepoPath { fn default() -> Self { - RepoPath(PathBuf::new()) + RepoPath(Path::new("").into()) } } @@ -458,7 +462,7 @@ impl AsRef for RepoPath { } impl std::ops::Deref for RepoPath { - type Target = PathBuf; + type Target = Path; fn deref(&self) -> &Self::Target { &self.0 diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index f8ffdc6714b5b..deeb65ffe7da6 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -1,10 +1,6 @@ use crate::repository::{GitFileStatus, RepoPath}; use anyhow::{anyhow, Result}; -use std::{ - path::{Path, PathBuf}, - process::Stdio, - sync::Arc, -}; +use std::{path::Path, process::Stdio, sync::Arc}; #[derive(Clone)] pub struct GitStatus { @@ -15,7 +11,7 @@ impl GitStatus { pub(crate) fn new( git_binary: &Path, working_directory: &Path, - path_prefixes: &[PathBuf], + path_prefixes: &[RepoPath], ) -> Result { let child = util::command::new_std_command(git_binary) .current_dir(working_directory) @@ -27,7 +23,7 @@ impl GitStatus { "-z", ]) .args(path_prefixes.iter().map(|path_prefix| { - if *path_prefix == Path::new("") { + if path_prefix.0.as_ref() == Path::new("") { Path::new(".") } else { path_prefix @@ -55,10 +51,11 @@ impl GitStatus { let (status, path) = entry.split_at(3); let status = status.trim(); Some(( - RepoPath(PathBuf::from(path)), + RepoPath(Path::new(path).into()), match status { "A" | "??" => GitFileStatus::Added, "M" => GitFileStatus::Modified, + "D" => GitFileStatus::Deleted, _ => return None, }, )) @@ -75,7 +72,7 @@ impl GitStatus { pub fn get(&self, path: &Path) -> Option { self.entries - .binary_search_by(|(repo_path, _)| repo_path.0.as_path().cmp(path)) + .binary_search_by(|(repo_path, _)| repo_path.0.as_ref().cmp(path)) .ok() .map(|index| self.entries[index].1) } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 02794fd33df49..a786ab7872929 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -54,7 +54,7 @@ use std::{ fmt, future::Future, mem, - ops::{AddAssign, Deref, DerefMut, Sub}, + ops::{Deref, DerefMut}, path::{Path, PathBuf}, pin::Pin, sync::{ @@ -171,6 +171,25 @@ pub struct Snapshot { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepositoryEntry { + /// The git status entries for this repository. + /// Note that the paths on this repository are relative to the git work directory. + /// If the .git folder is external to Zed, these paths will be relative to that folder, + /// and this data structure might reference files external to this worktree. + /// + /// For example: + /// + /// my_root_folder/ <-- repository root + /// .git + /// my_sub_folder_1/ + /// project_root/ <-- Project root, Zed opened here + /// changed_file_1 <-- File with changes, in worktree + /// my_sub_folder_2/ + /// changed_file_2 <-- File with changes, out of worktree + /// ... + /// + /// With this setup, this field would contain 2 entries, like so: + /// - my_sub_folder_1/project_root/changed_file_1 + /// - my_sub_folder_2/changed_file_2 pub(crate) git_entries_by_path: SumTree, pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, @@ -2408,10 +2427,11 @@ impl Snapshot { } /// Get the repository whose work directory contains the given path. - pub fn repository_for_work_directory(&self, path: &Path) -> Option { - self.repository_entries - .get(&RepositoryWorkDirectory(path.into())) - .cloned() + pub fn repository_for_work_directory( + &self, + path: &RepositoryWorkDirectory, + ) -> Option { + self.repository_entries.get(path).cloned() } /// Get the repository whose work directory contains the given path. @@ -2892,7 +2912,7 @@ impl BackgroundScannerState { work_directory: workdir_path, statuses: repo .repo_ptr - .status(&[repo_path.0]) + .status(&[repo_path]) .log_err() .unwrap_or_default(), }); @@ -2910,7 +2930,6 @@ impl BackgroundScannerState { scan_queue: scan_job_tx.clone(), ancestor_inodes, is_external: entry.is_external, - containing_repository, }) .unwrap(); } @@ -3522,7 +3541,7 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; #[derive(Clone, Debug, PartialEq, Eq)] pub struct GitEntry { - pub path: Arc, + pub path: RepoPath, entry_id: Option, git_status: GitFileStatus, } @@ -3551,7 +3570,7 @@ impl sum_tree::Item for GitEntry { fn summary(&self, _: &::Context) -> Self::Summary { GitEntrySummary { - max_path: self.path.clone(), + max_path: self.path.0.clone(), } } } @@ -3560,7 +3579,7 @@ impl sum_tree::KeyedItem for GitEntry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.path.clone()) + PathKey(self.path.0.clone()) } } @@ -4273,7 +4292,6 @@ impl BackgroundScanner { let next_entry_id = self.next_entry_id.clone(); let mut ignore_stack = job.ignore_stack.clone(); - let mut containing_repository = job.containing_repository.clone(); let mut new_ignore = None; let mut root_canonical_path = None; let mut new_entries: Vec = Vec::new(); @@ -4311,15 +4329,10 @@ impl BackgroundScanner { ); if let Some((work_directory, repository)) = repo { - let t0 = Instant::now(); - let statuses = repository - .status(&[PathBuf::from("")]) - .log_err() - .unwrap_or_default(); - log::trace!("computed git status in {:?}", t0.elapsed()); - containing_repository = Some(ScanJobContainingRepository { + self.update_git_statuses(UpdateGitStatusesJob { + location_in_repo: None, work_directory, - statuses, + repository, }); } } else if child_name == *GITIGNORE { @@ -4419,21 +4432,11 @@ impl BackgroundScanner { }, ancestor_inodes, scan_queue: job.scan_queue.clone(), - containing_repository: containing_repository.clone(), })); } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); child_entry.is_always_included = self.settings.is_path_always_included(&child_path); - if !child_entry.is_ignored { - if let Some(repo) = &containing_repository { - if let Ok(repo_path) = child_entry.path.strip_prefix(&repo.work_directory) { - let repo_path = RepoPath(repo_path.into()); - // TODO: Figure out what to do here - // child_entry.git_status = repo.statuses.get(&repo_path); - } - } - } } { @@ -4468,6 +4471,11 @@ impl BackgroundScanner { .always_included_entries .push(entry.path.clone()); } + // TODO: do this below + // Find this entrie's git repository + // Find this git repository's gitEntry for this + // Insert the new project id + // TODO: Track down other file entry creation and deletion phases, and make sure we clean up the git statuses as we go along } state.populate_dir(&job.path, new_entries, new_ignore); @@ -4606,7 +4614,10 @@ impl BackgroundScanner { state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); } Ok(None) => { - self.remove_repo_path(path, &mut state.snapshot); + self.remove_repo_path( + &RepositoryWorkDirectory(path.clone()), + &mut state.snapshot, + ); } Err(err) => { log::error!("error reading file {abs_path:?} on event: {err:#}"); @@ -4622,18 +4633,20 @@ impl BackgroundScanner { ); } - fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { + fn remove_repo_path( + &self, + path: &RepositoryWorkDirectory, + snapshot: &mut LocalSnapshot, + ) -> Option<()> { if !path + .0 .components() .any(|component| component.as_os_str() == *DOT_GIT) { if let Some(repository) = snapshot.repository_for_work_directory(path) { let entry = repository.work_directory.0; snapshot.git_repositories.remove(&entry); - snapshot - .snapshot - .repository_entries - .remove(&RepositoryWorkDirectory(path.into())); + snapshot.snapshot.repository_entries.remove(path); return Some(()); } } @@ -4777,7 +4790,7 @@ impl BackgroundScanner { if let Ok(repo_path) = repo_entry.relativize(snapshot, &entry.path) { let status = local_repo .repo_ptr - .status(&[repo_path.0.clone()]) + .status(&[repo_path.clone()]) .ok() .and_then(|status| status.get(&repo_path)); // TODO: figure out what to do here @@ -4914,8 +4927,8 @@ impl BackgroundScanner { .scoped(|scope| { scope.spawn(async { for repo_update in repo_updates { - // TODO: Switch this to calling the right API - // self.update_git_statuses(repo_update); + dbg!("update git status for ", &repo_update.work_directory); + self.update_git_statuses(repo_update); } updates_done_tx.blocking_send(()).ok(); }); @@ -4939,6 +4952,86 @@ impl BackgroundScanner { .await; } + /// Update the git statuses for a given batch of entries. + fn update_git_statuses(&self, job: UpdateGitStatusesJob) { + log::trace!("updating git statuses for repo {:?}", job.work_directory.0); + let t0 = Instant::now(); + + let Some(statuses) = job + .repository + .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()]) + .log_err() + else { + return; + }; + log::trace!( + "computed git statuses for repo {:?} in {:?}", + job.work_directory.0, + t0.elapsed() + ); + + let t0 = Instant::now(); + let mut changed_paths = Vec::new(); + let snapshot = self.state.lock().snapshot.snapshot.clone(); + + let Some(mut repository_entry) = + snapshot.repository_for_work_directory(&job.work_directory) + else { + log::error!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot"); + debug_assert!(false); + return; + }; + + let mut new_entries_by_path = SumTree::new(&()); + for (path, status) in statuses.entries.iter() { + let project_path: Option> = if let Some(location) = &job.location_in_repo { + // If we fail to strip the prefix, that means this status entry is + // external to this worktree, and we definitely won't have an entry_id + path.strip_prefix(location).ok().map(Into::into) + } else { + Some(job.work_directory.0.join(path).into()) + }; + + let entry_id = project_path + .as_ref() + .and_then(|path| snapshot.entry_for_path(path)) + .map(|entry| entry.id); + + new_entries_by_path.insert_or_replace( + GitEntry { + entry_id, + path: path.clone(), + git_status: *status, + }, + &(), + ); + + if let Some(path) = project_path { + changed_paths.push(path); + } + } + + repository_entry.git_entries_by_path = new_entries_by_path; + let mut state = self.state.lock(); + state + .snapshot + .repository_entries + .insert(job.work_directory.clone(), repository_entry); + + util::extend_sorted( + &mut state.changed_paths, + changed_paths.into_iter(), + usize::MAX, + Ord::cmp, + ); + + log::trace!( + "applied git status updates for repo {:?} in {:?}", + job.work_directory.0, + t0.elapsed(), + ); + } + fn build_change_set( &self, old_snapshot: &Snapshot, @@ -5107,13 +5200,13 @@ fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { struct RepoPaths { repo: Arc, relative_paths: Vec>, - repo_paths: Vec, + repo_paths: Vec, } impl RepoPaths { fn add_paths(&mut self, relative_path: &Arc, repo_path: RepoPath) { self.relative_paths.push(relative_path.clone()); - self.repo_paths.push(repo_path.0); + self.repo_paths.push(repo_path); } fn into_git_file_statuses(self) -> HashMap, GitFileStatus> { @@ -5136,7 +5229,6 @@ struct ScanJob { scan_queue: Sender, ancestor_inodes: TreeSet, is_external: bool, - containing_repository: Option, } #[derive(Clone)] diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 83a171d3b6a90..31eeef7f573dc 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2538,11 +2538,9 @@ async fn test_git_status_git_panel(cx: &mut TestAppContext) { })); - let project_path = Path::new("project"); - // Set up git repository before creating the worktree. let work_dir = root.path().join("project"); - let mut repo = git_init(work_dir.as_path()); + let repo = git_init(work_dir.as_path()); git_add("a.txt", &repo); git_add("c.txt", &repo); git_add("d.txt", &repo); From cf69b21dc4565a3898495579b890a76dc73857c8 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 16 Dec 2024 17:04:00 -0500 Subject: [PATCH 04/22] Work on git panel test some more Co-authored-by: Mikayla Co-authored-by: Nathan --- crates/worktree/src/worktree.rs | 140 +++++++++++++++----------- crates/worktree/src/worktree_tests.rs | 77 ++++++++++++-- 2 files changed, 153 insertions(+), 64 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a786ab7872929..3aea2d3284dee 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -30,6 +30,7 @@ use gpui::{ }; use ignore::IgnoreStack; use language::DiskState; +use log::debug; use parking_lot::Mutex; use paths::local_settings_folder_relative_path; use postage::{ @@ -2440,6 +2441,18 @@ impl Snapshot { .map(|e| e.1) } + pub(crate) fn insert_repository_entry(&mut self, repository_entry: RepositoryEntry) { + let Some(entry) = self.entry_for_id(repository_entry.work_directory.0) else { + log::error!("Attempting to insert a repository without the corresponding worktree entry for it's work directory"); + debug_assert!(false); + return; + }; + self.repository_entries.insert( + RepositoryWorkDirectory(entry.path.clone()), + repository_entry, + ); + } + pub fn repository_and_work_directory_for_path( &self, path: &Path, @@ -2939,7 +2952,7 @@ impl BackgroundScannerState { if let Some(mtime) = entry.mtime { // If an entry with the same inode was removed from the worktree during this scan, // then it *might* represent the same file or directory. But the OS might also have - // re-used the inode for a completely different file or directory. + // r*e-used the inode for a completely different file or directory. // // Conditionally reuse the old entry's id: // * if the mtime is the same, the file was probably been renamed. @@ -3178,7 +3191,6 @@ impl BackgroundScannerState { RepositoryEntry { work_directory: work_dir_id.into(), branch: repository.branch_name().map(Into::into), - // TODO: Fill in this data structure git_entries_by_path: Default::default(), location_in_repo, }, @@ -3542,7 +3554,6 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; #[derive(Clone, Debug, PartialEq, Eq)] pub struct GitEntry { pub path: RepoPath, - entry_id: Option, git_status: GitFileStatus, } @@ -4471,11 +4482,6 @@ impl BackgroundScanner { .always_included_entries .push(entry.path.clone()); } - // TODO: do this below - // Find this entrie's git repository - // Find this git repository's gitEntry for this - // Insert the new project id - // TODO: Track down other file entry creation and deletion phases, and make sure we clean up the git statuses as we go along } state.populate_dir(&job.path, new_entries, new_ignore); @@ -4543,31 +4549,76 @@ impl BackgroundScanner { } // Group all relative paths by their git repository. - // let mut paths_by_git_repo = HashMap::default(); - // for relative_path in relative_paths.iter() { - // if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(relative_path) { - // if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) { - // paths_by_git_repo - // .entry(repo.dot_git_dir_abs_path.clone()) - // .or_insert_with(|| RepoPaths { - // repo: repo.repo_ptr.clone(), - // repo_paths: Vec::new(), - // relative_paths: Vec::new(), - // }) - // .add_paths(relative_path, repo_path); - // } - // } - // } - - // TODO: Move this to where it should be for the new data structure - // Now call `git status` once per repository and collect each file's git status. - // let mut git_statuses_by_relative_path = - // paths_by_git_repo - // .into_values() - // .fold(HashMap::default(), |mut map, repo_paths| { - // map.extend(repo_paths.into_git_file_statuses()); - // map - // }); + let mut paths_by_git_repo = HashMap::default(); + for relative_path in relative_paths.iter() { + dbg!(relative_path); + if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(relative_path) { + dbg!(&repo.dot_git_dir_abs_path); + if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) { + dbg!(&repo_path); + // TODO: Remove workdirectoryEntry type (maybe) to remove unwrap + let work_directory = state + .snapshot + .entry_for_id(repo_entry.work_directory.0) + .unwrap() + .path + .clone(); + + paths_by_git_repo + .entry(work_directory) + .or_insert_with(|| RepoPaths { + repo: repo.repo_ptr.clone(), + repo_paths: Vec::new(), + relative_paths: Vec::new(), + }) + .add_paths(relative_path, repo_path); + } + } + } + + // TODO: Should we do this outside of the state lock? + for (work_directory_path, paths) in paths_by_git_repo.into_iter() { + dbg!(&paths.relative_paths, &paths.repo_paths); + if let Ok(status) = paths.repo.status(&paths.repo_paths) { + let mut changed_path_statuses = Vec::with_capacity(status.entries.len()); + + for (repo_path, status) in status.entries.iter() { + let ix = paths + .relative_paths + .iter() + .enumerate() + .find(|(_, path)| path == repo_path) + .map(|(ix, path)| ix); + + if let Some(ix) = ix { + paths.relative_paths.swap_remove(ix); + } + + dbg!((&repo_path, &status)); + changed_path_statuses.push(Edit::Insert(GitEntry { + path: repo_path.clone(), + git_status: *status, + })); + } + + for path in paths.relative_paths { + // TODO: relativize it + changed_path_statuses.push(Edit::Remove(RepoPath(path))); + } + // Find the diff between status results, and paths + // and generate Edit::Remove() for them + + // Update the statuses for new/updated paths associated with this repository + state.snapshot.repository_entries.update( + &RepositoryWorkDirectory(work_directory_path), + move |repository_entry| { + repository_entry + .git_entries_by_path + .edit(changed_path_statuses, &()) + }, + ); + } + } for (path, metadata) in relative_paths.iter().zip(metadata.into_iter()) { let abs_path: Arc = root_abs_path.join(path).into(); @@ -4606,11 +4657,6 @@ impl BackgroundScanner { } } - // Old usage of git_statuses_by_relative_path - // if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external { - // fs_entry.git_status = git_statuses_by_relative_path.remove(path); - // } - state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); } Ok(None) => { @@ -4992,14 +5038,8 @@ impl BackgroundScanner { Some(job.work_directory.0.join(path).into()) }; - let entry_id = project_path - .as_ref() - .and_then(|path| snapshot.entry_for_path(path)) - .map(|entry| entry.id); - new_entries_by_path.insert_or_replace( GitEntry { - entry_id, path: path.clone(), git_status: *status, }, @@ -5208,18 +5248,6 @@ impl RepoPaths { self.relative_paths.push(relative_path.clone()); self.repo_paths.push(repo_path); } - - fn into_git_file_statuses(self) -> HashMap, GitFileStatus> { - let mut statuses = HashMap::default(); - if let Ok(status) = self.repo.status(&self.repo_paths) { - for (repo_path, relative_path) in self.repo_paths.into_iter().zip(self.relative_paths) { - if let Some(path_status) = status.get(&repo_path) { - statuses.insert(relative_path, path_status); - } - } - } - statuses - } } struct ScanJob { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 31eeef7f573dc..950052a33b878 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2524,7 +2524,7 @@ async fn test_git_status(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_git_status_git_panel(cx: &mut TestAppContext) { +async fn test_git_repository_status(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); @@ -2568,19 +2568,80 @@ async fn test_git_status_git_panel(cx: &mut TestAppContext) { let snapshot = tree.snapshot(); let (dir, _) = snapshot.repositories().next().unwrap(); - // Takes a work directory, and returns all file entries with a git status. let entries = snapshot.git_status(dir).unwrap(); assert_eq!(entries.len(), 3); assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); assert_eq!(entries[0].git_status, GitFileStatus::Modified); - assert!(entries[0].entry_id.is_some()); assert_eq!(entries[1].path.as_ref(), Path::new("b.txt")); + // TODO: make this untracked; assert_eq!(entries[1].git_status, GitFileStatus::Added); - assert!(entries[1].entry_id.is_some()); assert_eq!(entries[2].path.as_ref(), Path::new("d.txt")); assert_eq!(entries[2].git_status, GitFileStatus::Deleted); - assert!(entries[2].entry_id.is_none()); + }); + + std::fs::write(work_dir.join("c.txt"), "some changes").unwrap(); + + dbg!("*************************************************"); + tree.flush_fs_events(cx).await; + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.executor().run_until_parked(); + + tree.read_with(cx, |tree, cx| { + let snapshot = tree.snapshot(); + let (dir, _) = snapshot.repositories().next().unwrap(); + + // Takes a work directory, and returns all file entries with a git status. + let entries = snapshot.git_status(dir).unwrap(); + + assert_eq!(entries.len(), 4); + assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); + assert_eq!(entries[0].git_status, GitFileStatus::Modified); + assert_eq!(entries[1].path.as_ref(), Path::new("b.txt")); + // TODO: make this untracked + assert_eq!(entries[1].git_status, GitFileStatus::Added); + // Status updated + assert_eq!(entries[2].path.as_ref(), Path::new("c.txt")); + assert_eq!(entries[2].git_status, GitFileStatus::Modified); + assert_eq!(entries[3].path.as_ref(), Path::new("d.txt")); + assert_eq!(entries[3].git_status, GitFileStatus::Deleted); + }); + + git_add("a.txt", &repo); + git_add("c.txt", &repo); + git_remove_index(Path::new("d.txt"), &repo); + git_commit("Another commit", &repo); + tree.flush_fs_events(cx).await; + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.executor().run_until_parked(); + + dbg!("*************************************************"); + std::fs::remove_file(work_dir.join("a.txt")).unwrap(); + std::fs::remove_file(work_dir.join("b.txt")).unwrap(); + tree.flush_fs_events(cx).await; + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.executor().run_until_parked(); + + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (dir, _) = snapshot.repositories().next().unwrap(); + let entries = snapshot.git_status(dir).unwrap(); + + dbg!(&entries); + + // Deleting an untracked entry, b.txt, should leave no status + // a.txt was tracked, and so should have a status + assert_eq!( + entries.len(), + 1, + "Entries length was incorrect\n{:#?}", + &entries + ); + assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); + assert_eq!(entries[0].git_status, GitFileStatus::Deleted); }); } @@ -2590,7 +2651,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { cx.executor().allow_parking(); let root = temp_tree(json!({ - "my-repo": { + "my-repo)": { // .git folder will go here "a.txt": "a", "sub-folder-1": { @@ -2824,14 +2885,14 @@ fn git_init(path: &Path) -> git2::Repository { fn git_add>(path: P, repo: &git2::Repository) { let path = path.as_ref(); let mut index = repo.index().expect("Failed to get index"); - index.add_path(path).expect("Failed to add a.txt"); + index.add_path(path).expect("Failed to add file"); index.write().expect("Failed to write index"); } #[track_caller] fn git_remove_index(path: &Path, repo: &git2::Repository) { let mut index = repo.index().expect("Failed to get index"); - index.remove_path(path).expect("Failed to add a.txt"); + index.remove_path(path).expect("Failed to add file"); index.write().expect("Failed to write index"); } From 52d8ebeaa0a9ab8969ae11ce9a11dc59907eda34 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 19 Dec 2024 16:19:26 -0500 Subject: [PATCH 05/22] Get test passing, fix a couple of TODOs --- Cargo.lock | 12 +++-- Cargo.toml | 5 +- crates/collections/Cargo.toml | 3 +- crates/collections/src/collections.rs | 14 ++++++ crates/git/src/repository.rs | 13 +++--- crates/git/src/status.rs | 7 +-- crates/worktree/src/worktree.rs | 67 ++++++++++----------------- crates/worktree/src/worktree_tests.rs | 13 ++---- 8 files changed, 65 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01b2f33866392..6fce0344794fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2795,7 +2795,8 @@ dependencies = [ name = "collections" version = "0.1.0" dependencies = [ - "rustc-hash 1.1.0", + "indexmap 2.7.0", + "rustc-hash 2.1.0", ] [[package]] @@ -6229,7 +6230,7 @@ dependencies = [ "heed", "html_to_markdown", "http_client", - "indexmap 1.9.3", + "indexmap 2.7.0", "indoc", "parking_lot", "paths", @@ -9621,7 +9622,7 @@ dependencies = [ "file_icons", "git", "gpui", - "indexmap 1.9.3", + "indexmap 2.7.0", "language", "menu", "pretty_assertions", @@ -11016,6 +11017,7 @@ checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "indexmap 1.9.3", + "indexmap 2.7.0", "schemars_derive", "serde", "serde_json", @@ -12806,7 +12808,7 @@ dependencies = [ "fs", "futures 0.3.31", "gpui", - "indexmap 1.9.3", + "indexmap 2.7.0", "log", "palette", "parking_lot", @@ -12841,7 +12843,7 @@ dependencies = [ "anyhow", "clap", "gpui", - "indexmap 1.9.3", + "indexmap 2.7.0", "log", "palette", "rust-embed", diff --git a/Cargo.toml b/Cargo.toml index 743f6178bfc12..e77875e6281eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -390,7 +390,7 @@ hyper = "0.14" http = "1.1" ignore = "0.4.22" image = "0.25.1" -indexmap = { version = "1.6.2", features = ["serde"] } +indexmap = { version = "2.7.0", features = ["serde"] } indoc = "2" itertools = "0.13.0" jsonwebtoken = "9.3" @@ -442,9 +442,10 @@ runtimelib = { version = "0.24.0", default-features = false, features = [ ] } rustc-demangle = "0.1.23" rust-embed = { version = "8.4", features = ["include-exclude"] } +rustc-hash = "2.1.0" rustls = "0.21.12" rustls-native-certs = "0.8.0" -schemars = { version = "0.8", features = ["impl_json_schema"] } +schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] } semver = "1.0" serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } diff --git a/crates/collections/Cargo.toml b/crates/collections/Cargo.toml index b16b4c1300e04..3daaf83c69bf0 100644 --- a/crates/collections/Cargo.toml +++ b/crates/collections/Cargo.toml @@ -16,4 +16,5 @@ doctest = false test-support = [] [dependencies] -rustc-hash = "1.1" +indexmap.workspace = true +rustc-hash.workspace = true diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs index 25f6135c1f887..f2e0034e1f046 100644 --- a/crates/collections/src/collections.rs +++ b/crates/collections/src/collections.rs @@ -1,15 +1,29 @@ +use rustc_hash::FxBuildHasher; + #[cfg(feature = "test-support")] pub type HashMap = FxHashMap; #[cfg(feature = "test-support")] pub type HashSet = FxHashSet; +#[cfg(feature = "test-support")] +pub type IndexMap = indexmap::IndexMap; + +#[cfg(feature = "test-support")] +pub type IndexSet = indexmap::IndexSet; + #[cfg(not(feature = "test-support"))] pub type HashMap = std::collections::HashMap; #[cfg(not(feature = "test-support"))] pub type HashSet = std::collections::HashSet; +#[cfg(not(feature = "test-support"))] +pub type IndexMap = indexmap::IndexMap; + +#[cfg(not(feature = "test-support"))] +pub type IndexSet = indexmap::IndexSet; + pub use rustc_hash::FxHasher; pub use rustc_hash::{FxHashMap, FxHashSet}; pub use std::collections::*; diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 85145e2640a4b..d38308183acc0 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -38,7 +38,8 @@ pub trait GitRepository: Send + Sync { /// Returns the SHA of the current HEAD. fn head_sha(&self) -> Option; - fn status(&self, path_prefixes: &[RepoPath]) -> Result; + //fn status(&self, path_prefixes: &[RepoPath]) -> Result; + fn status(&self, path_prefixes: &mut dyn Iterator) -> Result; fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; @@ -133,7 +134,7 @@ impl GitRepository for RealGitRepository { Some(self.repository.lock().head().ok()?.target()?.to_string()) } - fn status(&self, path_prefixes: &[RepoPath]) -> Result { + fn status(&self, path_prefixes: &mut dyn Iterator) -> Result { let working_directory = self .repository .lock() @@ -290,16 +291,13 @@ impl GitRepository for FakeGitRepository { state.dot_git_dir.clone() } - fn status(&self, path_prefixes: &[RepoPath]) -> Result { + fn status(&self, mut path_prefixes: &mut dyn Iterator) -> Result { let state = self.state.lock(); let mut entries = state .worktree_statuses .iter() .filter_map(|(repo_path, status)| { - if path_prefixes - .iter() - .any(|path_prefix| repo_path.0.starts_with(path_prefix)) - { + if (&mut path_prefixes).any(|path_prefix| repo_path.0.starts_with(path_prefix)) { Some((repo_path.to_owned(), *status)) } else { None @@ -396,6 +394,7 @@ pub enum GitFileStatus { Modified, Conflict, Deleted, + Untracked, } impl GitFileStatus { diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index deeb65ffe7da6..5d9eba79d93de 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -11,7 +11,7 @@ impl GitStatus { pub(crate) fn new( git_binary: &Path, working_directory: &Path, - path_prefixes: &[RepoPath], + path_prefixes: &mut dyn Iterator, ) -> Result { let child = util::command::new_std_command(git_binary) .current_dir(working_directory) @@ -22,7 +22,7 @@ impl GitStatus { "--untracked-files=all", "-z", ]) - .args(path_prefixes.iter().map(|path_prefix| { + .args(path_prefixes.map(|path_prefix| { if path_prefix.0.as_ref() == Path::new("") { Path::new(".") } else { @@ -53,9 +53,10 @@ impl GitStatus { Some(( RepoPath(Path::new(path).into()), match status { - "A" | "??" => GitFileStatus::Added, + "A" => GitFileStatus::Added, "M" => GitFileStatus::Modified, "D" => GitFileStatus::Deleted, + "??" => GitFileStatus::Untracked, _ => return None, }, )) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 3aea2d3284dee..48f1abc44eab7 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -6,7 +6,7 @@ mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; use clock::ReplicaId; -use collections::{HashMap, HashSet, VecDeque}; +use collections::{HashMap, HashSet, IndexSet, VecDeque}; use fs::{copy_recursive, Fs, MTime, PathEvent, RemoveOptions, Watcher}; use futures::{ channel::{ @@ -51,6 +51,7 @@ use std::{ cmp::Ordering, collections::hash_map, convert::TryFrom, + f32::consts::PI, ffi::OsStr, fmt, future::Future, @@ -2925,7 +2926,7 @@ impl BackgroundScannerState { work_directory: workdir_path, statuses: repo .repo_ptr - .status(&[repo_path]) + .status(&mut [repo_path].iter()) .log_err() .unwrap_or_default(), }); @@ -4504,6 +4505,8 @@ impl BackgroundScanner { abs_paths: Vec, scan_queue_tx: Option>, ) { + eprintln!("reload_entries_for_paths({relative_paths:?})"); + // grab metadata for all requested paths let metadata = futures::future::join_all( abs_paths .iter() @@ -4551,11 +4554,8 @@ impl BackgroundScanner { // Group all relative paths by their git repository. let mut paths_by_git_repo = HashMap::default(); for relative_path in relative_paths.iter() { - dbg!(relative_path); if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(relative_path) { - dbg!(&repo.dot_git_dir_abs_path); if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) { - dbg!(&repo_path); // TODO: Remove workdirectoryEntry type (maybe) to remove unwrap let work_directory = state .snapshot @@ -4568,47 +4568,27 @@ impl BackgroundScanner { .entry(work_directory) .or_insert_with(|| RepoPaths { repo: repo.repo_ptr.clone(), - repo_paths: Vec::new(), - relative_paths: Vec::new(), + repo_paths: HashSet::default(), }) - .add_paths(relative_path, repo_path); + .add_path(dbg!(repo_path)); } } } // TODO: Should we do this outside of the state lock? - for (work_directory_path, paths) in paths_by_git_repo.into_iter() { - dbg!(&paths.relative_paths, &paths.repo_paths); - if let Ok(status) = paths.repo.status(&paths.repo_paths) { - let mut changed_path_statuses = Vec::with_capacity(status.entries.len()); - - for (repo_path, status) in status.entries.iter() { - let ix = paths - .relative_paths - .iter() - .enumerate() - .find(|(_, path)| path == repo_path) - .map(|(ix, path)| ix); - - if let Some(ix) = ix { - paths.relative_paths.swap_remove(ix); - } - - dbg!((&repo_path, &status)); + for (work_directory_path, mut paths) in paths_by_git_repo { + if let Ok(status) = paths.repo.status(&mut paths.repo_paths.iter()) { + let mut changed_path_statuses = Vec::new(); + for (repo_path, status) in &*status.entries { + paths.remove_repo_path(repo_path); changed_path_statuses.push(Edit::Insert(GitEntry { path: repo_path.clone(), git_status: *status, })); } - - for path in paths.relative_paths { - // TODO: relativize it - changed_path_statuses.push(Edit::Remove(RepoPath(path))); + for path in paths.repo_paths { + changed_path_statuses.push(Edit::Remove(PathKey(path.0))); } - // Find the diff between status results, and paths - // and generate Edit::Remove() for them - - // Update the statuses for new/updated paths associated with this repository state.snapshot.repository_entries.update( &RepositoryWorkDirectory(work_directory_path), move |repository_entry| { @@ -4836,7 +4816,7 @@ impl BackgroundScanner { if let Ok(repo_path) = repo_entry.relativize(snapshot, &entry.path) { let status = local_repo .repo_ptr - .status(&[repo_path.clone()]) + .status(&mut [repo_path.clone()].iter()) .ok() .and_then(|status| status.get(&repo_path)); // TODO: figure out what to do here @@ -4973,7 +4953,6 @@ impl BackgroundScanner { .scoped(|scope| { scope.spawn(async { for repo_update in repo_updates { - dbg!("update git status for ", &repo_update.work_directory); self.update_git_statuses(repo_update); } updates_done_tx.blocking_send(()).ok(); @@ -5005,7 +4984,7 @@ impl BackgroundScanner { let Some(statuses) = job .repository - .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()]) + .status(&mut [git::WORK_DIRECTORY_REPO_PATH.clone()].iter()) .log_err() else { return; @@ -5237,16 +5216,19 @@ fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { result } +#[derive(Debug)] struct RepoPaths { repo: Arc, - relative_paths: Vec>, - repo_paths: Vec, + repo_paths: HashSet, } impl RepoPaths { - fn add_paths(&mut self, relative_path: &Arc, repo_path: RepoPath) { - self.relative_paths.push(relative_path.clone()); - self.repo_paths.push(repo_path); + fn add_path(&mut self, repo_path: RepoPath) { + self.repo_paths.insert(repo_path.clone()); + } + + fn remove_repo_path(&mut self, repo_path: &RepoPath) { + self.repo_paths.remove(repo_path); } } @@ -5676,6 +5658,7 @@ fn git_status_to_proto(status: GitFileStatus) -> i32 { GitFileStatus::Modified => proto::GitStatus::Modified as i32, GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, GitFileStatus::Deleted => proto::GitStatus::Deleted as i32, + GitFileStatus::Untracked => todo!(), } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 950052a33b878..9237a2fdd88d5 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2574,15 +2574,14 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); assert_eq!(entries[0].git_status, GitFileStatus::Modified); assert_eq!(entries[1].path.as_ref(), Path::new("b.txt")); - // TODO: make this untracked; - assert_eq!(entries[1].git_status, GitFileStatus::Added); + assert_eq!(entries[1].git_status, GitFileStatus::Untracked); assert_eq!(entries[2].path.as_ref(), Path::new("d.txt")); assert_eq!(entries[2].git_status, GitFileStatus::Deleted); }); std::fs::write(work_dir.join("c.txt"), "some changes").unwrap(); + eprintln!("File c.txt has been modified"); - dbg!("*************************************************"); tree.flush_fs_events(cx).await; cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; @@ -2595,12 +2594,11 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { // Takes a work directory, and returns all file entries with a git status. let entries = snapshot.git_status(dir).unwrap(); - assert_eq!(entries.len(), 4); + std::assert_eq!(entries.len(), 4, "entries: {entries:?}"); assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); assert_eq!(entries[0].git_status, GitFileStatus::Modified); assert_eq!(entries[1].path.as_ref(), Path::new("b.txt")); - // TODO: make this untracked - assert_eq!(entries[1].git_status, GitFileStatus::Added); + assert_eq!(entries[1].git_status, GitFileStatus::Untracked); // Status updated assert_eq!(entries[2].path.as_ref(), Path::new("c.txt")); assert_eq!(entries[2].git_status, GitFileStatus::Modified); @@ -2617,7 +2615,6 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { .await; cx.executor().run_until_parked(); - dbg!("*************************************************"); std::fs::remove_file(work_dir.join("a.txt")).unwrap(); std::fs::remove_file(work_dir.join("b.txt")).unwrap(); tree.flush_fs_events(cx).await; @@ -2630,8 +2627,6 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { let (dir, _) = snapshot.repositories().next().unwrap(); let entries = snapshot.git_status(dir).unwrap(); - dbg!(&entries); - // Deleting an untracked entry, b.txt, should leave no status // a.txt was tracked, and so should have a status assert_eq!( From af9dcfe0c52dcd130615758a471e3fe3051ab6f7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 19 Dec 2024 16:19:34 -0500 Subject: [PATCH 06/22] Small cleanup around RepositoryWorkDirectory --- crates/worktree/src/worktree.rs | 61 +++++++++++++++++---------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 48f1abc44eab7..952cbff8ba396 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -276,7 +276,7 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { /// In the majority of the cases, this is the folder that contains the .git folder. /// But if a sub-folder of a git repository is opened, this corresponds to the /// project root and the .git folder is located in a parent directory. -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct RepositoryWorkDirectory(pub(crate) Arc); impl Default for RepositoryWorkDirectory { @@ -1348,7 +1348,7 @@ impl LocalWorktree { pub fn local_git_repo(&self, path: &Path) -> Option> { self.repo_for_path(path) - .map(|(_, entry)| entry.repo_ptr.clone()) + .map(|(_, _, entry)| entry.repo_ptr.clone()) } pub fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { @@ -2631,10 +2631,21 @@ impl Snapshot { } impl LocalSnapshot { - pub fn repo_for_path(&self, path: &Path) -> Option<(RepositoryEntry, &LocalRepositoryEntry)> { - let (_, repo_entry) = self.repository_and_work_directory_for_path(path)?; + pub fn repo_for_path( + &self, + path: &Path, + ) -> Option<( + RepositoryWorkDirectory, + RepositoryEntry, + &LocalRepositoryEntry, + )> { + let (work_directory, repo_entry) = self.repository_and_work_directory_for_path(path)?; let work_directory_id = repo_entry.work_directory_id(); - Some((repo_entry, self.git_repositories.get(&work_directory_id)?)) + Some(( + work_directory, + repo_entry, + self.git_repositories.get(&work_directory_id)?, + )) } fn build_update( @@ -2919,18 +2930,16 @@ impl BackgroundScannerState { let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); let mut containing_repository = None; if !ignore_stack.is_abs_path_ignored(&abs_path, true) { - if let Some((repo_entry, repo)) = self.snapshot.repo_for_path(&path) { - if let Some(workdir_path) = repo_entry.work_directory(&self.snapshot) { - if let Ok(repo_path) = repo_entry.relativize(&self.snapshot, &path) { - containing_repository = Some(ScanJobContainingRepository { - work_directory: workdir_path, - statuses: repo - .repo_ptr - .status(&mut [repo_path].iter()) - .log_err() - .unwrap_or_default(), - }); - } + if let Some((workdir_path, repo_entry, repo)) = self.snapshot.repo_for_path(&path) { + if let Ok(repo_path) = repo_entry.relativize(&self.snapshot, &path) { + containing_repository = Some(ScanJobContainingRepository { + work_directory: workdir_path, + statuses: repo + .repo_ptr + .status(&mut [repo_path].iter()) + .log_err() + .unwrap_or_default(), + }); } } } @@ -4554,16 +4563,10 @@ impl BackgroundScanner { // Group all relative paths by their git repository. let mut paths_by_git_repo = HashMap::default(); for relative_path in relative_paths.iter() { - if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(relative_path) { + if let Some((work_directory, repo_entry, repo)) = + state.snapshot.repo_for_path(relative_path) + { if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) { - // TODO: Remove workdirectoryEntry type (maybe) to remove unwrap - let work_directory = state - .snapshot - .entry_for_id(repo_entry.work_directory.0) - .unwrap() - .path - .clone(); - paths_by_git_repo .entry(work_directory) .or_insert_with(|| RepoPaths { @@ -4580,7 +4583,7 @@ impl BackgroundScanner { if let Ok(status) = paths.repo.status(&mut paths.repo_paths.iter()) { let mut changed_path_statuses = Vec::new(); for (repo_path, status) in &*status.entries { - paths.remove_repo_path(repo_path); + paths.repo_paths.remove(repo_path); changed_path_statuses.push(Edit::Insert(GitEntry { path: repo_path.clone(), git_status: *status, @@ -4590,7 +4593,7 @@ impl BackgroundScanner { changed_path_statuses.push(Edit::Remove(PathKey(path.0))); } state.snapshot.repository_entries.update( - &RepositoryWorkDirectory(work_directory_path), + &work_directory_path, move |repository_entry| { repository_entry .git_entries_by_path @@ -4812,7 +4815,7 @@ impl BackgroundScanner { path_entry.scan_id = snapshot.scan_id; path_entry.is_ignored = entry.is_ignored; if !entry.is_dir() && !entry.is_ignored && !entry.is_external { - if let Some((ref repo_entry, local_repo)) = repo { + if let Some((_, ref repo_entry, local_repo)) = repo { if let Ok(repo_path) = repo_entry.relativize(snapshot, &entry.path) { let status = local_repo .repo_ptr From 652cb93f9b887fed40d3fcd151e15b1d3413098e Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 19 Dec 2024 16:19:38 -0500 Subject: [PATCH 07/22] Rename --- crates/worktree/src/worktree.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 952cbff8ba396..9947a8ba79440 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -4579,7 +4579,7 @@ impl BackgroundScanner { } // TODO: Should we do this outside of the state lock? - for (work_directory_path, mut paths) in paths_by_git_repo { + for (work_directory, mut paths) in paths_by_git_repo { if let Ok(status) = paths.repo.status(&mut paths.repo_paths.iter()) { let mut changed_path_statuses = Vec::new(); for (repo_path, status) in &*status.entries { @@ -4593,7 +4593,7 @@ impl BackgroundScanner { changed_path_statuses.push(Edit::Remove(PathKey(path.0))); } state.snapshot.repository_entries.update( - &work_directory_path, + &work_directory, move |repository_entry| { repository_entry .git_entries_by_path From 974c3e1b1ce72b00bd21f34869c19e0b2ee1358a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 17 Dec 2024 15:30:09 -0800 Subject: [PATCH 08/22] WIP: fix propogation --- crates/collections/src/collections.rs | 6 +- crates/git/src/repository.rs | 25 +- crates/git/src/status.rs | 4 +- crates/project/src/buffer_store.rs | 2 +- crates/proto/proto/zed.proto | 1 + crates/worktree/src/worktree.rs | 423 ++++++++++++++++++-------- crates/worktree/src/worktree_tests.rs | 24 +- 7 files changed, 344 insertions(+), 141 deletions(-) diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs index f2e0034e1f046..be7bbdb59f646 100644 --- a/crates/collections/src/collections.rs +++ b/crates/collections/src/collections.rs @@ -1,5 +1,3 @@ -use rustc_hash::FxBuildHasher; - #[cfg(feature = "test-support")] pub type HashMap = FxHashMap; @@ -7,10 +5,10 @@ pub type HashMap = FxHashMap; pub type HashSet = FxHashSet; #[cfg(feature = "test-support")] -pub type IndexMap = indexmap::IndexMap; +pub type IndexMap = indexmap::IndexMap; #[cfg(feature = "test-support")] -pub type IndexSet = indexmap::IndexSet; +pub type IndexSet = indexmap::IndexSet; #[cfg(not(feature = "test-support"))] pub type HashMap = std::collections::HashMap; diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index d38308183acc0..3a5b59f3b9373 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -38,8 +38,7 @@ pub trait GitRepository: Send + Sync { /// Returns the SHA of the current HEAD. fn head_sha(&self) -> Option; - //fn status(&self, path_prefixes: &[RepoPath]) -> Result; - fn status(&self, path_prefixes: &mut dyn Iterator) -> Result; + fn status(&self, path_prefixes: &[RepoPath]) -> Result; fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; @@ -134,7 +133,7 @@ impl GitRepository for RealGitRepository { Some(self.repository.lock().head().ok()?.target()?.to_string()) } - fn status(&self, path_prefixes: &mut dyn Iterator) -> Result { + fn status(&self, path_prefixes: &[RepoPath]) -> Result { let working_directory = self .repository .lock() @@ -291,13 +290,16 @@ impl GitRepository for FakeGitRepository { state.dot_git_dir.clone() } - fn status(&self, mut path_prefixes: &mut dyn Iterator) -> Result { + fn status(&self, path_prefixes: &[RepoPath]) -> Result { let state = self.state.lock(); let mut entries = state .worktree_statuses .iter() .filter_map(|(repo_path, status)| { - if (&mut path_prefixes).any(|path_prefix| repo_path.0.starts_with(path_prefix)) { + if path_prefixes + .iter() + .any(|path_prefix| repo_path.0.starts_with(path_prefix)) + { Some((repo_path.to_owned(), *status)) } else { None @@ -434,6 +436,13 @@ impl RepoPath { RepoPath(path.into()) } + + pub fn from_str(path: &str) -> Self { + let path = Path::new(path); + debug_assert!(path.is_relative(), "Repo paths must be relative"); + + RepoPath(path.into()) + } } impl From<&Path> for RepoPath { @@ -448,6 +457,12 @@ impl From for RepoPath { } } +impl From<&str> for RepoPath { + fn from(value: &str) -> Self { + Self::from_str(value) + } +} + impl Default for RepoPath { fn default() -> Self { RepoPath(Path::new("").into()) diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 5d9eba79d93de..0d62cfaae9df5 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -11,7 +11,7 @@ impl GitStatus { pub(crate) fn new( git_binary: &Path, working_directory: &Path, - path_prefixes: &mut dyn Iterator, + path_prefixes: &[RepoPath], ) -> Result { let child = util::command::new_std_command(git_binary) .current_dir(working_directory) @@ -22,7 +22,7 @@ impl GitStatus { "--untracked-files=all", "-z", ]) - .args(path_prefixes.map(|path_prefix| { + .args(path_prefixes.iter().map(|path_prefix| { if path_prefix.0.as_ref() == Path::new("") { Path::new(".") } else { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 3abc794d041f1..7c37ef481c440 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -561,7 +561,7 @@ impl LocalBufferStore { buffer_change_sets .into_iter() .filter_map(|(change_set, buffer_snapshot, path)| { - let (repo_entry, local_repo_entry) = snapshot.repo_for_path(&path)?; + let repo = snapshot.repo_for_path(&path)?; let relative_path = repo_entry.relativize(&snapshot, &path).ok()?; let base_text = local_repo_entry.repo().load_index_text(&relative_path); Some((change_set, buffer_snapshot, base_text)) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 9cad363109cca..c2c28b91d654d 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -1783,6 +1783,7 @@ message StatusEntry { GitStatus status = 2; } +// TODO: model this git status better, replicating the staged and unstaged states enum GitStatus { Added = 0; Modified = 1; diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 9947a8ba79440..77d3ffddcb0ec 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -6,7 +6,7 @@ mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; use clock::ReplicaId; -use collections::{HashMap, HashSet, IndexSet, VecDeque}; +use collections::{HashMap, HashSet, VecDeque}; use fs::{copy_recursive, Fs, MTime, PathEvent, RemoveOptions, Watcher}; use futures::{ channel::{ @@ -21,7 +21,6 @@ use fuzzy::CharBag; use git::GitHostingProviderRegistry; use git::{ repository::{GitFileStatus, GitRepository, RepoPath}, - status::GitStatus, COOKIES, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, }; use gpui::{ @@ -30,7 +29,7 @@ use gpui::{ }; use ignore::IgnoreStack; use language::DiskState; -use log::debug; + use parking_lot::Mutex; use paths::local_settings_folder_relative_path; use postage::{ @@ -51,7 +50,6 @@ use std::{ cmp::Ordering, collections::hash_map, convert::TryFrom, - f32::consts::PI, ffi::OsStr, fmt, future::Future, @@ -65,9 +63,10 @@ use std::{ }, time::{Duration, Instant}, }; -use sum_tree::{Bias, Edit, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; +use sum_tree::{Bias, Cursor, Edit, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ + maybe, paths::{home_dir, PathMatcher, SanitizedPath}, ResultExt, }; @@ -1348,7 +1347,7 @@ impl LocalWorktree { pub fn local_git_repo(&self, path: &Path) -> Option> { self.repo_for_path(path) - .map(|(_, _, entry)| entry.repo_ptr.clone()) + .map(|fields| fields.local_entry.repo_ptr.clone()) } pub fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { @@ -2224,7 +2223,6 @@ impl Snapshot { Some(removed_entry.path) } - // TODO: revisit the vec pub fn git_status<'a>(&'a self, work_dir: &'a impl AsRef) -> Option> { let path = work_dir.as_ref(); self.repository_for_path(&path) @@ -2235,8 +2233,9 @@ impl Snapshot { pub fn status_for_file(&self, path: impl Into) -> Option { let path = path.into(); self.repository_for_path(&path).and_then(|repo| { + let repo_path = repo.relativize(self, &path).unwrap(); repo.git_entries_by_path - .get(&PathKey(Arc::from(path)), &()) + .get(&PathKey(repo_path.0), &()) .map(|entry| entry.git_status) }) } @@ -2422,10 +2421,10 @@ impl Snapshot { self.traverse_from_offset(true, true, include_ignored, start) } - pub fn repositories(&self) -> impl Iterator, &RepositoryEntry)> { - self.repository_entries - .iter() - .map(|(path, entry)| (&path.0, entry)) + pub fn repositories( + &self, + ) -> impl Iterator { + self.repository_entries.iter() } /// Get the repository whose work directory contains the given path. @@ -2442,18 +2441,6 @@ impl Snapshot { .map(|e| e.1) } - pub(crate) fn insert_repository_entry(&mut self, repository_entry: RepositoryEntry) { - let Some(entry) = self.entry_for_id(repository_entry.work_directory.0) else { - log::error!("Attempting to insert a repository without the corresponding worktree entry for it's work directory"); - debug_assert!(false); - return; - }; - self.repository_entries.insert( - RepositoryWorkDirectory(entry.path.clone()), - repository_entry, - ); - } - pub fn repository_and_work_directory_for_path( &self, path: &Path, @@ -2471,7 +2458,7 @@ impl Snapshot { &'a self, entries: impl 'a + Iterator, ) -> impl 'a + Iterator)> { - let mut containing_repos = Vec::<(&Arc, &RepositoryEntry)>::new(); + let mut containing_repos = Vec::<(&RepositoryWorkDirectory, &RepositoryEntry)>::new(); let mut repositories = self.repositories().peekable(); entries.map(move |entry| { while let Some((repo_path, _)) = containing_repos.last() { @@ -2493,17 +2480,20 @@ impl Snapshot { }) } + /// TODO: Redo this entirely, API is wrong, conceptually it's a bit weird /// Updates the `git_status` of the given entries such that files' /// statuses bubble up to their ancestor directories. - pub fn propagate_git_statuses(&self, result: &mut [Entry]) { - let mut cursor = self.entries_by_path.cursor::(&()); - let mut entry_stack = Vec::::new(); + pub fn propagate_git_statuses(&self, entries: &[Entry]) -> Vec> { + let mut cursor = all_statuses_cursor(self); - let mut result_ix = 0; + let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); + let mut result = vec![None; entries.len()]; + let mut entry_ix = 0; loop { - let next_entry = result.get(result_ix); - let containing_entry = entry_stack.last().map(|ix| &result[*ix]); - + let next_entry = entries.get(entry_ix); + dbg!(&next_entry); + let containing_entry = entry_stack.last().map(|(ix, _)| &entries[*ix]); + dbg!(&containing_entry); let entry_to_finish = match (containing_entry, next_entry) { (Some(_), None) => entry_stack.pop(), (Some(containing_entry), Some(next_path)) => { @@ -2517,37 +2507,43 @@ impl Snapshot { (None, None) => break, }; - if let Some(entry_ix) = entry_to_finish { - cursor.seek_forward( - &TraversalTarget::PathSuccessor(&result[entry_ix].path), - Bias::Left, - &(), - ); - - // let statuses = cursor.start().1 - prev_statuses; - - // TODO: recreate this behavior, using the GitEntrySummary and the PathSuccessor target - // result[entry_ix].git_status = if statuses.conflict > 0 { - // Some(GitFileStatus::Conflict) - // } else if statuses.modified > 0 { - // Some(GitFileStatus::Modified) - // } else if statuses.added > 0 { - // Some(GitFileStatus::Added) - // } else { - // None - // }; + if let Some((entry_ix, prev_statuses)) = entry_to_finish { + dbg!(cursor.item()); + cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor( + &entries[entry_ix].path, + )); + + let statuses = cursor.start() - prev_statuses; + + result[entry_ix] = if statuses.conflict > 0 { + Some(GitFileStatus::Conflict) + } else if statuses.modified > 0 { + Some(GitFileStatus::Modified) + } else if statuses.added > 0 { + Some(GitFileStatus::Added) + } else if statuses.untracked > 0 { + Some(GitFileStatus::Untracked) + } else { + None + }; } else { - if result[result_ix].is_dir() { - cursor.seek_forward( - &TraversalTarget::Path(&result[result_ix].path), - Bias::Left, - &(), - ); - // entry_stack.push((result_ix, cursor.start().1)); + if entries[entry_ix].is_dir() { + cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor( + &entries[entry_ix].path, + )); + dbg!(cursor.start()); + dbg!(cursor.item()); + entry_stack.push((entry_ix, cursor.start())); + } else { + cursor.seek_forward(&GitEntryTraversalTarget::Path(&entries[entry_ix].path)); + dbg!(cursor.item()); + result[entry_ix] = cursor.item().map(|entry| entry.git_status) } - result_ix += 1; + entry_ix += 1; } } + + result } pub fn paths(&self) -> impl Iterator> { @@ -2630,22 +2626,24 @@ impl Snapshot { } } +// TODO: Bad name -> Bad code structure, refactor to remove this type +struct LocalRepositoryFields<'a> { + work_directory: RepositoryWorkDirectory, + repository_entry: RepositoryEntry, + local_entry: &'a LocalRepositoryEntry, +} + impl LocalSnapshot { - pub fn repo_for_path( - &self, - path: &Path, - ) -> Option<( - RepositoryWorkDirectory, - RepositoryEntry, - &LocalRepositoryEntry, - )> { - let (work_directory, repo_entry) = self.repository_and_work_directory_for_path(path)?; - let work_directory_id = repo_entry.work_directory_id(); - Some(( + pub(crate) fn repo_for_path(&self, path: &Path) -> Option> { + let (work_directory, repository_entry) = + self.repository_and_work_directory_for_path(path)?; + let work_directory_id = repository_entry.work_directory_id(); + + Some(LocalRepositoryFields { work_directory, - repo_entry, - self.git_repositories.get(&work_directory_id)?, - )) + repository_entry, + local_entry: self.git_repositories.get(&work_directory_id)?, + }) } fn build_update( @@ -2928,21 +2926,7 @@ impl BackgroundScannerState { let path = entry.path.clone(); let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); - let mut containing_repository = None; - if !ignore_stack.is_abs_path_ignored(&abs_path, true) { - if let Some((workdir_path, repo_entry, repo)) = self.snapshot.repo_for_path(&path) { - if let Ok(repo_path) = repo_entry.relativize(&self.snapshot, &path) { - containing_repository = Some(ScanJobContainingRepository { - work_directory: workdir_path, - statuses: repo - .repo_ptr - .status(&mut [repo_path].iter()) - .log_err() - .unwrap_or_default(), - }); - } - } - } + if !ancestor_inodes.contains(&entry.inode) { ancestor_inodes.insert(entry.inode); scan_job_tx @@ -2962,7 +2946,7 @@ impl BackgroundScannerState { if let Some(mtime) = entry.mtime { // If an entry with the same inode was removed from the worktree during this scan, // then it *might* represent the same file or directory. But the OS might also have - // r*e-used the inode for a completely different file or directory. + // re-used the inode for a completely different file or directory. // // Conditionally reuse the old entry's id: // * if the mtime is the same, the file was probably been renamed. @@ -3570,6 +3554,7 @@ pub struct GitEntry { #[derive(Clone, Debug)] pub struct GitEntrySummary { max_path: Arc, + statuses: GitStatuses, } impl sum_tree::Summary for GitEntrySummary { @@ -3578,11 +3563,13 @@ impl sum_tree::Summary for GitEntrySummary { fn zero(_cx: &Self::Context) -> Self { GitEntrySummary { max_path: Arc::from(Path::new("")), + statuses: Default::default(), } } fn add_summary(&mut self, rhs: &Self, _: &Self::Context) { self.max_path = rhs.max_path.clone(); + self.statuses += rhs.statuses; } } @@ -3592,6 +3579,25 @@ impl sum_tree::Item for GitEntry { fn summary(&self, _: &::Context) -> Self::Summary { GitEntrySummary { max_path: self.path.0.clone(), + statuses: match self.git_status { + GitFileStatus::Added => GitStatuses { + added: 1, + ..Default::default() + }, + GitFileStatus::Modified => GitStatuses { + modified: 1, + ..Default::default() + }, + GitFileStatus::Conflict => GitStatuses { + conflict: 1, + ..Default::default() + }, + GitFileStatus::Deleted => Default::default(), + GitFileStatus::Untracked => GitStatuses { + untracked: 1, + ..Default::default() + }, + }, } } } @@ -3604,6 +3610,59 @@ impl sum_tree::KeyedItem for GitEntry { } } +#[derive(Clone, Debug, Default, Copy)] +struct GitStatuses { + added: usize, + modified: usize, + conflict: usize, + untracked: usize, +} + +impl std::ops::Add for GitStatuses { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + GitStatuses { + added: self.added + rhs.added, + modified: self.modified + rhs.modified, + conflict: self.conflict + rhs.conflict, + untracked: self.untracked + rhs.untracked, + } + } +} + +impl std::ops::AddAssign for GitStatuses { + fn add_assign(&mut self, rhs: Self) { + self.added += rhs.added; + self.modified += rhs.modified; + self.conflict += rhs.conflict; + self.untracked += rhs.untracked; + } +} + +impl std::ops::Sub for GitStatuses { + type Output = GitStatuses; + + fn sub(self, rhs: Self) -> Self::Output { + GitStatuses { + added: self.added - rhs.added, + modified: self.modified - rhs.modified, + conflict: self.conflict - rhs.conflict, + untracked: self.untracked - rhs.untracked, + } + } +} + +impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for GitStatuses { + fn zero(_cx: &()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a GitEntrySummary, _: &()) { + *self += summary.statuses + } +} + impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for PathKey { fn zero(_cx: &()) -> Self { Default::default() @@ -3614,6 +3673,133 @@ impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for PathKey { } } +impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for TraversalProgress<'a> { + fn zero(_cx: &()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a GitEntrySummary, _: &()) { + self.max_path = summary.max_path.as_ref(); + } +} + +#[derive(Debug)] +enum GitEntryTraversalTarget<'a> { + PathSuccessor(&'a Path), + Path(&'a Path), +} + +impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses)> + for GitEntryTraversalTarget<'b> +{ + fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering { + match self { + GitEntryTraversalTarget::Path(path) => path.cmp(&cursor_location.0.max_path), + GitEntryTraversalTarget::PathSuccessor(path) => { + if cursor_location.0.max_path.starts_with(path) { + Ordering::Greater + } else { + Ordering::Equal + } + } + } + } +} + +struct AllStatusesCursor<'a, I> { + repositories: I, + current_cursor: Option, GitStatuses)>>, + statuses_so_far: GitStatuses, +} + +impl<'a, I> AllStatusesCursor<'a, I> +where + I: Iterator, +{ + fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { + dbg!(target); + dbg!(self.statuses_so_far); + let mut should_step_repositories = false; + let mut statuses_so_far_added = None; + loop { + dbg!("seek forward loop"); + if let Some(cursor) = self.current_cursor.as_mut() { + dbg!("have a cursor"); + cursor.seek_forward(target, Bias::Left, &()); + + if cursor.item().is_none() { + self.statuses_so_far += dbg!(cursor.start().1); + statuses_so_far_added = Some(cursor.start().1); + should_step_repositories = true + } + + /* + |*******| + ^ + + */ + } + if should_step_repositories { + should_step_repositories = false; + let maybe_next_cursor = maybe!({ + let (workdir, repository) = self.repositories.next()?; + dbg!(workdir); + Some( + repository + .git_entries_by_path + .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), + ) + }); + if let Some(next_cursor) = maybe_next_cursor { + self.current_cursor = Some(next_cursor); + if let Some(statuses_so_far_added) = statuses_so_far_added { + self.statuses_so_far = self.statuses_so_far - statuses_so_far_added; + } + continue; + } else { + break; + } + } + break; + } + } + + fn item(&self) -> Option<&GitEntry> { + self.current_cursor + .as_ref() + .and_then(|cursor| cursor.item()) + } + + fn start(&self) -> GitStatuses { + if let Some(cursor) = self.current_cursor.as_ref() { + cursor.start().1 + self.statuses_so_far + } else { + self.statuses_so_far + } + } +} + +fn all_statuses_cursor<'a>( + snapshot: &'a Snapshot, +) -> AllStatusesCursor<'a, impl Iterator> +{ + let mut repositories = snapshot.repositories(); + let cursor = util::maybe!({ + let (workdir, entry) = repositories.next()?; + dbg!(workdir); + Some( + entry + .git_entries_by_path + .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), + ) + }); + AllStatusesCursor { + repositories, + current_cursor: cursor, + statuses_so_far: Default::default(), + } +} + impl Entry { fn new( path: Arc, @@ -4563,15 +4749,18 @@ impl BackgroundScanner { // Group all relative paths by their git repository. let mut paths_by_git_repo = HashMap::default(); for relative_path in relative_paths.iter() { - if let Some((work_directory, repo_entry, repo)) = - state.snapshot.repo_for_path(relative_path) + if let Some(LocalRepositoryFields { + work_directory, + repository_entry, + local_entry, + }) = state.snapshot.repo_for_path(relative_path) { - if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) { + if let Ok(repo_path) = repository_entry.relativize(&state.snapshot, relative_path) { paths_by_git_repo .entry(work_directory) .or_insert_with(|| RepoPaths { - repo: repo.repo_ptr.clone(), - repo_paths: HashSet::default(), + repo: local_entry.repo_ptr.clone(), + repo_paths: Default::default(), }) .add_path(dbg!(repo_path)); } @@ -4580,10 +4769,10 @@ impl BackgroundScanner { // TODO: Should we do this outside of the state lock? for (work_directory, mut paths) in paths_by_git_repo { - if let Ok(status) = paths.repo.status(&mut paths.repo_paths.iter()) { + if let Ok(status) = paths.repo.status(&paths.repo_paths) { let mut changed_path_statuses = Vec::new(); for (repo_path, status) in &*status.entries { - paths.repo_paths.remove(repo_path); + paths.remove_repo_path(repo_path); changed_path_statuses.push(Edit::Insert(GitEntry { path: repo_path.clone(), git_status: *status, @@ -4778,7 +4967,7 @@ impl BackgroundScanner { .abs_path .strip_prefix(snapshot.abs_path.as_path()) .unwrap(); - let repo = snapshot.repo_for_path(path); + for mut entry in snapshot.child_entries(path).cloned() { let was_ignored = entry.is_ignored; let abs_path: Arc = snapshot.abs_path().join(&entry.path).into(); @@ -4814,19 +5003,6 @@ impl BackgroundScanner { let mut path_entry = snapshot.entries_by_id.get(&entry.id, &()).unwrap().clone(); path_entry.scan_id = snapshot.scan_id; path_entry.is_ignored = entry.is_ignored; - if !entry.is_dir() && !entry.is_ignored && !entry.is_external { - if let Some((_, ref repo_entry, local_repo)) = repo { - if let Ok(repo_path) = repo_entry.relativize(snapshot, &entry.path) { - let status = local_repo - .repo_ptr - .status(&mut [repo_path.clone()].iter()) - .ok() - .and_then(|status| status.get(&repo_path)); - // TODO: figure out what to do here - // entry.git_status = status; - } - } - } entries_by_id_edits.push(Edit::Insert(path_entry)); entries_by_path_edits.push(Edit::Insert(entry)); } @@ -4987,7 +5163,7 @@ impl BackgroundScanner { let Some(statuses) = job .repository - .status(&mut [git::WORK_DIRECTORY_REPO_PATH.clone()].iter()) + .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()]) .log_err() else { return; @@ -5222,16 +5398,29 @@ fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { #[derive(Debug)] struct RepoPaths { repo: Arc, - repo_paths: HashSet, + // sorted + repo_paths: Vec, } impl RepoPaths { fn add_path(&mut self, repo_path: RepoPath) { - self.repo_paths.insert(repo_path.clone()); + match self.repo_paths.binary_search(&repo_path) { + Ok(_) => {} + Err(ix) => self.repo_paths.insert(ix, repo_path), + } } fn remove_repo_path(&mut self, repo_path: &RepoPath) { - self.repo_paths.remove(repo_path); + match self.repo_paths.binary_search(&repo_path) { + Ok(ix) => { + self.repo_paths.remove(ix); + } + Err(_) => { + dbg!(repo_path); + log::error!("Attempted to remove a repo path that was never added"); + debug_assert!(false); + } + } } } @@ -5244,12 +5433,6 @@ struct ScanJob { is_external: bool, } -#[derive(Clone)] -struct ScanJobContainingRepository { - work_directory: RepositoryWorkDirectory, - statuses: GitStatus, -} - struct UpdateIgnoreStatusJob { abs_path: Arc, ignore_stack: Arc, diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 9237a2fdd88d5..6720859b53952 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1497,10 +1497,14 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { cx.executor().run_until_parked(); let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); + + dbg!(snapshot.git_status(&Path::new(""))); + + dbg!("******************************************"); check_propagated_statuses( &snapshot, &[ - (Path::new(""), Some(GitFileStatus::Modified)), + (Path::new(""), Some(GitFileStatus::Modified)), // This is testing our propogation stuff, which we just said we wouldn't do (Path::new("a.txt"), None), (Path::new("b/c.txt"), Some(GitFileStatus::Modified)), ], @@ -2399,11 +2403,11 @@ async fn test_git_status(cx: &mut TestAppContext) { assert_eq!( snapshot.status_for_file(project_path.join(B_TXT)), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); assert_eq!( snapshot.status_for_file(project_path.join(F_TXT)), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); }); @@ -2433,7 +2437,7 @@ async fn test_git_status(cx: &mut TestAppContext) { let snapshot = tree.snapshot(); assert_eq!( snapshot.status_for_file(project_path.join(F_TXT)), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None); assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); @@ -2455,13 +2459,14 @@ async fn test_git_status(cx: &mut TestAppContext) { assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); assert_eq!( snapshot.status_for_file(project_path.join(B_TXT)), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); assert_eq!( snapshot.status_for_file(project_path.join(E_TXT)), Some(GitFileStatus::Modified) ); }); + dbg!("***********************************"); std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); std::fs::remove_dir_all(work_dir.join("c")).unwrap(); @@ -2587,7 +2592,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { .await; cx.executor().run_until_parked(); - tree.read_with(cx, |tree, cx| { + tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); let (dir, _) = snapshot.repositories().next().unwrap(); @@ -2857,15 +2862,16 @@ fn check_propagated_statuses( snapshot: &Snapshot, expected_statuses: &[(&Path, Option)], ) { - let mut entries = expected_statuses + let entries = expected_statuses .iter() .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone()) .collect::>(); - snapshot.propagate_git_statuses(&mut entries); + let statuses = snapshot.propagate_git_statuses(&entries); assert_eq!( entries .iter() - .map(|e| (e.path.as_ref(), snapshot.status_for_file(e.path.as_ref()))) + .enumerate() + .map(|(ix, e)| (e.path.as_ref(), statuses[ix])) .collect::>(), expected_statuses ); From f1651196cb577fa4291f1a5b928b646cfa926b2d Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 18 Dec 2024 13:24:34 -0500 Subject: [PATCH 09/22] WIP --- crates/worktree/src/worktree.rs | 122 +++++++++++++++++--------- crates/worktree/src/worktree_tests.rs | 58 +++++++++++- 2 files changed, 135 insertions(+), 45 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 77d3ffddcb0ec..603901d4d9ed9 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2480,6 +2480,67 @@ impl Snapshot { }) } + pub fn propagate_git_statuses2(&self, entries: &[Entry]) -> Vec> { + let mut cursor = all_statuses_cursor(self); + let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); + + let mut result = entries + .iter() + .map(|entry| { + let (work_directory, repo_entry) = + self.repository_and_work_directory_for_path(&entry.path)?; + let RepoPath(path) = repo_entry.relativize(self, &entry.path).ok()?; + let git_entry = repo_entry.git_entries_by_path.get(&PathKey(path), &())?; + Some(git_entry.git_status) + }) + .collect::>(); + + let mut entry_ix = 0; + loop { + let next_entry = entries.get(entry_ix); + let containing_entry = entry_stack.last().map(|(ix, _)| &entries[*ix]); + + let entry_to_finish = match (containing_entry, next_entry) { + (Some(_), None) => entry_stack.pop(), + (Some(containing_entry), Some(next_path)) => { + if next_path.path.starts_with(&containing_entry.path) { + None + } else { + entry_stack.pop() + } + } + (None, Some(_)) => None, + (None, None) => break, + }; + + if let Some((entry_ix, prev_statuses)) = entry_to_finish { + cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor(dbg!( + &entries[entry_ix].path + ))); + + let statuses = dbg!(cursor.start()) - dbg!(prev_statuses); + + result[entry_ix] = if statuses.conflict > 0 { + Some(GitFileStatus::Conflict) + } else if statuses.modified > 0 { + Some(GitFileStatus::Modified) + } else if statuses.added > 0 { + Some(GitFileStatus::Added) + } else { + None + }; + } else { + if entries[entry_ix].is_dir() { + cursor.seek_forward(&GitEntryTraversalTarget::Path(&entries[entry_ix].path)); + entry_stack.push((entry_ix, cursor.start())); + } + entry_ix += 1; + } + } + + result + } + /// TODO: Redo this entirely, API is wrong, conceptually it's a bit weird /// Updates the `git_status` of the given entries such that files' /// statuses bubble up to their ancestor directories. @@ -3718,49 +3779,26 @@ where { fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { dbg!(target); - dbg!(self.statuses_so_far); - let mut should_step_repositories = false; - let mut statuses_so_far_added = None; loop { - dbg!("seek forward loop"); - if let Some(cursor) = self.current_cursor.as_mut() { - dbg!("have a cursor"); - cursor.seek_forward(target, Bias::Left, &()); - - if cursor.item().is_none() { - self.statuses_so_far += dbg!(cursor.start().1); - statuses_so_far_added = Some(cursor.start().1); - should_step_repositories = true - } - - /* - |*******| - ^ - - */ - } - if should_step_repositories { - should_step_repositories = false; - let maybe_next_cursor = maybe!({ - let (workdir, repository) = self.repositories.next()?; - dbg!(workdir); - Some( - repository + let cursor = match &mut self.current_cursor { + Some(cursor) => cursor, + None => { + let Some((_, entry)) = self.repositories.next() else { + break; + }; + self.current_cursor = Some( + entry .git_entries_by_path .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), - ) - }); - if let Some(next_cursor) = maybe_next_cursor { - self.current_cursor = Some(next_cursor); - if let Some(statuses_so_far_added) = statuses_so_far_added { - self.statuses_so_far = self.statuses_so_far - statuses_so_far_added; - } - continue; - } else { - break; + ); + self.current_cursor.as_mut().unwrap() } + }; + let found = cursor.seek_forward(target, Bias::Left, &()); + if found { + break; } - break; + self.current_cursor = None; } } @@ -3772,9 +3810,9 @@ where fn start(&self) -> GitStatuses { if let Some(cursor) = self.current_cursor.as_ref() { - cursor.start().1 + self.statuses_so_far + dbg!(cursor.start().1) } else { - self.statuses_so_far + dbg!(GitStatuses::default()) } } } @@ -3786,7 +3824,6 @@ fn all_statuses_cursor<'a>( let mut repositories = snapshot.repositories(); let cursor = util::maybe!({ let (workdir, entry) = repositories.next()?; - dbg!(workdir); Some( entry .git_entries_by_path @@ -4762,7 +4799,7 @@ impl BackgroundScanner { repo: local_entry.repo_ptr.clone(), repo_paths: Default::default(), }) - .add_path(dbg!(repo_path)); + .add_path(repo_path); } } } @@ -5416,7 +5453,6 @@ impl RepoPaths { self.repo_paths.remove(ix); } Err(_) => { - dbg!(repo_path); log::error!("Attempted to remove a repo path that was never added"); debug_assert!(false); } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 6720859b53952..ce9c6527db928 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1500,7 +1500,6 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { dbg!(snapshot.git_status(&Path::new(""))); - dbg!("******************************************"); check_propagated_statuses( &snapshot, &[ @@ -2834,6 +2833,61 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_propagate_statuses_two_repos(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "x": { + ".git": {}, + "x1": "foo" + }, + "y": { + ".git": {}, + "y1": "bar" + } + }), + ) + .await; + + fs.set_status_for_repo_via_git_operation( + Path::new("/root/x/.git"), + &[(Path::new("x1"), GitFileStatus::Added)], + ); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/y/.git"), + &[(Path::new("y1"), GitFileStatus::Conflict)], + ); + + let tree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + cx.executor().run_until_parked(); + let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new("x"), Some(GitFileStatus::Added)), + (Path::new("x/x1"), Some(GitFileStatus::Added)), + (Path::new("y"), Some(GitFileStatus::Conflict)), + (Path::new("y/y1"), Some(GitFileStatus::Conflict)), + ], + ); +} + #[gpui::test] async fn test_private_single_file_worktree(cx: &mut TestAppContext) { init_test(cx); @@ -2866,7 +2920,7 @@ fn check_propagated_statuses( .iter() .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone()) .collect::>(); - let statuses = snapshot.propagate_git_statuses(&entries); + let statuses = snapshot.propagate_git_statuses2(&entries); assert_eq!( entries .iter() From ff2a364b2438ec2686bdabd4a35f948a4816cad8 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 18 Dec 2024 18:53:19 -0500 Subject: [PATCH 10/22] wat Co-authored-by: Mikayla --- crates/worktree/src/worktree.rs | 126 +++++++++----------------- crates/worktree/src/worktree_tests.rs | 31 +++++-- 2 files changed, 69 insertions(+), 88 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 603901d4d9ed9..c098ad219d918 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2480,13 +2480,17 @@ impl Snapshot { }) } - pub fn propagate_git_statuses2(&self, entries: &[Entry]) -> Vec> { + pub fn propagate_git_statuses(&self, entries: &[Entry]) -> Vec> { let mut cursor = all_statuses_cursor(self); let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); let mut result = entries .iter() .map(|entry| { + if entry.is_dir() { + return None; + } + let (work_directory, repo_entry) = self.repository_and_work_directory_for_path(&entry.path)?; let RepoPath(path) = repo_entry.relativize(self, &entry.path).ok()?; @@ -2495,11 +2499,22 @@ impl Snapshot { }) .collect::>(); + // dbg!(entries + // .iter() + // .zip(result.iter()) + // .map(|(entry, status)| { (entry.path.clone(), status) }) + // .collect::>()); + let mut entry_ix = 0; loop { let next_entry = entries.get(entry_ix); let containing_entry = entry_stack.last().map(|(ix, _)| &entries[*ix]); + dbg!(entry_stack + .iter() + .map(|(ix, statuses)| (entries[*ix].path.clone(), statuses)) + .collect::>()); + let entry_to_finish = match (containing_entry, next_entry) { (Some(_), None) => entry_stack.pop(), (Some(containing_entry), Some(next_path)) => { @@ -2514,67 +2529,14 @@ impl Snapshot { }; if let Some((entry_ix, prev_statuses)) = entry_to_finish { - cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor(dbg!( + dbg!("seeking for entry to finish"); + cursor.seek_forward(dbg!(&GitEntryTraversalTarget::PathSuccessor( &entries[entry_ix].path ))); let statuses = dbg!(cursor.start()) - dbg!(prev_statuses); - result[entry_ix] = if statuses.conflict > 0 { - Some(GitFileStatus::Conflict) - } else if statuses.modified > 0 { - Some(GitFileStatus::Modified) - } else if statuses.added > 0 { - Some(GitFileStatus::Added) - } else { - None - }; - } else { - if entries[entry_ix].is_dir() { - cursor.seek_forward(&GitEntryTraversalTarget::Path(&entries[entry_ix].path)); - entry_stack.push((entry_ix, cursor.start())); - } - entry_ix += 1; - } - } - - result - } - - /// TODO: Redo this entirely, API is wrong, conceptually it's a bit weird - /// Updates the `git_status` of the given entries such that files' - /// statuses bubble up to their ancestor directories. - pub fn propagate_git_statuses(&self, entries: &[Entry]) -> Vec> { - let mut cursor = all_statuses_cursor(self); - - let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); - let mut result = vec![None; entries.len()]; - let mut entry_ix = 0; - loop { - let next_entry = entries.get(entry_ix); - dbg!(&next_entry); - let containing_entry = entry_stack.last().map(|(ix, _)| &entries[*ix]); - dbg!(&containing_entry); - let entry_to_finish = match (containing_entry, next_entry) { - (Some(_), None) => entry_stack.pop(), - (Some(containing_entry), Some(next_path)) => { - if next_path.path.starts_with(&containing_entry.path) { - None - } else { - entry_stack.pop() - } - } - (None, Some(_)) => None, - (None, None) => break, - }; - - if let Some((entry_ix, prev_statuses)) = entry_to_finish { - dbg!(cursor.item()); - cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor( - &entries[entry_ix].path, - )); - - let statuses = cursor.start() - prev_statuses; + dbg!((&entries[entry_ix].path, &statuses)); result[entry_ix] = if statuses.conflict > 0 { Some(GitFileStatus::Conflict) @@ -2582,23 +2544,14 @@ impl Snapshot { Some(GitFileStatus::Modified) } else if statuses.added > 0 { Some(GitFileStatus::Added) - } else if statuses.untracked > 0 { - Some(GitFileStatus::Untracked) } else { None }; } else { if entries[entry_ix].is_dir() { - cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor( - &entries[entry_ix].path, - )); - dbg!(cursor.start()); - dbg!(cursor.item()); - entry_stack.push((entry_ix, cursor.start())); - } else { + dbg!("seeking for entry is_dir"); cursor.seek_forward(&GitEntryTraversalTarget::Path(&entries[entry_ix].path)); - dbg!(cursor.item()); - result[entry_ix] = cursor.item().map(|entry| entry.git_status) + entry_stack.push((entry_ix, cursor.start())); } entry_ix += 1; } @@ -3778,14 +3731,15 @@ where I: Iterator, { fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { - dbg!(target); loop { + dbg!("starting loop"); let cursor = match &mut self.current_cursor { Some(cursor) => cursor, None => { - let Some((_, entry)) = self.repositories.next() else { + let Some((work_dir, entry)) = self.repositories.next() else { break; }; + dbg!(work_dir); self.current_cursor = Some( entry .git_entries_by_path @@ -3795,9 +3749,10 @@ where } }; let found = cursor.seek_forward(target, Bias::Left, &()); - if found { + if dbg!(found) { break; } + self.statuses_so_far = cursor.start().1; self.current_cursor = None; } } @@ -3809,10 +3764,12 @@ where } fn start(&self) -> GitStatuses { + dbg!(self.current_cursor.is_none()); + if let Some(cursor) = self.current_cursor.as_ref() { - dbg!(cursor.start().1) + cursor.start().1 } else { - dbg!(GitStatuses::default()) + self.statuses_so_far } } } @@ -3821,18 +3778,23 @@ fn all_statuses_cursor<'a>( snapshot: &'a Snapshot, ) -> AllStatusesCursor<'a, impl Iterator> { + dbg!(snapshot + .repositories() + .map(|(workdir, _)| workdir) + .collect::>()); let mut repositories = snapshot.repositories(); - let cursor = util::maybe!({ - let (workdir, entry) = repositories.next()?; - Some( - entry - .git_entries_by_path - .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), - ) - }); + // let cursor = util::maybe!({ + // let (workdir, entry) = repositories.next()?; + // dbg!(workdir); + // Some( + // entry + // .git_entries_by_path + // .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), + // ) + // }); AllStatusesCursor { repositories, - current_cursor: cursor, + current_cursor: None, statuses_so_far: Default::default(), } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index ce9c6527db928..e0b1a85b5cbe2 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2759,7 +2759,6 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { "h1.txt": "", "h2.txt": "" }, - }), ) .await; @@ -2789,19 +2788,39 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { cx.executor().run_until_parked(); let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); + dbg!("********************************"); + check_propagated_statuses( + &snapshot, + &[ + (Path::new(""), Some(GitFileStatus::Conflict)), // This one is missing + // (Path::new("a"), Some(GitFileStatus::Added)), + // (Path::new("a/b"), Some(GitFileStatus::Modified)), // This one ISN'T + // (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), + // (Path::new("a/b/c2.txt"), None), + // (Path::new("a/d"), Some(GitFileStatus::Modified)), //This one is missing + // (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), + // (Path::new("f"), None), + // (Path::new("f/no-status.txt"), None), + (Path::new("g"), Some(GitFileStatus::Conflict)), // This one is missing + (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)), + ], + ); + + panic!("first test passed"); + check_propagated_statuses( &snapshot, &[ - (Path::new(""), Some(GitFileStatus::Conflict)), + (Path::new(""), Some(GitFileStatus::Conflict)), // This one is missing (Path::new("a"), Some(GitFileStatus::Modified)), - (Path::new("a/b"), Some(GitFileStatus::Added)), + (Path::new("a/b"), Some(GitFileStatus::Added)), // This one ISN'T (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), (Path::new("a/b/c2.txt"), None), - (Path::new("a/d"), Some(GitFileStatus::Modified)), + (Path::new("a/d"), Some(GitFileStatus::Modified)), //This one is missing (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), (Path::new("f"), None), (Path::new("f/no-status.txt"), None), - (Path::new("g"), Some(GitFileStatus::Conflict)), + (Path::new("g"), Some(GitFileStatus::Conflict)), // This one is missing (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)), ], ); @@ -2920,7 +2939,7 @@ fn check_propagated_statuses( .iter() .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone()) .collect::>(); - let statuses = snapshot.propagate_git_statuses2(&entries); + let statuses = snapshot.propagate_git_statuses(&entries); assert_eq!( entries .iter() From e8f3fc6fa5b2e72f71bfe995650daa6857e7f6dd Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 18 Dec 2024 16:39:07 -0800 Subject: [PATCH 11/22] One test to go --- crates/sum_tree/src/sum_tree.rs | 1 + crates/sum_tree/src/tree_map.rs | 1 + crates/worktree/src/worktree.rs | 81 +++++++-------------------- crates/worktree/src/worktree_tests.rs | 48 +++++++--------- 4 files changed, 42 insertions(+), 89 deletions(-) diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index fbfe3b06f3ab4..174b1c732d54b 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -341,6 +341,7 @@ impl SumTree { items } + /// NOTE: This iterator is not fused and may produce results after the iterator is complete pub fn iter(&self) -> Iter { Iter::new(self) } diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 9a4d952e93f22..a374b99580d0c 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -149,6 +149,7 @@ impl TreeMap { self.0 = new_map; } + /// NOTE: This iterator is not fused and may produce results after the iterator is complete pub fn iter(&self) -> impl Iterator + '_ { self.0.iter().map(|entry| (&entry.key, &entry.value)) } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index c098ad219d918..6322350a5a43b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -53,6 +53,7 @@ use std::{ ffi::OsStr, fmt, future::Future, + iter::FusedIterator, mem, ops::{Deref, DerefMut}, path::{Path, PathBuf}, @@ -66,7 +67,6 @@ use std::{ use sum_tree::{Bias, Cursor, Edit, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ - maybe, paths::{home_dir, PathMatcher, SanitizedPath}, ResultExt, }; @@ -2423,8 +2423,8 @@ impl Snapshot { pub fn repositories( &self, - ) -> impl Iterator { - self.repository_entries.iter() + ) -> impl Iterator + FusedIterator { + self.repository_entries.iter().fuse() } /// Get the repository whose work directory contains the given path. @@ -2491,30 +2491,18 @@ impl Snapshot { return None; } - let (work_directory, repo_entry) = - self.repository_and_work_directory_for_path(&entry.path)?; + let (_, repo_entry) = self.repository_and_work_directory_for_path(&entry.path)?; let RepoPath(path) = repo_entry.relativize(self, &entry.path).ok()?; let git_entry = repo_entry.git_entries_by_path.get(&PathKey(path), &())?; Some(git_entry.git_status) }) .collect::>(); - // dbg!(entries - // .iter() - // .zip(result.iter()) - // .map(|(entry, status)| { (entry.path.clone(), status) }) - // .collect::>()); - let mut entry_ix = 0; loop { let next_entry = entries.get(entry_ix); let containing_entry = entry_stack.last().map(|(ix, _)| &entries[*ix]); - dbg!(entry_stack - .iter() - .map(|(ix, statuses)| (entries[*ix].path.clone(), statuses)) - .collect::>()); - let entry_to_finish = match (containing_entry, next_entry) { (Some(_), None) => entry_stack.pop(), (Some(containing_entry), Some(next_path)) => { @@ -2529,14 +2517,11 @@ impl Snapshot { }; if let Some((entry_ix, prev_statuses)) = entry_to_finish { - dbg!("seeking for entry to finish"); - cursor.seek_forward(dbg!(&GitEntryTraversalTarget::PathSuccessor( - &entries[entry_ix].path - ))); + cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor( + &entries[entry_ix].path, + )); - let statuses = dbg!(cursor.start()) - dbg!(prev_statuses); - - dbg!((&entries[entry_ix].path, &statuses)); + let statuses = cursor.start() - prev_statuses; result[entry_ix] = if statuses.conflict > 0 { Some(GitFileStatus::Conflict) @@ -2549,7 +2534,6 @@ impl Snapshot { }; } else { if entries[entry_ix].is_dir() { - dbg!("seeking for entry is_dir"); cursor.seek_forward(&GitEntryTraversalTarget::Path(&entries[entry_ix].path)); entry_stack.push((entry_ix, cursor.start())); } @@ -3728,18 +3712,17 @@ struct AllStatusesCursor<'a, I> { impl<'a, I> AllStatusesCursor<'a, I> where - I: Iterator, + I: FusedIterator + Iterator, { fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { loop { - dbg!("starting loop"); let cursor = match &mut self.current_cursor { Some(cursor) => cursor, None => { - let Some((work_dir, entry)) = self.repositories.next() else { + let Some((_, entry)) = self.repositories.next() else { break; }; - dbg!(work_dir); + self.current_cursor = Some( entry .git_entries_by_path @@ -3748,26 +3731,18 @@ where self.current_cursor.as_mut().unwrap() } }; - let found = cursor.seek_forward(target, Bias::Left, &()); - if dbg!(found) { + cursor.seek_forward(target, Bias::Left, &()); + if cursor.item().is_some() { break; } - self.statuses_so_far = cursor.start().1; + self.statuses_so_far += cursor.start().1; self.current_cursor = None; } } - fn item(&self) -> Option<&GitEntry> { - self.current_cursor - .as_ref() - .and_then(|cursor| cursor.item()) - } - fn start(&self) -> GitStatuses { - dbg!(self.current_cursor.is_none()); - if let Some(cursor) = self.current_cursor.as_ref() { - cursor.start().1 + cursor.start().1 + self.statuses_so_far } else { self.statuses_so_far } @@ -3776,22 +3751,11 @@ where fn all_statuses_cursor<'a>( snapshot: &'a Snapshot, -) -> AllStatusesCursor<'a, impl Iterator> -{ - dbg!(snapshot - .repositories() - .map(|(workdir, _)| workdir) - .collect::>()); - let mut repositories = snapshot.repositories(); - // let cursor = util::maybe!({ - // let (workdir, entry) = repositories.next()?; - // dbg!(workdir); - // Some( - // entry - // .git_entries_by_path - // .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), - // ) - // }); +) -> AllStatusesCursor< + 'a, + impl Iterator + FusedIterator, +> { + let repositories = snapshot.repositories(); AllStatusesCursor { repositories, current_cursor: None, @@ -5414,10 +5378,7 @@ impl RepoPaths { Ok(ix) => { self.repo_paths.remove(ix); } - Err(_) => { - log::error!("Attempted to remove a repo path that was never added"); - debug_assert!(false); - } + Err(_) => {} } } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index e0b1a85b5cbe2..f4288b25037e8 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2189,7 +2189,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { ); assert_eq!( tree.status_for_file(Path::new("projects/project1/b")), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); }); @@ -2210,7 +2210,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { ); assert_eq!( tree.status_for_file(Path::new("projects/project2/b")), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); }); } @@ -2498,7 +2498,7 @@ async fn test_git_status(cx: &mut TestAppContext) { let snapshot = tree.snapshot(); assert_eq!( snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); }); @@ -2522,7 +2522,7 @@ async fn test_git_status(cx: &mut TestAppContext) { .join(Path::new(renamed_dir_name)) .join(RENAMED_FILE) ), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); }); } @@ -2650,7 +2650,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { cx.executor().allow_parking(); let root = temp_tree(json!({ - "my-repo)": { + "my-repo": { // .git folder will go here "a.txt": "a", "sub-folder-1": { @@ -2711,7 +2711,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { assert_eq!(snapshot.status_for_file("c.txt"), None); assert_eq!( snapshot.status_for_file("d/e.txt"), - Some(GitFileStatus::Added) + Some(GitFileStatus::Untracked) ); }); @@ -2788,35 +2788,24 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { cx.executor().run_until_parked(); let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); - dbg!("********************************"); check_propagated_statuses( &snapshot, &[ - (Path::new(""), Some(GitFileStatus::Conflict)), // This one is missing - // (Path::new("a"), Some(GitFileStatus::Added)), - // (Path::new("a/b"), Some(GitFileStatus::Modified)), // This one ISN'T - // (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), - // (Path::new("a/b/c2.txt"), None), - // (Path::new("a/d"), Some(GitFileStatus::Modified)), //This one is missing - // (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), - // (Path::new("f"), None), - // (Path::new("f/no-status.txt"), None), - (Path::new("g"), Some(GitFileStatus::Conflict)), // This one is missing + (Path::new(""), Some(GitFileStatus::Conflict)), + (Path::new("g"), Some(GitFileStatus::Conflict)), (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)), ], ); - panic!("first test passed"); - check_propagated_statuses( &snapshot, &[ - (Path::new(""), Some(GitFileStatus::Conflict)), // This one is missing + (Path::new(""), Some(GitFileStatus::Conflict)), (Path::new("a"), Some(GitFileStatus::Modified)), - (Path::new("a/b"), Some(GitFileStatus::Added)), // This one ISN'T + (Path::new("a/b"), Some(GitFileStatus::Added)), (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), (Path::new("a/b/c2.txt"), None), - (Path::new("a/d"), Some(GitFileStatus::Modified)), //This one is missing + (Path::new("a/d"), Some(GitFileStatus::Modified)), (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), (Path::new("f"), None), (Path::new("f/no-status.txt"), None), @@ -2853,7 +2842,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_propagate_statuses_two_repos(cx: &mut TestAppContext) { +async fn test_propagate_statuses_repos_under_project(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( @@ -2861,11 +2850,11 @@ async fn test_propagate_statuses_two_repos(cx: &mut TestAppContext) { json!({ "x": { ".git": {}, - "x1": "foo" + "x1.txt": "foo" }, "y": { ".git": {}, - "y1": "bar" + "y1.txt": "bar" } }), ) @@ -2873,11 +2862,11 @@ async fn test_propagate_statuses_two_repos(cx: &mut TestAppContext) { fs.set_status_for_repo_via_git_operation( Path::new("/root/x/.git"), - &[(Path::new("x1"), GitFileStatus::Added)], + &[(Path::new("x1.txt"), GitFileStatus::Added)], ); fs.set_status_for_repo_via_git_operation( Path::new("/root/y/.git"), - &[(Path::new("y1"), GitFileStatus::Conflict)], + &[(Path::new("y1.txt"), GitFileStatus::Conflict)], ); let tree = Worktree::local( @@ -2899,10 +2888,11 @@ async fn test_propagate_statuses_two_repos(cx: &mut TestAppContext) { check_propagated_statuses( &snapshot, &[ + // (Path::new(""), None), // /root, doesn't have a git repository in it and so should not have a git repository status (Path::new("x"), Some(GitFileStatus::Added)), - (Path::new("x/x1"), Some(GitFileStatus::Added)), + (Path::new("x/x1.txt"), Some(GitFileStatus::Added)), (Path::new("y"), Some(GitFileStatus::Conflict)), - (Path::new("y/y1"), Some(GitFileStatus::Conflict)), + (Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)), ], ); } From 51c301620d04d7935d98105f4c2b406209f5d576 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 18 Dec 2024 17:38:42 -0800 Subject: [PATCH 12/22] Fix several bugs in how statuses are computed for multiple sub repositories co-authored-by: Cole --- crates/worktree/src/worktree.rs | 46 ++++++++++++++++---- crates/worktree/src/worktree_tests.rs | 61 +++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 6322350a5a43b..102ee60f8ab81 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3706,7 +3706,10 @@ impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses struct AllStatusesCursor<'a, I> { repositories: I, - current_cursor: Option, GitStatuses)>>, + current_cursor: Option<( + RepositoryWorkDirectory, + Cursor<'a, GitEntry, (TraversalProgress<'a>, GitStatuses)>, + )>, statuses_so_far: GitStatuses, } @@ -3715,33 +3718,58 @@ where I: FusedIterator + Iterator, { fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { + let mut target_was_in_last_repo = false; loop { - let cursor = match &mut self.current_cursor { + let (work_dir, cursor) = match &mut self.current_cursor { Some(cursor) => cursor, None => { - let Some((_, entry)) = self.repositories.next() else { + let Some((work_dir, entry)) = self.repositories.next() else { break; }; - self.current_cursor = Some( + self.current_cursor = Some(( + work_dir.clone(), entry .git_entries_by_path .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), - ); + )); self.current_cursor.as_mut().unwrap() } }; - cursor.seek_forward(target, Bias::Left, &()); - if cursor.item().is_some() { - break; + + let path = match target { + GitEntryTraversalTarget::PathSuccessor(path) => path, + GitEntryTraversalTarget::Path(path) => path, + }; + if let Some(relative_path) = path.strip_prefix(&work_dir.0).ok() { + target_was_in_last_repo = true; + + let new_target = match target { + GitEntryTraversalTarget::PathSuccessor(_) => { + GitEntryTraversalTarget::PathSuccessor(relative_path.as_ref()) + } + GitEntryTraversalTarget::Path(_) => { + GitEntryTraversalTarget::Path(relative_path.as_ref()) + } + }; + + cursor.seek_forward(&new_target, Bias::Left, &()); + if cursor.item().is_some() { + break; + } + } else { + if target_was_in_last_repo { + break; + } } + self.statuses_so_far += cursor.start().1; self.current_cursor = None; } } fn start(&self) -> GitStatuses { - if let Some(cursor) = self.current_cursor.as_ref() { + if let Some((_, cursor)) = self.current_cursor.as_ref() { cursor.start().1 + self.statuses_so_far } else { self.statuses_so_far diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index f4288b25037e8..c6dd6d01dc975 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2842,7 +2842,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_propagate_statuses_repos_under_project(cx: &mut TestAppContext) { +async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( @@ -2850,11 +2850,18 @@ async fn test_propagate_statuses_repos_under_project(cx: &mut TestAppContext) { json!({ "x": { ".git": {}, - "x1.txt": "foo" + "x1.txt": "foo", + "x2.txt": "bar" }, "y": { ".git": {}, - "y1.txt": "bar" + "y1.txt": "baz", + "y2.txt": "qux" + }, + "z": { + ".git": {}, + "z1.txt": "quux", + "z2.txt": "quuux" } }), ) @@ -2868,6 +2875,14 @@ async fn test_propagate_statuses_repos_under_project(cx: &mut TestAppContext) { Path::new("/root/y/.git"), &[(Path::new("y1.txt"), GitFileStatus::Conflict)], ); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/y/.git"), + &[(Path::new("y2.txt"), GitFileStatus::Modified)], + ); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/z/.git"), + &[(Path::new("z2.txt"), GitFileStatus::Modified)], + ); let tree = Worktree::local( Path::new("/root"), @@ -2888,11 +2903,49 @@ async fn test_propagate_statuses_repos_under_project(cx: &mut TestAppContext) { check_propagated_statuses( &snapshot, &[ - // (Path::new(""), None), // /root, doesn't have a git repository in it and so should not have a git repository status (Path::new("x"), Some(GitFileStatus::Added)), (Path::new("x/x1.txt"), Some(GitFileStatus::Added)), + ], + ); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new("y"), Some(GitFileStatus::Conflict)), + (Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)), + ], + ); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new("z"), Some(GitFileStatus::Modified)), + (Path::new("z/z2.txt"), Some(GitFileStatus::Modified)), + ], + ); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new(""), None), // /root doesn't have a git repository in it and so should not have a git repository status + (Path::new("x"), Some(GitFileStatus::Added)), + (Path::new("x/x1.txt"), Some(GitFileStatus::Added)), + ], + ); + + check_propagated_statuses( + &snapshot, + &[ + (Path::new(""), None), + (Path::new("x"), Some(GitFileStatus::Added)), + (Path::new("x/x1.txt"), Some(GitFileStatus::Added)), + (Path::new("x/x2.txt"), None), (Path::new("y"), Some(GitFileStatus::Conflict)), (Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)), + (Path::new("y/y2.txt"), Some(GitFileStatus::Modified)), + (Path::new("z"), Some(GitFileStatus::Modified)), + (Path::new("z/z1.txt"), None), + (Path::new("z/z2.txt"), Some(GitFileStatus::Modified)), ], ); } From 2f037263f6441e654dae105e965eca9e8f3a4d97 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 19 Dec 2024 17:23:58 -0500 Subject: [PATCH 13/22] Debugging --- crates/worktree/src/worktree.rs | 122 ++++++++++++++++---------- crates/worktree/src/worktree_tests.rs | 3 + 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 102ee60f8ab81..436d2c1620122 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -54,7 +54,7 @@ use std::{ fmt, future::Future, iter::FusedIterator, - mem, + mem::{self, take}, ops::{Deref, DerefMut}, path::{Path, PathBuf}, pin::Pin, @@ -3681,12 +3681,30 @@ impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for TraversalProgress<'a> { } } -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] enum GitEntryTraversalTarget<'a> { PathSuccessor(&'a Path), Path(&'a Path), } +impl<'a> GitEntryTraversalTarget<'a> { + fn path(&self) -> &'a Path { + match self { + GitEntryTraversalTarget::Path(path) => path, + GitEntryTraversalTarget::PathSuccessor(path) => path, + } + } + + fn with_path(self, path: &Path) -> GitEntryTraversalTarget<'_> { + match self { + GitEntryTraversalTarget::PathSuccessor(_) => { + GitEntryTraversalTarget::PathSuccessor(path) + } + GitEntryTraversalTarget::Path(_) => GitEntryTraversalTarget::Path(path), + } + } +} + impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses)> for GitEntryTraversalTarget<'b> { @@ -3705,74 +3723,89 @@ impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses } struct AllStatusesCursor<'a, I> { - repositories: I, - current_cursor: Option<( - RepositoryWorkDirectory, + repos: I, + snapshot: &'a Snapshot, + current_location: Option<( + &'a RepositoryEntry, Cursor<'a, GitEntry, (TraversalProgress<'a>, GitStatuses)>, )>, - statuses_so_far: GitStatuses, + statuses_before_current_repo: GitStatuses, } impl<'a, I> AllStatusesCursor<'a, I> where - I: FusedIterator + Iterator, + I: Iterator + FusedIterator, { fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { - let mut target_was_in_last_repo = false; + eprintln!("seek to {target:?}"); + let mut hold_at_beginning = false; + loop { - let (work_dir, cursor) = match &mut self.current_cursor { - Some(cursor) => cursor, + let (entry, cursor) = match &mut self.current_location { + Some(location) => location, None => { - let Some((work_dir, entry)) = self.repositories.next() else { + let Some((work_dir, entry)) = self.repos.next() else { + eprintln!("exhausted repositories"); break; }; + eprintln!("next repository: {work_dir:?}"); - self.current_cursor = Some(( - work_dir.clone(), + self.current_location.insert(( + entry, entry .git_entries_by_path .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), - )); - self.current_cursor.as_mut().unwrap() + )) } }; - let path = match target { - GitEntryTraversalTarget::PathSuccessor(path) => path, - GitEntryTraversalTarget::Path(path) => path, - }; - if let Some(relative_path) = path.strip_prefix(&work_dir.0).ok() { - target_was_in_last_repo = true; - - let new_target = match target { - GitEntryTraversalTarget::PathSuccessor(_) => { - GitEntryTraversalTarget::PathSuccessor(relative_path.as_ref()) - } - GitEntryTraversalTarget::Path(_) => { - GitEntryTraversalTarget::Path(relative_path.as_ref()) - } - }; + if take(&mut hold_at_beginning) { + eprintln!("hold at beginning"); + break; + } - cursor.seek_forward(&new_target, Bias::Left, &()); - if cursor.item().is_some() { + if let Ok(RepoPath(repo_path)) = entry.relativize(self.snapshot, target.path()) { + let target = &target.with_path(&repo_path); + eprintln!("internal seek to {target:?}"); + cursor.seek_forward(target, Bias::Left, &()); + if let Some(item) = cursor.item() { + eprintln!("found {item:?}"); break; } - } else { - if target_was_in_last_repo { - break; + match target { + GitEntryTraversalTarget::Path(_) => panic!("wat"), + GitEntryTraversalTarget::PathSuccessor(path) => { + eprintln!("end of repo, hold"); + hold_at_beginning = true; + } } + } else { + eprintln!("seek to end of repo"); + cursor.seek_forward( + // FIXME + &GitEntryTraversalTarget::Path("2775f0d7-ad3a-4ae0-b921-85ce581b258c".as_ref()), + Bias::Left, + &(), + ); } - self.statuses_so_far += cursor.start().1; - self.current_cursor = None; + let old_statuses = self.statuses_before_current_repo; + self.statuses_before_current_repo += cursor.start().1; + eprintln!( + "adding: {:?} + {:?} = {:?}", + old_statuses, + cursor.start().1, + self.statuses_before_current_repo + ); + self.current_location = None; } } fn start(&self) -> GitStatuses { - if let Some((_, cursor)) = self.current_cursor.as_ref() { - cursor.start().1 + self.statuses_so_far + if let Some((_, cursor)) = self.current_location.as_ref() { + cursor.start().1 + self.statuses_before_current_repo } else { - self.statuses_so_far + self.statuses_before_current_repo } } } @@ -3783,11 +3816,12 @@ fn all_statuses_cursor<'a>( 'a, impl Iterator + FusedIterator, > { - let repositories = snapshot.repositories(); + let repos = snapshot.repositories(); AllStatusesCursor { - repositories, - current_cursor: None, - statuses_so_far: Default::default(), + snapshot, + repos, + current_location: None, + statuses_before_current_repo: Default::default(), } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index c6dd6d01dc975..597c5861b6f63 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2908,11 +2908,14 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext ], ); + eprintln!("*******************************"); + check_propagated_statuses( &snapshot, &[ (Path::new("y"), Some(GitFileStatus::Conflict)), (Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)), + (Path::new("y/y2.txt"), Some(GitFileStatus::Modified)), ], ); From cf68eabc790ae4e01673a62d501a10f7ffe3e77d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 19 Dec 2024 15:53:38 -0800 Subject: [PATCH 14/22] Finish re-implementing status propagation co-authored-by: Cole --- crates/git/src/repository.rs | 2 + crates/sum_tree/src/cursor.rs | 1 + crates/sum_tree/src/sum_tree.rs | 1 - crates/sum_tree/src/tree_map.rs | 1 - crates/worktree/src/worktree.rs | 71 ++++++--------- crates/worktree/src/worktree_tests.rs | 121 +++++++++++++++++++++++--- 6 files changed, 140 insertions(+), 57 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 3a5b59f3b9373..0eb6aad06a12c 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -292,6 +292,7 @@ impl GitRepository for FakeGitRepository { fn status(&self, path_prefixes: &[RepoPath]) -> Result { let state = self.state.lock(); + let mut entries = state .worktree_statuses .iter() @@ -307,6 +308,7 @@ impl GitRepository for FakeGitRepository { }) .collect::>(); entries.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + Ok(GitStatus { entries: entries.into(), }) diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 773e7db88bad3..dfa167842d9cc 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -60,6 +60,7 @@ where } } + /// Item is None, when the list is empty, or this cursor is at the end of the list. #[track_caller] pub fn item(&self) -> Option<&'a T> { self.assert_did_seek(); diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 174b1c732d54b..fbfe3b06f3ab4 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -341,7 +341,6 @@ impl SumTree { items } - /// NOTE: This iterator is not fused and may produce results after the iterator is complete pub fn iter(&self) -> Iter { Iter::new(self) } diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index a374b99580d0c..9a4d952e93f22 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -149,7 +149,6 @@ impl TreeMap { self.0 = new_map; } - /// NOTE: This iterator is not fused and may produce results after the iterator is complete pub fn iter(&self) -> impl Iterator + '_ { self.0.iter().map(|entry| (&entry.key, &entry.value)) } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 436d2c1620122..4a4ed3405dd3a 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -54,7 +54,7 @@ use std::{ fmt, future::Future, iter::FusedIterator, - mem::{self, take}, + mem::{self}, ops::{Deref, DerefMut}, path::{Path, PathBuf}, pin::Pin, @@ -2423,8 +2423,8 @@ impl Snapshot { pub fn repositories( &self, - ) -> impl Iterator + FusedIterator { - self.repository_entries.iter().fuse() + ) -> impl Iterator { + self.repository_entries.iter() } /// Get the repository whose work directory contains the given path. @@ -3703,6 +3703,19 @@ impl<'a> GitEntryTraversalTarget<'a> { GitEntryTraversalTarget::Path(_) => GitEntryTraversalTarget::Path(path), } } + + fn cmp_path(&self, other: &Path) -> std::cmp::Ordering { + self.cmp( + &( + TraversalProgress { + max_path: other, + ..Default::default() + }, + Default::default(), + ), + &(), + ) + } } impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses)> @@ -3724,9 +3737,8 @@ impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses struct AllStatusesCursor<'a, I> { repos: I, - snapshot: &'a Snapshot, current_location: Option<( - &'a RepositoryEntry, + &'a RepositoryWorkDirectory, Cursor<'a, GitEntry, (TraversalProgress<'a>, GitStatuses)>, )>, statuses_before_current_repo: GitStatuses, @@ -3737,21 +3749,16 @@ where I: Iterator + FusedIterator, { fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { - eprintln!("seek to {target:?}"); - let mut hold_at_beginning = false; - loop { - let (entry, cursor) = match &mut self.current_location { + let (work_dir, cursor) = match &mut self.current_location { Some(location) => location, None => { let Some((work_dir, entry)) = self.repos.next() else { - eprintln!("exhausted repositories"); break; }; - eprintln!("next repository: {work_dir:?}"); self.current_location.insert(( - entry, + work_dir, entry .git_entries_by_path .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), @@ -3759,44 +3766,20 @@ where } }; - if take(&mut hold_at_beginning) { - eprintln!("hold at beginning"); - break; - } - - if let Ok(RepoPath(repo_path)) = entry.relativize(self.snapshot, target.path()) { + if let Some(repo_path) = target.path().strip_prefix(&work_dir.0).ok() { let target = &target.with_path(&repo_path); - eprintln!("internal seek to {target:?}"); cursor.seek_forward(target, Bias::Left, &()); - if let Some(item) = cursor.item() { - eprintln!("found {item:?}"); + if let Some(_) = cursor.item() { break; } - match target { - GitEntryTraversalTarget::Path(_) => panic!("wat"), - GitEntryTraversalTarget::PathSuccessor(path) => { - eprintln!("end of repo, hold"); - hold_at_beginning = true; - } - } + } else if target.cmp_path(&work_dir.0).is_gt() { + // Fill the cursor with everything from this intermediary repository + cursor.seek_forward(target, Bias::Right, &()); } else { - eprintln!("seek to end of repo"); - cursor.seek_forward( - // FIXME - &GitEntryTraversalTarget::Path("2775f0d7-ad3a-4ae0-b921-85ce581b258c".as_ref()), - Bias::Left, - &(), - ); + break; } - let old_statuses = self.statuses_before_current_repo; self.statuses_before_current_repo += cursor.start().1; - eprintln!( - "adding: {:?} + {:?} = {:?}", - old_statuses, - cursor.start().1, - self.statuses_before_current_repo - ); self.current_location = None; } } @@ -3816,9 +3799,8 @@ fn all_statuses_cursor<'a>( 'a, impl Iterator + FusedIterator, > { - let repos = snapshot.repositories(); + let repos = snapshot.repositories().fuse(); AllStatusesCursor { - snapshot, repos, current_location: None, statuses_before_current_repo: Default::default(), @@ -4725,7 +4707,6 @@ impl BackgroundScanner { abs_paths: Vec, scan_queue_tx: Option>, ) { - eprintln!("reload_entries_for_paths({relative_paths:?})"); // grab metadata for all requested paths let metadata = futures::future::join_all( abs_paths diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 597c5861b6f63..ff7c74dcd90d4 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2873,11 +2873,10 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext ); fs.set_status_for_repo_via_git_operation( Path::new("/root/y/.git"), - &[(Path::new("y1.txt"), GitFileStatus::Conflict)], - ); - fs.set_status_for_repo_via_git_operation( - Path::new("/root/y/.git"), - &[(Path::new("y2.txt"), GitFileStatus::Modified)], + &[ + (Path::new("y1.txt"), GitFileStatus::Conflict), + (Path::new("y2.txt"), GitFileStatus::Modified), + ], ); fs.set_status_for_repo_via_git_operation( Path::new("/root/z/.git"), @@ -2894,10 +2893,11 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext .await .unwrap(); + tree.flush_fs_events(cx).await; cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; - cx.executor().run_until_parked(); + let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); check_propagated_statuses( @@ -2908,8 +2908,6 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext ], ); - eprintln!("*******************************"); - check_propagated_statuses( &snapshot, &[ @@ -2930,7 +2928,6 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext check_propagated_statuses( &snapshot, &[ - (Path::new(""), None), // /root doesn't have a git repository in it and so should not have a git repository status (Path::new("x"), Some(GitFileStatus::Added)), (Path::new("x/x1.txt"), Some(GitFileStatus::Added)), ], @@ -2939,7 +2936,6 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext check_propagated_statuses( &snapshot, &[ - (Path::new(""), None), (Path::new("x"), Some(GitFileStatus::Added)), (Path::new("x/x1.txt"), Some(GitFileStatus::Added)), (Path::new("x/x2.txt"), None), @@ -2953,6 +2949,111 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext ); } +#[gpui::test] +async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "x": { + ".git": {}, + "x1.txt": "foo", + "x2.txt": "bar", + "y": { + ".git": {}, + "y1.txt": "baz", + "y2.txt": "qux" + } + }, + }), + ) + .await; + + fs.set_status_for_repo_via_git_operation( + Path::new("/root/x/.git"), + &[(Path::new("x2.txt"), GitFileStatus::Modified)], + ); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/x/y/.git"), + &[(Path::new("y1.txt"), GitFileStatus::Conflict)], + ); + + let tree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + tree.flush_fs_events(cx).await; + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.executor().run_until_parked(); + + let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); + + // Sanity check the propagation for x/y + check_propagated_statuses( + &snapshot, + &[ + (Path::new("x/y"), Some(GitFileStatus::Conflict)), // the y git repository has conflict file in it, and so should have a conflict status + (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)), + (Path::new("x/y/y2.txt"), None), + ], + ); + + // Test one of the fundamental cases of propogation blocking, the transition from one git repository to another + check_propagated_statuses( + &snapshot, + &[ + (Path::new("x"), Some(GitFileStatus::Conflict)), // FIXME: This should be Some(Modified) + (Path::new("x/y"), Some(GitFileStatus::Conflict)), + (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)), + ], + ); + + // Sanity check everything around it + check_propagated_statuses( + &snapshot, + &[ + (Path::new("x"), Some(GitFileStatus::Conflict)), // FIXME: This should be Some(Modified) + (Path::new("x/x1.txt"), None), + (Path::new("x/x2.txt"), Some(GitFileStatus::Modified)), + (Path::new("x/y"), Some(GitFileStatus::Conflict)), + (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)), + (Path::new("x/y/y2.txt"), None), + ], + ); + + // Test the other fundamental case, transitioning from git repository to non-git repository + check_propagated_statuses( + &snapshot, + &[ + (Path::new(""), Some(GitFileStatus::Conflict)), // FIXME: This should be None + (Path::new("x"), Some(GitFileStatus::Conflict)), // FIXME: This should be Some(Modified) + (Path::new("x/x1.txt"), None), + ], + ); + + // And all together now + check_propagated_statuses( + &snapshot, + &[ + (Path::new(""), Some(GitFileStatus::Conflict)), // FIXME: This should be None + (Path::new("x"), Some(GitFileStatus::Conflict)), // FIXME: This should be Some(Modified) + (Path::new("x/x1.txt"), None), + (Path::new("x/x2.txt"), Some(GitFileStatus::Modified)), + (Path::new("x/y"), Some(GitFileStatus::Conflict)), + (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)), + (Path::new("x/y/y2.txt"), None), + ], + ); +} + #[gpui::test] async fn test_private_single_file_worktree(cx: &mut TestAppContext) { init_test(cx); From ceaffa89b2b0d2c14dbc7b345262f33352cf6336 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 19 Dec 2024 17:04:32 -0800 Subject: [PATCH 15/22] Roll out some of the git status changes through zed co-authored-by: cole --- Cargo.lock | 1 + crates/collab/src/db/queries/projects.rs | 1 - crates/collab/src/db/queries/rooms.rs | 1 - crates/editor/src/git/project_diff.rs | 2 +- crates/editor/src/items.rs | 21 +++- crates/git/src/repository.rs | 7 ++ crates/git_ui/Cargo.toml | 5 +- crates/git_ui/src/git_panel.rs | 123 +++++++++++------------ crates/git_ui/src/git_ui.rs | 5 +- crates/project/src/buffer_store.rs | 19 ++-- crates/project/src/lsp_store.rs | 1 + crates/project/src/project.rs | 13 +-- crates/project/src/worktree_store.rs | 13 +++ crates/title_bar/src/title_bar.rs | 4 +- crates/worktree/src/worktree.rs | 49 +++++---- 15 files changed, 155 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fce0344794fd..f3365887f9d4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5194,6 +5194,7 @@ dependencies = [ "util", "windows 0.58.0", "workspace", + "worktree", ] [[package]] diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 7ff8aa7a9fbb1..064d683b088df 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -643,7 +643,6 @@ impl Database { canonical_path: db_entry.canonical_path, is_ignored: db_entry.is_ignored, is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), // This is only used in the summarization backlog, so if it's None, // that just means we won't be able to detect when to resummarize // based on total number of backlogged bytes - instead, we'd go diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index a3a99bee71a44..902c2e001fcea 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -662,7 +662,6 @@ impl Database { canonical_path: db_entry.canonical_path, is_ignored: db_entry.is_ignored, is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), // This is only used in the summarization backlog, so if it's None, // that just means we won't be able to detect when to resummarize // based on total number of backlogged bytes - instead, we'd go diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index e76e5922dbe9a..7dc4e0d892816 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -19,7 +19,7 @@ use gpui::{ }; use language::{Buffer, BufferRow}; use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}; -use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; +use project::{Entry, Project, ProjectEntryId, ProjectPath, WorktreeId}; use text::{OffsetRangeExt, ToPoint}; use theme::ActiveTheme; use ui::prelude::*; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 298ef5a3f0609..1a53da3ae5436 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -615,9 +615,20 @@ impl Item for Editor { .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).project_path(cx)) - .and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx)) - .map(|entry| { - entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected) + .and_then(|path| { + let project = self.project.as_ref()?.read(cx); + let entry = project.entry_for_path(&path, cx)?; + let git_status = project + .worktree_for_id(path.worktree_id, cx)? + .read(cx) + .snapshot() + .status_for_file(path.path); + + Some(entry_git_aware_label_color( + git_status, + entry.is_ignored, + params.selected, + )) }) .unwrap_or_else(|| entry_label_color(params.selected)) } else { @@ -1559,10 +1570,10 @@ pub fn entry_git_aware_label_color( Color::Ignored } else { match git_status { - Some(GitFileStatus::Added) => Color::Created, + Some(GitFileStatus::Added) | Some(GitFileStatus::Untracked) => Color::Created, Some(GitFileStatus::Modified) => Color::Modified, Some(GitFileStatus::Conflict) => Color::Conflict, - None => entry_label_color(selected), + Some(GitFileStatus::Deleted) | None => entry_label_color(selected), } } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 0eb6aad06a12c..d2515237b3400 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -7,6 +7,7 @@ use gpui::SharedString; use parking_lot::Mutex; use rope::Rope; use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; use std::sync::LazyLock; use std::{ cmp::Ordering, @@ -485,6 +486,12 @@ impl std::ops::Deref for RepoPath { } } +impl Borrow for RepoPath { + fn borrow(&self) -> &Path { + self.0.as_ref() + } +} + #[derive(Debug)] pub struct RepoPathDescendants<'a>(pub &'a Path); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 87ba730bf9b77..65358e96b6999 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -14,7 +14,9 @@ path = "src/git_ui.rs" [dependencies] anyhow.workspace = true +collections.workspace = true db.workspace = true +git.workspace = true gpui.workspace = true project.workspace = true schemars.workspace = true @@ -25,8 +27,7 @@ settings.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -git.workspace = true -collections.workspace = true +worktree.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ea7585d978540..c89db9f7d003b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,4 +1,3 @@ -use collections::HashMap; use std::{ cell::OnceCell, collections::HashSet, @@ -8,14 +7,15 @@ use std::{ sync::Arc, time::Duration, }; +use worktree::GitEntry; -use git::repository::GitFileStatus; +use git::repository::{GitFileStatus, RepoPath}; use util::{ResultExt, TryFutureExt}; use db::kvp::KEY_VALUE_STORE; use gpui::*; -use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId}; +use project::{EntryKind, Fs, Project, ProjectEntryId, WorktreeId}; use serde::{Deserialize, Serialize}; use settings::Settings as _; use ui::{ @@ -81,13 +81,18 @@ pub struct GitPanel { project: Model, scroll_handle: UniformListScrollHandle, scrollbar_state: ScrollbarState, - selected_item: Option, + _selected_item: Option, show_scrollbar: bool, - expanded_dir_ids: HashMap>, + // todo!(): Reintroduce expanded directories, once we're deriving directories from paths + // expanded_dir_ids: HashMap>, // The entries that are currently shown in the panel, aka // not hidden by folding or such - visible_entries: Vec<(WorktreeId, Vec, OnceCell>>)>, + visible_entries: Vec<( + WorktreeId, + Vec, + OnceCell>, + )>, width: Option, } @@ -116,8 +121,11 @@ impl GitPanel { }) .detach(); cx.subscribe(&project, |this, _project, event, cx| match event { - project::Event::WorktreeRemoved(id) => { - this.expanded_dir_ids.remove(id); + project::Event::GitRepositoryUpdated => { + this.update_visible_entries(None, cx); + } + project::Event::WorktreeRemoved(_id) => { + // this.expanded_dir_ids.remove(id); this.update_visible_entries(None, cx); cx.notify(); } @@ -141,12 +149,11 @@ impl GitPanel { project, visible_entries: Vec::new(), current_modifiers: cx.modifiers(), - expanded_dir_ids: Default::default(), - + // expanded_dir_ids: Default::default(), width: Some(px(360.)), scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()), scroll_handle, - selected_item: None, + _selected_item: None, show_scrollbar: !Self::should_autohide_scrollbar(cx), hide_scrollbar_task: None, }; @@ -225,8 +232,8 @@ impl GitPanel { } fn calculate_depth_and_difference( - entry: &Entry, - visible_worktree_entries: &HashSet>, + entry: &GitEntry, + visible_worktree_entries: &HashSet, ) -> (usize, usize) { let (depth, difference) = entry .path @@ -293,12 +300,7 @@ impl GitPanel { fn entry_count(&self) -> usize { self.visible_entries .iter() - .map(|(_, entries, _)| { - entries - .iter() - .filter(|entry| entry.git_status.is_some()) - .count() - }) + .map(|(_, entries, _)| entries.len()) .sum() } @@ -306,7 +308,7 @@ impl GitPanel { &self, range: Range, cx: &mut ViewContext, - mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), + mut callback: impl FnMut(usize, EntryDetails, &mut ViewContext), ) { let mut ix = 0; for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries { @@ -324,23 +326,23 @@ impl GitPanel { if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { let snapshot = worktree.read(cx).snapshot(); let root_name = OsStr::new(snapshot.root_name()); - let expanded_entry_ids = self - .expanded_dir_ids - .get(&snapshot.id()) - .map(Vec::as_slice) - .unwrap_or(&[]); + // let expanded_entry_ids = self + // .expanded_dir_ids + // .get(&snapshot.id()) + // .map(Vec::as_slice) + // .unwrap_or(&[]); let entry_range = range.start.saturating_sub(ix)..end_ix - ix; - let entries = entries_paths.get_or_init(|| { + let entries: &HashSet = entries_paths.get_or_init(|| { visible_worktree_entries .iter() .map(|e| (e.path.clone())) .collect() }); - for entry in visible_worktree_entries[entry_range].iter() { + for (ix, entry) in visible_worktree_entries[entry_range].iter().enumerate() { let status = entry.git_status; - let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); + let is_expanded = true; //expanded_entry_ids.binary_search(&entry.id).is_ok(); let (depth, difference) = Self::calculate_depth_and_difference(entry, entries); @@ -365,13 +367,13 @@ impl GitPanel { let details = EntryDetails { filename, display_name, - kind: entry.kind, + kind: EntryKind::File, is_expanded, - path: entry.path.clone(), - status, + path: entry.path.0.clone(), + status: Some(status), depth, }; - callback(entry.id, details, cx); + callback(ix, details, cx); } } ix = end_ix; @@ -381,7 +383,7 @@ impl GitPanel { // todo!(): Update expanded directory state fn update_visible_entries( &mut self, - new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, + _new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, cx: &mut ViewContext, ) { let project = self.project.read(cx); @@ -391,17 +393,14 @@ impl GitPanel { let worktree_id = snapshot.id(); let mut visible_worktree_entries = Vec::new(); - let mut entry_iter = snapshot.entries(true, 0); - while let Some(entry) = entry_iter.entry() { - // Only include entries with a git status - if entry.git_status.is_some() { - visible_worktree_entries.push(entry.clone()); - } - entry_iter.advance(); + let repositories = snapshot.repositories().take(1); // Only use the first for now + for (work_dir, _) in repositories { + visible_worktree_entries + .extend(snapshot.git_status(&work_dir).unwrap_or(Vec::new())); } - snapshot.propagate_git_statuses(&mut visible_worktree_entries); - project::sort_worktree_entries(&mut visible_worktree_entries); + // let statuses = snapshot.propagate_git_statuses(&visible_worktree_entries); + // project::sort_worktree_entries(&mut visible_worktree_entries); if !visible_worktree_entries.is_empty() { self.visible_entries @@ -409,20 +408,21 @@ impl GitPanel { } } - if let Some((worktree_id, entry_id)) = new_selected_entry { - self.selected_item = self.visible_entries.iter().enumerate().find_map( - |(worktree_index, (id, entries, _))| { - if *id == worktree_id { - entries - .iter() - .position(|entry| entry.id == entry_id) - .map(|entry_index| worktree_index * entries.len() + entry_index) - } else { - None - } - }, - ); - } + // todo!(): re-implement this + // if let Some((worktree_id, entry_id)) = new_selected_entry { + // self.selected_item = self.visible_entries.iter().enumerate().find_map( + // |(worktree_index, (id, entries, _))| { + // if *id == worktree_id { + // entries + // .iter() + // .position(|entry| entry.id == entry_id) + // .map(|entry_index| worktree_index * entries.len() + entry_index) + // } else { + // None + // } + // }, + // ); + // } cx.notify(); } @@ -635,8 +635,8 @@ impl GitPanel { uniform_list(cx.view().clone(), "entries", item_count, { |this, range, cx| { let mut items = Vec::with_capacity(range.end - range.start); - this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(this.render_entry(id, details, cx)); + this.for_each_visible_entry(range, cx, |ix, details, cx| { + items.push(this.render_entry(ix, details, cx)); }); items } @@ -652,16 +652,15 @@ impl GitPanel { fn render_entry( &self, - id: ProjectEntryId, + ix: usize, details: EntryDetails, cx: &ViewContext, ) -> impl IntoElement { - let id = id.to_proto() as usize; - let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into()); + let checkbox_id = ElementId::Name(format!("checkbox_{}", ix).into()); let is_staged = ToggleState::Selected; h_flex() - .id(id) + .id(("git-panel-entry", ix)) .h(px(28.)) .w_full() .pl(px(12. + 12. * details.depth as f32)) diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 19aa554073918..52649e9007f76 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -44,10 +44,13 @@ const REMOVED_COLOR: Hsla = Hsla { // todo!(): Add updated status colors to theme pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement { match status { - GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)), + GitFileStatus::Added | GitFileStatus::Untracked => { + Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)) + } GitFileStatus::Modified => { Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR)) } GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)), + GitFileStatus::Deleted => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)), } } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 7c37ef481c440..97f8a8e3d4ec7 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -561,9 +561,15 @@ impl LocalBufferStore { buffer_change_sets .into_iter() .filter_map(|(change_set, buffer_snapshot, path)| { - let repo = snapshot.repo_for_path(&path)?; - let relative_path = repo_entry.relativize(&snapshot, &path).ok()?; - let base_text = local_repo_entry.repo().load_index_text(&relative_path); + let repo_fields = snapshot.repo_for_path(&path)?; + let relative_path = repo_fields + .repository_entry + .relativize(&snapshot, &path) + .ok()?; + let base_text = repo_fields + .local_entry + .repo() + .load_index_text(&relative_path); Some((change_set, buffer_snapshot, base_text)) }) .collect::>() @@ -1153,16 +1159,17 @@ impl BufferStore { Worktree::Local(worktree) => { let worktree = worktree.snapshot(); let blame_params = maybe!({ - let (repo_entry, local_repo_entry) = match worktree.repo_for_path(&file.path) { + let repo_fields = match worktree.repo_for_path(&file.path) { Some(repo_for_path) => repo_for_path, None => return Ok(None), }; - let relative_path = repo_entry + let relative_path = repo_fields + .repository_entry .relativize(&worktree, &file.path) .context("failed to relativize buffer path")?; - let repo = local_repo_entry.repo().clone(); + let repo = repo_fields.local_entry.repo().clone(); let content = match version { Some(version) => buffer.rope_for_version(&version).clone(), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8522404da2586..6d05b02dc233e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3097,6 +3097,7 @@ impl LspStore { WorktreeStoreEvent::WorktreeUpdateSent(worktree) => { worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree)); } + WorktreeStoreEvent::GitRepositoryUpdated => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 87c3ed1a7c7c6..06151fca0df54 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -243,6 +243,7 @@ pub enum Event { ActivateProjectPanel, WorktreeAdded(WorktreeId), WorktreeOrderChanged, + GitRepositoryUpdated, WorktreeRemoved(WorktreeId), WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet), WorktreeUpdatedGitRepositories(WorktreeId), @@ -2300,6 +2301,7 @@ impl Project { } WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged), WorktreeStoreEvent::WorktreeUpdateSent(_) => {} + WorktreeStoreEvent::GitRepositoryUpdated => cx.emit(Event::GitRepositoryUpdated), } } @@ -3550,17 +3552,6 @@ impl Project { ) } - pub fn get_repo( - &self, - project_path: &ProjectPath, - cx: &AppContext, - ) -> Option> { - self.worktree_for_id(project_path.worktree_id, cx)? - .read(cx) - .as_local()? - .local_git_repo(&project_path.path) - } - pub fn get_first_worktree_root_repo(&self, cx: &AppContext) -> Option> { let worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?; let root_entry = worktree.root_git_entry()?; diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index c39b88cd40f40..9fce4d90c9e28 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -62,6 +62,7 @@ pub enum WorktreeStoreEvent { WorktreeReleased(EntityId, WorktreeId), WorktreeOrderChanged, WorktreeUpdateSent(Model), + GitRepositoryUpdated, } impl EventEmitter for WorktreeStore {} @@ -322,6 +323,7 @@ impl WorktreeStore { let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await; let worktree = worktree?; + this.update(&mut cx, |this, cx| this.add(&worktree, cx))?; if visible { @@ -374,6 +376,17 @@ impl WorktreeStore { this.send_project_updates(cx); }) .detach(); + + cx.subscribe( + worktree, + |this, _, event: &worktree::Event, cx| match event { + worktree::Event::UpdatedGitRepositories(_) => { + cx.emit(WorktreeStoreEvent::GitRepositoryUpdated); + } + worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedEntries(_) => {} + }, + ) + .detach(); } pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 39285ad7ec223..b405d7083df4c 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -17,7 +17,7 @@ use gpui::{ Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; -use project::{Project, GitRepository}; +use project::Project; use rpc::proto; use settings::Settings as _; use smallvec::SmallVec; @@ -432,7 +432,7 @@ impl TitleBar { let workspace = self.workspace.upgrade()?; let branch_name = entry .as_ref() - .and_then(GitRepository::branch) + .and_then(|entry| entry.branch()) .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; Some( Button::new("project_branch_trigger", branch_name) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 4a4ed3405dd3a..3623b97aa8a9f 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2229,11 +2229,10 @@ impl Snapshot { .map(|repo| repo.git_entries_by_path.iter().cloned().collect()) } - #[cfg(any(test, feature = "test-support"))] - pub fn status_for_file(&self, path: impl Into) -> Option { - let path = path.into(); - self.repository_for_path(&path).and_then(|repo| { - let repo_path = repo.relativize(self, &path).unwrap(); + pub fn status_for_file(&self, path: impl AsRef) -> Option { + let path = path.as_ref(); + self.repository_for_path(path).and_then(|repo| { + let repo_path = repo.relativize(self, path).unwrap(); repo.git_entries_by_path .get(&PathKey(repo_path.0), &()) .map(|entry| entry.git_status) @@ -2625,14 +2624,14 @@ impl Snapshot { } // TODO: Bad name -> Bad code structure, refactor to remove this type -struct LocalRepositoryFields<'a> { - work_directory: RepositoryWorkDirectory, - repository_entry: RepositoryEntry, - local_entry: &'a LocalRepositoryEntry, +pub struct LocalRepositoryFields<'a> { + pub work_directory: RepositoryWorkDirectory, + pub repository_entry: RepositoryEntry, + pub local_entry: &'a LocalRepositoryEntry, } impl LocalSnapshot { - pub(crate) fn repo_for_path(&self, path: &Path) -> Option> { + pub fn repo_for_path(&self, path: &Path) -> Option> { let (work_directory, repository_entry) = self.repository_and_work_directory_for_path(path)?; let work_directory_id = repository_entry.work_directory_id(); @@ -3546,7 +3545,7 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; #[derive(Clone, Debug, PartialEq, Eq)] pub struct GitEntry { pub path: RepoPath, - git_status: GitFileStatus, + pub git_status: GitFileStatus, } #[derive(Clone, Debug)] @@ -3706,18 +3705,32 @@ impl<'a> GitEntryTraversalTarget<'a> { fn cmp_path(&self, other: &Path) -> std::cmp::Ordering { self.cmp( - &( - TraversalProgress { - max_path: other, - ..Default::default() - }, - Default::default(), - ), + &TraversalProgress { + max_path: other, + ..Default::default() + }, &(), ) } } +impl<'a, 'b> SeekTarget<'a, GitEntrySummary, TraversalProgress<'a>> + for GitEntryTraversalTarget<'b> +{ + fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { + match self { + GitEntryTraversalTarget::Path(path) => path.cmp(&cursor_location.max_path), + GitEntryTraversalTarget::PathSuccessor(path) => { + if cursor_location.max_path.starts_with(path) { + Ordering::Greater + } else { + Ordering::Equal + } + } + } + } +} + impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses)> for GitEntryTraversalTarget<'b> { From 942bedb8552ee135b4b5961d7a1c94e1570f381a Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 20 Dec 2024 19:38:21 -0500 Subject: [PATCH 16/22] Push entry changes out further Co-authored-by: Mikayla --- Cargo.lock | 1 + crates/editor/src/git/project_diff.rs | 31 +- crates/image_viewer/src/image_viewer.rs | 8 +- crates/outline_panel/Cargo.toml | 5 +- crates/outline_panel/src/outline_panel.rs | 1113 +++++++++++++-------- crates/project/src/project.rs | 20 +- crates/project/src/worktree_store.rs | 2 +- crates/project_panel/src/project_panel.rs | 95 +- crates/tab_switcher/src/tab_switcher.rs | 15 +- crates/worktree/src/worktree.rs | 55 + 10 files changed, 836 insertions(+), 509 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d92c2deabc07..afb272bb37185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8430,6 +8430,7 @@ dependencies = [ "editor", "file_icons", "fuzzy", + "git", "gpui", "itertools 0.13.0", "language", diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 7dc4e0d892816..5ae26da03db6b 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -19,7 +19,7 @@ use gpui::{ }; use language::{Buffer, BufferRow}; use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}; -use project::{Entry, Project, ProjectEntryId, ProjectPath, WorktreeId}; +use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; use text::{OffsetRangeExt, ToPoint}; use theme::ActiveTheme; use ui::prelude::*; @@ -194,14 +194,27 @@ impl ProjectDiffEditor { let open_tasks = project .update(&mut cx, |project, cx| { let worktree = project.worktree_for_id(id, cx)?; - let applicable_entries = worktree - .read(cx) - .entries(false, 0) - .filter(|entry| !entry.is_external) - .filter(|entry| entry.is_file()) - .filter_map(|entry| Some((entry.git_status?, entry))) - .filter_map(|(git_status, entry)| { - Some((git_status, entry.id, project.path_for_entry(entry.id, cx)?)) + let snapshot = worktree.read(cx).snapshot(); + let applicable_entries = snapshot + .repositories() + .filter_map(|(work_dir, _entry)| { + Some((work_dir, snapshot.git_status(work_dir)?)) + }) + .flat_map(|(work_dir, statuses)| { + statuses.into_iter().map(|git_entry| { + (git_entry.git_status, work_dir.join(git_entry.path)) + }) + }) + .filter_map(|(status, path)| { + let id = snapshot.entry_for_path(&path)?.id; + Some(( + status, + id, + ProjectPath { + worktree_id: snapshot.id(), + path: path.into(), + }, + )) }) .collect::>(); Some( diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 570948a82282f..b78f1bd085cae 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -96,12 +96,18 @@ impl Item for ImageView { fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { let project_path = self.image_item.read(cx).project_path(cx); + let label_color = if ItemSettings::get_global(cx).git_status { + let git_status = self + .project + .read(cx) + .project_path_git_status(&project_path, cx); + self.project .read(cx) .entry_for_path(&project_path, cx) .map(|entry| { - entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected) + entry_git_aware_label_color(git_status, entry.is_ignored, params.selected) }) .unwrap_or_else(|| params.text_color()) } else { diff --git a/crates/outline_panel/Cargo.toml b/crates/outline_panel/Cargo.toml index 6dfe1ceccc052..51e80d8fdfdf5 100644 --- a/crates/outline_panel/Cargo.toml +++ b/crates/outline_panel/Cargo.toml @@ -19,8 +19,9 @@ db.workspace = true editor.workspace = true file_icons.workspace = true fuzzy.workspace = true -itertools.workspace = true +git.workspace = true gpui.workspace = true +itertools.workspace = true language.workspace = true log.workspace = true menu.workspace = true @@ -36,8 +37,8 @@ smol.workspace = true theme.workspace = true ui.workspace = true util.workspace = true -worktree.workspace = true workspace.workspace = true +worktree.workspace = true [dev-dependencies] search = { workspace = true, features = ["test-support"] } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 586ae89a5bb05..359970a142f8c 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -22,6 +22,7 @@ use editor::{ }; use file_icons::FileIcons; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use git::repository::GitFileStatus; use gpui::{ actions, anchored, deferred, div, point, px, size, uniform_list, Action, AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, @@ -348,10 +349,18 @@ enum ExcerptOutlines { NotFetched, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct FoldedDirsEntry { + worktree_id: WorktreeId, + entries: Vec, + git_file_statuses: Vec>, +} + +// TODO: collapse the inner enums into panel entry #[derive(Clone, Debug)] enum PanelEntry { Fs(FsEntry), - FoldedDirs(WorktreeId, Vec), + FoldedDirs(FoldedDirsEntry), Outline(OutlineEntry), Search(SearchEntry), } @@ -383,7 +392,18 @@ impl PartialEq for PanelEntry { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Fs(a), Self::Fs(b)) => a == b, - (Self::FoldedDirs(a1, a2), Self::FoldedDirs(b1, b2)) => a1 == b1 && a2 == b2, + ( + Self::FoldedDirs(FoldedDirsEntry { + worktree_id: worktree_id_a, + entries: entries_a, + .. + }), + Self::FoldedDirs(FoldedDirsEntry { + worktree_id: worktree_id_b, + entries: entries_b, + .. + }), + ) => worktree_id_a == worktree_id_b && entries_a == entries_b, (Self::Outline(a), Self::Outline(b)) => a == b, ( Self::Search(SearchEntry { @@ -505,54 +525,126 @@ impl SearchData { } } -#[derive(Clone, Debug, PartialEq, Eq)] -enum OutlineEntry { - Excerpt(BufferId, ExcerptId, ExcerptRange), - Outline(BufferId, ExcerptId, Outline), +#[derive(Clone, Debug, Eq)] +struct OutlineEntryExcerpt { + id: ExcerptId, + buffer_id: BufferId, + range: ExcerptRange, +} + +impl PartialEq for OutlineEntryExcerpt { + fn eq(&self, other: &Self) -> bool { + self.buffer_id == other.buffer_id && self.id == other.id + } +} + +impl Hash for OutlineEntryExcerpt { + fn hash(&self, state: &mut H) { + (self.buffer_id, self.id).hash(state) + } } #[derive(Clone, Debug, Eq)] -enum FsEntry { - ExternalFile(BufferId, Vec), - Directory(WorktreeId, Entry), - File(WorktreeId, Entry, BufferId, Vec), +struct OutlineEntryOutline { + buffer_id: BufferId, + excerpt_id: ExcerptId, + outline: Outline, } -impl PartialEq for FsEntry { +impl PartialEq for OutlineEntryOutline { fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b, - (Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => { - id_a == id_b && entry_a.id == entry_b.id - } - ( - Self::File(worktree_a, entry_a, id_a, ..), - Self::File(worktree_b, entry_b, id_b, ..), - ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b, - _ => false, - } + self.buffer_id == other.buffer_id && self.excerpt_id == other.excerpt_id } } -impl Hash for FsEntry { +impl Hash for OutlineEntryOutline { fn hash(&self, state: &mut H) { + (self.buffer_id, self.excerpt_id).hash(state); + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum OutlineEntry { + Excerpt(OutlineEntryExcerpt), + Outline(OutlineEntryOutline), +} + +impl OutlineEntry { + fn ids(&self) -> (BufferId, ExcerptId) { match self { - Self::ExternalFile(buffer_id, _) => { - buffer_id.hash(state); - } - Self::Directory(worktree_id, entry) => { - worktree_id.hash(state); - entry.id.hash(state); - } - Self::File(worktree_id, entry, buffer_id, _) => { - worktree_id.hash(state); - entry.id.hash(state); - buffer_id.hash(state); - } + OutlineEntry::Excerpt(excerpt) => (excerpt.buffer_id, excerpt.id), + OutlineEntry::Outline(outline) => (outline.buffer_id, outline.excerpt_id), } } } +#[derive(Debug, Clone, Eq)] +struct FsEntryFile { + worktree_id: WorktreeId, + entry: Entry, + buffer_id: BufferId, + excerpts: Vec, + git_status: Option, +} + +impl PartialEq for FsEntryFile { + fn eq(&self, other: &Self) -> bool { + self.worktree_id == other.worktree_id + && self.entry.id == other.entry.id + && self.buffer_id == other.buffer_id + } +} + +impl Hash for FsEntryFile { + fn hash(&self, state: &mut H) { + (self.buffer_id, self.entry.id, self.worktree_id).hash(state); + } +} + +#[derive(Debug, Clone, Eq)] +struct FsEntryDirectory { + worktree_id: WorktreeId, + entry: Entry, + git_status: Option, +} + +impl PartialEq for FsEntryDirectory { + fn eq(&self, other: &Self) -> bool { + self.worktree_id == other.worktree_id && self.entry.id == other.entry.id + } +} + +impl Hash for FsEntryDirectory { + fn hash(&self, state: &mut H) { + (self.worktree_id, self.entry.id).hash(state); + } +} + +#[derive(Debug, Clone, Eq)] +struct FsEntryExternalFile { + buffer_id: BufferId, + excerpts: Vec, +} + +impl PartialEq for FsEntryExternalFile { + fn eq(&self, other: &Self) -> bool { + self.buffer_id == other.buffer_id + } +} + +impl Hash for FsEntryExternalFile { + fn hash(&self, state: &mut H) { + self.buffer_id.hash(state); + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum FsEntry { + ExternalFile(FsEntryExternalFile), + Directory(FsEntryDirectory), + File(FsEntryFile), +} + struct ActiveItem { item_handle: Box, active_editor: WeakView, @@ -775,7 +867,12 @@ impl OutlinePanel { } fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext) { - if let Some(PanelEntry::FoldedDirs(worktree_id, entries)) = self.selected_entry().cloned() { + if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id, + entries, + .. + })) = self.selected_entry().cloned() + { self.unfolded_dirs .entry(worktree_id) .or_default() @@ -786,11 +883,11 @@ impl OutlinePanel { fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext) { let (worktree_id, entry) = match self.selected_entry().cloned() { - Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, entry))) => { - (worktree_id, Some(entry)) + Some(PanelEntry::Fs(FsEntry::Directory(directory))) => { + (directory.worktree_id, Some(directory.entry)) } - Some(PanelEntry::FoldedDirs(worktree_id, entries)) => { - (worktree_id, entries.last().cloned()) + Some(PanelEntry::FoldedDirs(folded_dirs)) => { + (folded_dirs.worktree_id, folded_dirs.entries.last().cloned()) } _ => return, }; @@ -875,12 +972,12 @@ impl OutlinePanel { let mut scroll_to_buffer = None; let scroll_target = match entry { PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None, - PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { + PanelEntry::Fs(FsEntry::ExternalFile(file)) => { change_selection = false; - scroll_to_buffer = Some(*buffer_id); + scroll_to_buffer = Some(file.buffer_id); multi_buffer_snapshot.excerpts().find_map( |(excerpt_id, buffer_snapshot, excerpt_range)| { - if &buffer_snapshot.remote_id() == buffer_id { + if buffer_snapshot.remote_id() == file.buffer_id { multi_buffer_snapshot .anchor_in_excerpt(excerpt_id, excerpt_range.context.start) } else { @@ -889,13 +986,14 @@ impl OutlinePanel { }, ) } - PanelEntry::Fs(FsEntry::File(_, file_entry, buffer_id, _)) => { + + PanelEntry::Fs(FsEntry::File(file)) => { change_selection = false; - scroll_to_buffer = Some(*buffer_id); + scroll_to_buffer = Some(file.buffer_id); self.project .update(cx, |project, cx| { project - .path_for_entry(file_entry.id, cx) + .path_for_entry(file.entry.id, cx) .and_then(|path| project.get_open_buffer(&path, cx)) }) .map(|buffer| { @@ -909,18 +1007,17 @@ impl OutlinePanel { .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start) }) } - PanelEntry::Outline(OutlineEntry::Outline(_, excerpt_id, outline)) => { - multi_buffer_snapshot - .anchor_in_excerpt(*excerpt_id, outline.range.start) - .or_else(|| { - multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end) - }) - } - PanelEntry::Outline(OutlineEntry::Excerpt(_, excerpt_id, excerpt_range)) => { + PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot + .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.start) + .or_else(|| { + multi_buffer_snapshot + .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.end) + }), + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { change_selection = false; - multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, excerpt_range.context.start) + multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start) } - PanelEntry::Search(SearchEntry { match_range, .. }) => Some(match_range.start), + PanelEntry::Search(search_entry) => Some(search_entry.match_range.start), }; if let Some(anchor) = scroll_target { @@ -960,8 +1057,10 @@ impl OutlinePanel { .iter() .rev() .filter_map(|entry| match entry { - FsEntry::File(_, _, buffer_id, _) - | FsEntry::ExternalFile(buffer_id, _) => Some(*buffer_id), + FsEntry::File(file) => Some(file.buffer_id), + FsEntry::ExternalFile(external_file) => { + Some(external_file.buffer_id) + } FsEntry::Directory(..) => None, }) .skip_while(|id| *id != buffer_id) @@ -1044,69 +1143,68 @@ impl OutlinePanel { match &selected_entry { PanelEntry::Fs(fs_entry) => match fs_entry { FsEntry::ExternalFile(..) => None, - FsEntry::File(worktree_id, entry, ..) - | FsEntry::Directory(worktree_id, entry) => { - entry.path.parent().and_then(|parent_path| { - previous_entries.find(|entry| match entry { - PanelEntry::Fs(FsEntry::Directory(dir_worktree_id, dir_entry)) => { - dir_worktree_id == worktree_id - && dir_entry.path.as_ref() == parent_path - } - PanelEntry::FoldedDirs(dirs_worktree_id, dirs) => { - dirs_worktree_id == worktree_id - && dirs - .last() - .map_or(false, |dir| dir.path.as_ref() == parent_path) - } - _ => false, - }) + FsEntry::File(FsEntryFile { + worktree_id, entry, .. + }) + | FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + }) => entry.path.parent().and_then(|parent_path| { + previous_entries.find(|entry| match entry { + PanelEntry::Fs(FsEntry::Directory(directory)) => { + directory.worktree_id == *worktree_id + && directory.entry.path.as_ref() == parent_path + } + PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id: dirs_worktree_id, + entries: dirs, + .. + }) => { + dirs_worktree_id == worktree_id + && dirs + .last() + .map_or(false, |dir| dir.path.as_ref() == parent_path) + } + _ => false, }) - } + }), }, - PanelEntry::FoldedDirs(worktree_id, entries) => entries + PanelEntry::FoldedDirs(folded_dirs) => folded_dirs + .entries .first() .and_then(|entry| entry.path.parent()) .and_then(|parent_path| { previous_entries.find(|entry| { - if let PanelEntry::Fs(FsEntry::Directory(dir_worktree_id, dir_entry)) = - entry - { - dir_worktree_id == worktree_id - && dir_entry.path.as_ref() == parent_path + if let PanelEntry::Fs(FsEntry::Directory(directory)) = entry { + directory.worktree_id == folded_dirs.worktree_id + && directory.entry.path.as_ref() == parent_path } else { false } }) }), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt_buffer_id, excerpt_id, _)) => { + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { previous_entries.find(|entry| match entry { - PanelEntry::Fs(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => { - file_buffer_id == excerpt_buffer_id - && file_excerpts.contains(excerpt_id) + PanelEntry::Fs(FsEntry::File(file)) => { + file.buffer_id == excerpt.buffer_id + && file.excerpts.contains(&excerpt.id) } - PanelEntry::Fs(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => { - file_buffer_id == excerpt_buffer_id - && file_excerpts.contains(excerpt_id) + PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { + external_file.buffer_id == excerpt.buffer_id + && external_file.excerpts.contains(&excerpt.id) } _ => false, }) } - PanelEntry::Outline(OutlineEntry::Outline( - outline_buffer_id, - outline_excerpt_id, - _, - )) => previous_entries.find(|entry| { - if let PanelEntry::Outline(OutlineEntry::Excerpt( - excerpt_buffer_id, - excerpt_id, - _, - )) = entry - { - outline_buffer_id == excerpt_buffer_id && outline_excerpt_id == excerpt_id - } else { - false - } - }), + PanelEntry::Outline(OutlineEntry::Outline(outline)) => { + previous_entries.find(|entry| { + if let PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) = entry { + outline.buffer_id == excerpt.buffer_id + && outline.excerpt_id == excerpt.id + } else { + false + } + }) + } PanelEntry::Search(_) => { previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_))) } @@ -1164,8 +1262,12 @@ impl OutlinePanel { ) { self.select_entry(entry.clone(), true, cx); let is_root = match &entry { - PanelEntry::Fs(FsEntry::File(worktree_id, entry, ..)) - | PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self + PanelEntry::Fs(FsEntry::File(FsEntryFile { + worktree_id, entry, .. + })) + | PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + })) => self .project .read(cx) .worktree_for_id(*worktree_id, cx) @@ -1173,7 +1275,11 @@ impl OutlinePanel { worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) }) .unwrap_or(false), - PanelEntry::FoldedDirs(worktree_id, entries) => entries + PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id, + entries, + .. + }) => entries .first() .and_then(|entry| { self.project @@ -1232,9 +1338,11 @@ impl OutlinePanel { fn is_foldable(&self, entry: &PanelEntry) -> bool { let (directory_worktree, directory_entry) = match entry { - PanelEntry::Fs(FsEntry::Directory(directory_worktree, directory_entry)) => { - (*directory_worktree, Some(directory_entry)) - } + PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, + entry: directory_entry, + .. + })) => (*worktree_id, Some(directory_entry)), _ => return false, }; let Some(directory_entry) = directory_entry else { @@ -1270,24 +1378,34 @@ impl OutlinePanel { }; let mut buffers_to_unfold = HashSet::default(); let entry_to_expand = match &selected_entry { - PanelEntry::FoldedDirs(worktree_id, dir_entries) => dir_entries.last().map(|entry| { + PanelEntry::FoldedDirs(FoldedDirsEntry { + entries: dir_entries, + worktree_id, + .. + }) => dir_entries.last().map(|entry| { buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry)); CollapsedEntry::Dir(*worktree_id, entry.id) }), - PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => { - buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, dir_entry)); - Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id)) + PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + })) => { + buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry)); + Some(CollapsedEntry::Dir(*worktree_id, entry.id)) } - PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { + PanelEntry::Fs(FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + })) => { buffers_to_unfold.insert(*buffer_id); Some(CollapsedEntry::File(*worktree_id, *buffer_id)) } - PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { - buffers_to_unfold.insert(*buffer_id); - Some(CollapsedEntry::ExternalFile(*buffer_id)) + PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { + buffers_to_unfold.insert(external_file.buffer_id); + Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) } - PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => { - Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { + Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) } PanelEntry::Search(_) | PanelEntry::Outline(..) => return, }; @@ -1330,19 +1448,24 @@ impl OutlinePanel { let mut buffers_to_fold = HashSet::default(); let collapsed = match &selected_entry { - PanelEntry::Fs(FsEntry::Directory(worktree_id, selected_dir_entry)) => { + PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + })) => { if self .collapsed_entries - .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id)) + .insert(CollapsedEntry::Dir(*worktree_id, entry.id)) { - buffers_to_fold - .extend(self.buffers_inside_directory(*worktree_id, selected_dir_entry)); + buffers_to_fold.extend(self.buffers_inside_directory(*worktree_id, entry)); true } else { false } } - PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { + PanelEntry::Fs(FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + })) => { if self .collapsed_entries .insert(CollapsedEntry::File(*worktree_id, *buffer_id)) @@ -1353,34 +1476,35 @@ impl OutlinePanel { false } } - PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { + PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { if self .collapsed_entries - .insert(CollapsedEntry::ExternalFile(*buffer_id)) + .insert(CollapsedEntry::ExternalFile(external_file.buffer_id)) { - buffers_to_fold.insert(*buffer_id); + buffers_to_fold.insert(external_file.buffer_id); true } else { false } } - PanelEntry::FoldedDirs(worktree_id, dir_entries) => { + PanelEntry::FoldedDirs(folded_dirs) => { let mut folded = false; - if let Some(dir_entry) = dir_entries.last() { + if let Some(dir_entry) = folded_dirs.entries.last() { if self .collapsed_entries - .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id)) + .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id)) { folded = true; - buffers_to_fold - .extend(self.buffers_inside_directory(*worktree_id, dir_entry)); + buffers_to_fold.extend( + self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry), + ); } } folded } - PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => self + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self .collapsed_entries - .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)), + .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)), PanelEntry::Search(_) | PanelEntry::Outline(..) => false, }; @@ -1409,31 +1533,42 @@ impl OutlinePanel { .iter() .fold(HashSet::default(), |mut entries, fs_entry| { match fs_entry { - FsEntry::ExternalFile(buffer_id, _) => { - buffers_to_unfold.insert(*buffer_id); - entries.insert(CollapsedEntry::ExternalFile(*buffer_id)); - entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map( - |excerpts| { - excerpts.iter().map(|(excerpt_id, _)| { - CollapsedEntry::Excerpt(*buffer_id, *excerpt_id) - }) - }, - )); - } - FsEntry::Directory(worktree_id, entry) => { - entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id)); + FsEntry::ExternalFile(external_file) => { + buffers_to_unfold.insert(external_file.buffer_id); + entries.insert(CollapsedEntry::ExternalFile(external_file.buffer_id)); + entries.extend( + self.excerpts + .get(&external_file.buffer_id) + .into_iter() + .flat_map(|excerpts| { + excerpts.iter().map(|(excerpt_id, _)| { + CollapsedEntry::Excerpt( + external_file.buffer_id, + *excerpt_id, + ) + }) + }), + ); } - FsEntry::File(worktree_id, _, buffer_id, _) => { - buffers_to_unfold.insert(*buffer_id); - entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id)); - entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map( - |excerpts| { - excerpts.iter().map(|(excerpt_id, _)| { - CollapsedEntry::Excerpt(*buffer_id, *excerpt_id) - }) - }, + FsEntry::Directory(directory) => { + entries.insert(CollapsedEntry::Dir( + directory.worktree_id, + directory.entry.id, )); } + FsEntry::File(file) => { + buffers_to_unfold.insert(file.buffer_id); + entries.insert(CollapsedEntry::File(file.worktree_id, file.buffer_id)); + entries.extend( + self.excerpts.get(&file.buffer_id).into_iter().flat_map( + |excerpts| { + excerpts.iter().map(|(excerpt_id, _)| { + CollapsedEntry::Excerpt(file.buffer_id, *excerpt_id) + }) + }, + ), + ); + } }; entries }); @@ -1459,22 +1594,28 @@ impl OutlinePanel { .cached_entries .iter() .flat_map(|cached_entry| match &cached_entry.entry { - PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => { - Some(CollapsedEntry::Dir(*worktree_id, entry.id)) - } - PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { + PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)), + PanelEntry::Fs(FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + })) => { buffers_to_fold.insert(*buffer_id); Some(CollapsedEntry::File(*worktree_id, *buffer_id)) } - PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { - buffers_to_fold.insert(*buffer_id); - Some(CollapsedEntry::ExternalFile(*buffer_id)) - } - PanelEntry::FoldedDirs(worktree_id, entries) => { - Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)) + PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { + buffers_to_fold.insert(external_file.buffer_id); + Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) } - PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => { - Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) + PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id, + entries, + .. + }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)), + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { + Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) } PanelEntry::Search(_) | PanelEntry::Outline(..) => None, }) @@ -1498,7 +1639,11 @@ impl OutlinePanel { let mut fold = false; let mut buffers_to_toggle = HashSet::default(); match entry { - PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => { + PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, + entry: dir_entry, + .. + })) => { let entry_id = dir_entry.id; let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry)); @@ -1514,7 +1659,11 @@ impl OutlinePanel { fold = true; } } - PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { + PanelEntry::Fs(FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + })) => { let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id); buffers_to_toggle.insert(*buffer_id); if !self.collapsed_entries.remove(&collapsed_entry) { @@ -1522,15 +1671,19 @@ impl OutlinePanel { fold = true; } } - PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { - let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id); - buffers_to_toggle.insert(*buffer_id); + PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { + let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id); + buffers_to_toggle.insert(external_file.buffer_id); if !self.collapsed_entries.remove(&collapsed_entry) { self.collapsed_entries.insert(collapsed_entry); fold = true; } } - PanelEntry::FoldedDirs(worktree_id, dir_entries) => { + PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id, + entries: dir_entries, + .. + }) => { if let Some(dir_entry) = dir_entries.first() { let entry_id = dir_entry.id; let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); @@ -1549,8 +1702,8 @@ impl OutlinePanel { } } } - PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => { - let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id); + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { + let collapsed_entry = CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id); if !self.collapsed_entries.remove(&collapsed_entry) { self.collapsed_entries.insert(collapsed_entry); } @@ -1625,7 +1778,9 @@ impl OutlinePanel { .selected_entry() .and_then(|entry| match entry { PanelEntry::Fs(entry) => self.relative_path(entry, cx), - PanelEntry::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()), + PanelEntry::FoldedDirs(folded_dirs) => { + folded_dirs.entries.last().map(|entry| entry.path.clone()) + } PanelEntry::Search(_) | PanelEntry::Outline(..) => None, }) .map(|p| p.to_string_lossy().to_string()) @@ -1679,23 +1834,24 @@ impl OutlinePanel { return Ok(()); }; let related_buffer_entry = match &entry_with_selection { - PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { - project.update(&mut cx, |project, cx| { - let entry_id = project - .buffer_for_id(*buffer_id, cx) - .and_then(|buffer| buffer.read(cx).entry_id(cx)); - project - .worktree_for_id(*worktree_id, cx) - .zip(entry_id) - .and_then(|(worktree, entry_id)| { - let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); - Some((worktree, entry)) - }) - })? - } + PanelEntry::Fs(FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + })) => project.update(&mut cx, |project, cx| { + let entry_id = project + .buffer_for_id(*buffer_id, cx) + .and_then(|buffer| buffer.read(cx).entry_id(cx)); + project + .worktree_for_id(*worktree_id, cx) + .zip(entry_id) + .and_then(|(worktree, entry_id)| { + let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); + Some((worktree, entry)) + }) + })?, PanelEntry::Outline(outline_entry) => { - let &(OutlineEntry::Outline(buffer_id, excerpt_id, _) - | OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) = outline_entry; + let (buffer_id, excerpt_id) = outline_entry.ids(); outline_panel.update(&mut cx, |outline_panel, cx| { outline_panel .collapsed_entries @@ -1808,25 +1964,21 @@ impl OutlinePanel { fn render_excerpt( &self, - buffer_id: BufferId, - excerpt_id: ExcerptId, - range: &ExcerptRange, + excerpt: &OutlineEntryExcerpt, depth: usize, cx: &mut ViewContext, ) -> Option> { - let item_id = ElementId::from(excerpt_id.to_proto() as usize); + let item_id = ElementId::from(excerpt.id.to_proto() as usize); let is_active = match self.selected_entry() { - Some(PanelEntry::Outline(OutlineEntry::Excerpt( - selected_buffer_id, - selected_excerpt_id, - _, - ))) => selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id, + Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => { + selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id + } _ => false, }; let has_outlines = self .excerpts - .get(&buffer_id) - .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines { + .get(&excerpt.buffer_id) + .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines { ExcerptOutlines::Outlines(outlines) => Some(outlines), ExcerptOutlines::Invalidated(outlines) => Some(outlines), ExcerptOutlines::NotFetched => None, @@ -1834,7 +1986,7 @@ impl OutlinePanel { .map_or(false, |outlines| !outlines.is_empty()); let is_expanded = !self .collapsed_entries - .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)); + .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)); let color = entry_git_aware_label_color(None, false, is_active); let icon = if has_outlines { FileIcons::get_chevron_icon(is_expanded, cx) @@ -1844,14 +1996,14 @@ impl OutlinePanel { } .unwrap_or_else(empty_icon); - let label = self.excerpt_label(buffer_id, range, cx)?; + let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?; let label_element = Label::new(label) .single_line() .color(color) .into_any_element(); Some(self.entry_element( - PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, range.clone())), + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())), item_id, depth, Some(icon), @@ -1878,50 +2030,40 @@ impl OutlinePanel { fn render_outline( &self, - buffer_id: BufferId, - excerpt_id: ExcerptId, - rendered_outline: &Outline, + outline: &OutlineEntryOutline, depth: usize, string_match: Option<&StringMatch>, cx: &mut ViewContext, ) -> Stateful
{ - let (item_id, label_element) = ( - ElementId::from(SharedString::from(format!( - "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}", - rendered_outline.range, &rendered_outline.text, - ))), - outline::render_item( - rendered_outline, - string_match - .map(|string_match| string_match.ranges().collect::>()) - .unwrap_or_default(), - cx, - ) - .into_any_element(), - ); + let item_id = ElementId::from(SharedString::from(format!( + "{:?}|{:?}{:?}|{:?}", + outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text, + ))); + + let label_element = outline::render_item( + &outline.outline, + string_match + .map(|string_match| string_match.ranges().collect::>()) + .unwrap_or_default(), + cx, + ) + .into_any_element(); + let is_active = match self.selected_entry() { - Some(PanelEntry::Outline(OutlineEntry::Outline( - selected_buffer_id, - selected_excerpt_id, - selected_entry, - ))) => { - selected_buffer_id == &buffer_id - && selected_excerpt_id == &excerpt_id - && selected_entry == rendered_outline + Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => { + outline == selected && outline.outline == selected.outline } _ => false, }; + let icon = if self.is_singleton_active(cx) { None } else { Some(empty_icon()) }; + self.entry_element( - PanelEntry::Outline(OutlineEntry::Outline( - buffer_id, - excerpt_id, - rendered_outline.clone(), - )), + PanelEntry::Outline(OutlineEntry::Outline(outline.clone())), item_id, depth, icon, @@ -1944,10 +2086,14 @@ impl OutlinePanel { _ => false, }; let (item_id, label_element, icon) = match rendered_entry { - FsEntry::File(worktree_id, entry, ..) => { + FsEntry::File(FsEntryFile { + worktree_id, + entry, + git_status, + .. + }) => { let name = self.entry_name(worktree_id, entry, cx); - let color = - entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active); + let color = entry_git_aware_label_color(*git_status, entry.is_ignored, is_active); let icon = if settings.file_icons { FileIcons::get_icon(&entry.path, cx) .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) @@ -1967,14 +2113,18 @@ impl OutlinePanel { icon.unwrap_or_else(empty_icon), ) } - FsEntry::Directory(worktree_id, entry) => { + FsEntry::Directory(FsEntryDirectory { + worktree_id, + + entry, + git_status, + }) => { let name = self.entry_name(worktree_id, entry, cx); let is_expanded = !self .collapsed_entries .contains(&CollapsedEntry::Dir(*worktree_id, entry.id)); - let color = - entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active); + let color = entry_git_aware_label_color(*git_status, entry.is_ignored, is_active); let icon = if settings.folder_icons { FileIcons::get_folder_icon(is_expanded, cx) } else { @@ -1995,9 +2145,9 @@ impl OutlinePanel { icon.unwrap_or_else(empty_icon), ) } - FsEntry::ExternalFile(buffer_id, _) => { + FsEntry::ExternalFile(external_file) => { let color = entry_label_color(is_active); - let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) { + let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) { Some(buffer_snapshot) => match buffer_snapshot.file() { Some(file) => { let path = file.path(); @@ -2015,7 +2165,7 @@ impl OutlinePanel { None => (None, "Unknown buffer".to_string()), }; ( - ElementId::from(buffer_id.to_proto() as usize), + ElementId::from(external_file.buffer_id.to_proto() as usize), HighlightedLabel::new( name, string_match @@ -2042,29 +2192,29 @@ impl OutlinePanel { fn render_folded_dirs( &self, - worktree_id: WorktreeId, - dir_entries: &[Entry], + folded_dir: &FoldedDirsEntry, depth: usize, string_match: Option<&StringMatch>, cx: &mut ViewContext, ) -> Stateful
{ let settings = OutlinePanelSettings::get_global(cx); let is_active = match self.selected_entry() { - Some(PanelEntry::FoldedDirs(selected_worktree_id, selected_entries)) => { - selected_worktree_id == &worktree_id && selected_entries == dir_entries + Some(PanelEntry::FoldedDirs(selected_dirs)) => { + selected_dirs.worktree_id == folded_dir.worktree_id + && selected_dirs.entries == folded_dir.entries } _ => false, }; let (item_id, label_element, icon) = { - let name = self.dir_names_string(dir_entries, worktree_id, cx); + let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx); - let is_expanded = dir_entries.iter().all(|dir| { + let is_expanded = folded_dir.entries.iter().all(|dir| { !self .collapsed_entries - .contains(&CollapsedEntry::Dir(worktree_id, dir.id)) + .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id)) }); - let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored); - let git_status = dir_entries.first().and_then(|entry| entry.git_status); + let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored); + let git_status = folded_dir.git_file_statuses.first().cloned().flatten(); let color = entry_git_aware_label_color(git_status, is_ignored, is_active); let icon = if settings.folder_icons { FileIcons::get_folder_icon(is_expanded, cx) @@ -2075,10 +2225,12 @@ impl OutlinePanel { .map(|icon| icon.color(color).into_any_element()); ( ElementId::from( - dir_entries + folded_dir + .entries .last() .map(|entry| entry.id.to_proto()) - .unwrap_or_else(|| worktree_id.to_proto()) as usize, + .unwrap_or_else(|| folded_dir.worktree_id.to_proto()) + as usize, ), HighlightedLabel::new( name, @@ -2093,7 +2245,7 @@ impl OutlinePanel { }; self.entry_element( - PanelEntry::FoldedDirs(worktree_id, dir_entries.to_vec()), + PanelEntry::FoldedDirs(folded_dir.clone()), item_id, depth, Some(icon), @@ -2484,14 +2636,15 @@ impl OutlinePanel { let mut entries = entries.into_values().collect::>(); // For a proper git status propagation, we have to keep the entries sorted lexicographically. entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref())); - worktree_snapshot.propagate_git_statuses(&mut entries); + let statuses = worktree_snapshot.propagate_git_statuses(&entries); + let entries = entries.into_iter().zip(statuses).collect::>(); (worktree_id, entries) }) .flat_map(|(worktree_id, entries)| { { entries .into_iter() - .filter_map(|entry| { + .filter_map(|(entry, git_status)| { if auto_fold_dirs { if let Some(parent) = entry.path.parent() { let children = new_children_count @@ -2508,19 +2661,24 @@ impl OutlinePanel { } if entry.is_dir() { - Some(FsEntry::Directory(worktree_id, entry)) + Some(FsEntry::Directory(FsEntryDirectory { + worktree_id, + entry, + git_status, + })) } else { let (buffer_id, excerpts) = worktree_excerpts .get_mut(&worktree_id) .and_then(|worktree_excerpts| { worktree_excerpts.remove(&entry.id) })?; - Some(FsEntry::File( + Some(FsEntry::File(FsEntryFile { worktree_id, - entry, buffer_id, + entry, excerpts, - )) + git_status, + })) } }) .collect::>() @@ -2533,25 +2691,29 @@ impl OutlinePanel { let new_visible_entries = external_excerpts .into_iter() .sorted_by_key(|(id, _)| *id) - .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts)) + .map(|(buffer_id, excerpts)| { + FsEntry::ExternalFile(FsEntryExternalFile { + buffer_id, + excerpts, + }) + }) .chain(worktree_entries) .filter(|visible_item| { match visible_item { - FsEntry::Directory(worktree_id, dir_entry) => { + FsEntry::Directory(directory) => { let parent_id = back_to_common_visited_parent( &mut visited_dirs, - worktree_id, - dir_entry, + &directory.worktree_id, + &directory.entry, ); - let depth = if root_entries.contains(&dir_entry.id) { - 0 - } else { + let mut depth = 0; + if !root_entries.contains(&directory.entry.id) { if auto_fold_dirs { let children = new_children_count - .get(worktree_id) + .get(&directory.worktree_id) .and_then(|children_count| { - children_count.get(&dir_entry.path) + children_count.get(&directory.entry.path) }) .copied() .unwrap_or_default(); @@ -2562,7 +2724,7 @@ impl OutlinePanel { .last() .map(|(parent_dir_id, _)| { new_unfolded_dirs - .get(worktree_id) + .get(&directory.worktree_id) .map_or(true, |unfolded_dirs| { unfolded_dirs .contains(parent_dir_id) @@ -2571,23 +2733,29 @@ impl OutlinePanel { .unwrap_or(true)) { new_unfolded_dirs - .entry(*worktree_id) + .entry(directory.worktree_id) .or_default() - .insert(dir_entry.id); + .insert(directory.entry.id); } } - parent_id + depth = parent_id .and_then(|(worktree_id, id)| { new_depth_map.get(&(worktree_id, id)).copied() }) .unwrap_or(0) - + 1 + + 1; }; - visited_dirs.push((dir_entry.id, dir_entry.path.clone())); - new_depth_map.insert((*worktree_id, dir_entry.id), depth); + visited_dirs + .push((directory.entry.id, directory.entry.path.clone())); + new_depth_map + .insert((directory.worktree_id, directory.entry.id), depth); } - FsEntry::File(worktree_id, file_entry, ..) => { + FsEntry::File(FsEntryFile { + worktree_id, + entry: file_entry, + .. + }) => { let parent_id = back_to_common_visited_parent( &mut visited_dirs, worktree_id, @@ -2718,8 +2886,10 @@ impl OutlinePanel { .iter() .find(|fs_entry| match fs_entry { FsEntry::Directory(..) => false, - FsEntry::File(_, _, file_buffer_id, _) - | FsEntry::ExternalFile(file_buffer_id, _) => *file_buffer_id == buffer_id, + FsEntry::File(FsEntryFile { buffer_id, .. }) + | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { + buffer_id == buffer_id + } }) .cloned() .map(PanelEntry::Fs); @@ -2869,26 +3039,31 @@ impl OutlinePanel { .cloned(); let closest_container = match outline_item { - Some(outline) => { - PanelEntry::Outline(OutlineEntry::Outline(buffer_id, excerpt_id, outline)) - } + Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline { + buffer_id, + excerpt_id, + outline, + })), None => { self.cached_entries.iter().rev().find_map(|cached_entry| { match &cached_entry.entry { - PanelEntry::Outline(OutlineEntry::Excerpt( - entry_buffer_id, - entry_excerpt_id, - _, - )) => { - if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id { + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { + if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id { Some(cached_entry.entry.clone()) } else { None } } PanelEntry::Fs( - FsEntry::ExternalFile(file_buffer_id, file_excerpts) - | FsEntry::File(_, _, file_buffer_id, file_excerpts), + FsEntry::ExternalFile(FsEntryExternalFile { + buffer_id: file_buffer_id, + excerpts: file_excerpts, + }) + | FsEntry::File(FsEntryFile { + buffer_id: file_buffer_id, + excerpts: file_excerpts, + .. + }), ) => { if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) { Some(cached_entry.entry.clone()) @@ -2987,8 +3162,15 @@ impl OutlinePanel { .iter() .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| { match fs_entry { - FsEntry::File(_, _, buffer_id, file_excerpts) - | FsEntry::ExternalFile(buffer_id, file_excerpts) => { + FsEntry::File(FsEntryFile { + buffer_id, + excerpts: file_excerpts, + .. + }) + | FsEntry::ExternalFile(FsEntryExternalFile { + buffer_id, + excerpts: file_excerpts, + }) => { let excerpts = self.excerpts.get(buffer_id); for &file_excerpt in file_excerpts { if let Some(excerpt) = excerpts @@ -3038,21 +3220,28 @@ impl OutlinePanel { fn abs_path(&self, entry: &PanelEntry, cx: &AppContext) -> Option { match entry { PanelEntry::Fs( - FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _), + FsEntry::File(FsEntryFile { buffer_id, .. }) + | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }), ) => self .buffer_snapshot_for_id(*buffer_id, cx) .and_then(|buffer_snapshot| { let file = File::from_dyn(buffer_snapshot.file())?; file.worktree.read(cx).absolutize(&file.path).ok() }), - PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self + PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + })) => self .project .read(cx) .worktree_for_id(*worktree_id, cx)? .read(cx) .absolutize(&entry.path) .ok(), - PanelEntry::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| { + PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id, + entries: dirs, + .. + }) => dirs.last().and_then(|entry| { self.project .read(cx) .worktree_for_id(*worktree_id, cx) @@ -3064,12 +3253,12 @@ impl OutlinePanel { fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option> { match entry { - FsEntry::ExternalFile(buffer_id, _) => { + FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?; Some(buffer_snapshot.file()?.path().clone()) } - FsEntry::Directory(_, entry) => Some(entry.path.clone()), - FsEntry::File(_, entry, ..) => Some(entry.path.clone()), + FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()), + FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()), } } @@ -3135,7 +3324,7 @@ impl OutlinePanel { let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| { let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; - let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec)>; + let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>; let track_matches = query.is_some(); #[derive(Debug)] @@ -3149,29 +3338,29 @@ impl OutlinePanel { for entry in outline_panel.fs_entries.clone() { let is_expanded = outline_panel.is_expanded(&entry); let (depth, should_add) = match &entry { - FsEntry::Directory(worktree_id, dir_entry) => { + FsEntry::Directory(directory_entry) => { let mut should_add = true; let is_root = project .read(cx) - .worktree_for_id(*worktree_id, cx) + .worktree_for_id(directory_entry.worktree_id, cx) .map_or(false, |worktree| { - worktree.read(cx).root_entry() == Some(dir_entry) + worktree.read(cx).root_entry() == Some(&directory_entry.entry) }); let folded = auto_fold_dirs && !is_root && outline_panel .unfolded_dirs - .get(worktree_id) + .get(&directory_entry.worktree_id) .map_or(true, |unfolded_dirs| { - !unfolded_dirs.contains(&dir_entry.id) + !unfolded_dirs.contains(&directory_entry.entry.id) }); let fs_depth = outline_panel .fs_entries_depth - .get(&(*worktree_id, dir_entry.id)) + .get(&(directory_entry.worktree_id, directory_entry.entry.id)) .copied() .unwrap_or(0); while let Some(parent) = parent_dirs.last() { - if dir_entry.path.starts_with(&parent.path) { + if directory_entry.entry.path.starts_with(&parent.path) { break; } parent_dirs.pop(); @@ -3179,11 +3368,14 @@ impl OutlinePanel { let auto_fold = match parent_dirs.last() { Some(parent) => { parent.folded - && Some(parent.path.as_ref()) == dir_entry.path.parent() + && Some(parent.path.as_ref()) + == directory_entry.entry.path.parent() && outline_panel .fs_children_count - .get(worktree_id) - .and_then(|entries| entries.get(&dir_entry.path)) + .get(&directory_entry.worktree_id) + .and_then(|entries| { + entries.get(&directory_entry.entry.path) + }) .copied() .unwrap_or_default() .may_be_fold_part() @@ -3201,7 +3393,7 @@ impl OutlinePanel { parent.depth + 1 }; parent_dirs.push(ParentStats { - path: dir_entry.path.clone(), + path: directory_entry.entry.path.clone(), folded, expanded: parent_expanded && is_expanded, depth: new_depth, @@ -3210,7 +3402,7 @@ impl OutlinePanel { } None => { parent_dirs.push(ParentStats { - path: dir_entry.path.clone(), + path: directory_entry.entry.path.clone(), folded, expanded: is_expanded, depth: fs_depth, @@ -3219,37 +3411,38 @@ impl OutlinePanel { } }; - if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) = - folded_dirs_entry.take() + if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take() { if folded - && worktree_id == &folded_worktree_id - && dir_entry.path.parent() - == folded_dirs.last().map(|entry| entry.path.as_ref()) + && directory_entry.worktree_id == folded_dirs.worktree_id + && directory_entry.entry.path.parent() + == folded_dirs + .entries + .last() + .map(|entry| entry.path.as_ref()) { - folded_dirs.push(dir_entry.clone()); - folded_dirs_entry = - Some((folded_depth, folded_worktree_id, folded_dirs)) + folded_dirs.entries.push(directory_entry.entry.clone()); + folded_dirs_entry = Some((folded_depth, folded_dirs)) } else { if !is_singleton { let start_of_collapsed_dir_sequence = !parent_expanded && parent_dirs .iter() .rev() - .nth(folded_dirs.len() + 1) + .nth(folded_dirs.entries.len() + 1) .map_or(true, |parent| parent.expanded); if start_of_collapsed_dir_sequence || parent_expanded || query.is_some() { if parent_folded { - folded_dirs.push(dir_entry.clone()); + folded_dirs + .entries + .push(directory_entry.entry.clone()); should_add = false; } - let new_folded_dirs = PanelEntry::FoldedDirs( - folded_worktree_id, - folded_dirs, - ); + let new_folded_dirs = + PanelEntry::FoldedDirs(folded_dirs.clone()); outline_panel.push_entry( &mut generation_state, track_matches, @@ -3263,12 +3456,25 @@ impl OutlinePanel { folded_dirs_entry = if parent_folded { None } else { - Some((depth, *worktree_id, vec![dir_entry.clone()])) + Some(( + depth, + FoldedDirsEntry { + worktree_id: directory_entry.worktree_id, + entries: vec![directory_entry.entry.clone()], + git_file_statuses: vec![directory_entry.git_status], + }, + )) }; } } else if folded { - folded_dirs_entry = - Some((depth, *worktree_id, vec![dir_entry.clone()])); + folded_dirs_entry = Some(( + depth, + FoldedDirsEntry { + worktree_id: directory_entry.worktree_id, + entries: vec![directory_entry.entry.clone()], + git_file_statuses: vec![directory_entry.git_status], + }, + )); } let should_add = @@ -3276,21 +3482,22 @@ impl OutlinePanel { (depth, should_add) } FsEntry::ExternalFile(..) => { - if let Some((folded_depth, worktree_id, folded_dirs)) = - folded_dirs_entry.take() - { + if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() { let parent_expanded = parent_dirs .iter() .rev() .find(|parent| { - folded_dirs.iter().all(|entry| entry.path != parent.path) + folded_dir + .entries + .iter() + .all(|entry| entry.path != parent.path) }) .map_or(true, |parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, track_matches, - PanelEntry::FoldedDirs(worktree_id, folded_dirs), + PanelEntry::FoldedDirs(folded_dir), folded_depth, cx, ); @@ -3299,22 +3506,23 @@ impl OutlinePanel { parent_dirs.clear(); (0, true) } - FsEntry::File(worktree_id, file_entry, ..) => { - if let Some((folded_depth, worktree_id, folded_dirs)) = - folded_dirs_entry.take() - { + FsEntry::File(file) => { + if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() { let parent_expanded = parent_dirs .iter() .rev() .find(|parent| { - folded_dirs.iter().all(|entry| entry.path != parent.path) + folded_dirs + .entries + .iter() + .all(|entry| entry.path != parent.path) }) .map_or(true, |parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, track_matches, - PanelEntry::FoldedDirs(worktree_id, folded_dirs), + PanelEntry::FoldedDirs(folded_dirs), folded_depth, cx, ); @@ -3323,23 +3531,22 @@ impl OutlinePanel { let fs_depth = outline_panel .fs_entries_depth - .get(&(*worktree_id, file_entry.id)) + .get(&(file.worktree_id, file.entry.id)) .copied() .unwrap_or(0); while let Some(parent) = parent_dirs.last() { - if file_entry.path.starts_with(&parent.path) { + if file.entry.path.starts_with(&parent.path) { break; } parent_dirs.pop(); } - let (depth, should_add) = match parent_dirs.last() { + match parent_dirs.last() { Some(parent) => { let new_depth = parent.depth + 1; (new_depth, parent.expanded) } None => (fs_depth, true), - }; - (depth, should_add) + } } }; @@ -3373,12 +3580,16 @@ impl OutlinePanel { let excerpts_to_consider = if is_singleton || query.is_some() || (should_add && is_expanded) { match &entry { - FsEntry::File(_, _, buffer_id, entry_excerpts) => { - Some((*buffer_id, entry_excerpts)) - } - FsEntry::ExternalFile(buffer_id, entry_excerpts) => { - Some((*buffer_id, entry_excerpts)) - } + FsEntry::File(FsEntryFile { + buffer_id, + excerpts, + .. + }) + | FsEntry::ExternalFile(FsEntryExternalFile { + buffer_id, + excerpts, + .. + }) => Some((*buffer_id, excerpts)), _ => None, } } else { @@ -3417,17 +3628,22 @@ impl OutlinePanel { } } - if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() { + if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() { let parent_expanded = parent_dirs .iter() .rev() - .find(|parent| folded_dirs.iter().all(|entry| entry.path != parent.path)) + .find(|parent| { + folded_dirs + .entries + .iter() + .all(|entry| entry.path != parent.path) + }) .map_or(true, |parent| parent.expanded); if parent_expanded || query.is_some() { outline_panel.push_entry( &mut generation_state, track_matches, - PanelEntry::FoldedDirs(worktree_id, folded_dirs), + PanelEntry::FoldedDirs(folded_dirs), folded_depth, cx, ); @@ -3490,13 +3706,17 @@ impl OutlinePanel { depth: usize, cx: &mut WindowContext, ) { - let entry = if let PanelEntry::FoldedDirs(worktree_id, entries) = &entry { - match entries.len() { + let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry { + match folded_dirs_entry.entries.len() { 0 => { debug_panic!("Empty folded dirs receiver"); return; } - 1 => PanelEntry::Fs(FsEntry::Directory(*worktree_id, entries[0].clone())), + 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id: folded_dirs_entry.worktree_id, + entry: folded_dirs_entry.entries[0].clone(), + git_status: folded_dirs_entry.git_file_statuses[0], + })), _ => entry, } } else { @@ -3515,22 +3735,22 @@ impl OutlinePanel { .push(StringMatchCandidate::new(id, &file_name)); } } - PanelEntry::FoldedDirs(worktree_id, entries) => { - let dir_names = self.dir_names_string(entries, *worktree_id, cx); + PanelEntry::FoldedDirs(folded_dir_entry) => { + let dir_names = self.dir_names_string( + &folded_dir_entry.entries, + folded_dir_entry.worktree_id, + cx, + ); { state .match_candidates .push(StringMatchCandidate::new(id, &dir_names)); } } - PanelEntry::Outline(outline_entry) => match outline_entry { - OutlineEntry::Outline(_, _, outline) => { - state - .match_candidates - .push(StringMatchCandidate::new(id, &outline.text)); - } - OutlineEntry::Excerpt(..) => {} - }, + PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state + .match_candidates + .push(StringMatchCandidate::new(id, &outline_entry.outline.text)), + PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {} PanelEntry::Search(new_search_entry) => { if let Some(search_data) = new_search_entry.render_data.get() { state @@ -3580,11 +3800,17 @@ impl OutlinePanel { fn is_expanded(&self, entry: &FsEntry) -> bool { let entry_to_check = match entry { - FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id), - FsEntry::File(worktree_id, _, buffer_id, _) => { - CollapsedEntry::File(*worktree_id, *buffer_id) + FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { + CollapsedEntry::ExternalFile(*buffer_id) } - FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id), + FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + }) => CollapsedEntry::File(*worktree_id, *buffer_id), + FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + }) => CollapsedEntry::Dir(*worktree_id, entry.id), }; !self.collapsed_entries.contains(&entry_to_check) } @@ -3708,11 +3934,11 @@ impl OutlinePanel { self.push_entry( state, track_matches, - PanelEntry::Outline(OutlineEntry::Excerpt( + PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt { buffer_id, - excerpt_id, - excerpt.range.clone(), - )), + id: excerpt_id, + range: excerpt.range.clone(), + })), excerpt_depth, cx, ); @@ -3733,11 +3959,11 @@ impl OutlinePanel { self.push_entry( state, track_matches, - PanelEntry::Outline(OutlineEntry::Outline( + PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline { buffer_id, excerpt_id, - outline.clone(), - )), + outline: outline.clone(), + })), outline_base_depth + outline.depth, cx, ); @@ -3763,9 +3989,9 @@ impl OutlinePanel { let kind = search_state.kind; let related_excerpts = match &parent_entry { - FsEntry::Directory(_, _) => return, - FsEntry::ExternalFile(_, excerpts) => excerpts, - FsEntry::File(_, _, _, excerpts) => excerpts, + FsEntry::Directory(_) => return, + FsEntry::ExternalFile(external) => &external.excerpts, + FsEntry::File(file) => &file.excerpts, } .iter() .copied() @@ -4031,24 +4257,28 @@ impl OutlinePanel { fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &AppContext) -> u64 { let item_text_chars = match entry { - PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => self - .buffer_snapshot_for_id(*buffer_id, cx) + PanelEntry::Fs(FsEntry::ExternalFile(external)) => self + .buffer_snapshot_for_id(external.buffer_id, cx) .and_then(|snapshot| { Some(snapshot.file()?.path().file_name()?.to_string_lossy().len()) }) .unwrap_or_default(), - PanelEntry::Fs(FsEntry::Directory(_, directory)) => directory + PanelEntry::Fs(FsEntry::Directory(directory)) => directory + .entry .path .file_name() .map(|name| name.to_string_lossy().len()) .unwrap_or_default(), - PanelEntry::Fs(FsEntry::File(_, file, _, _)) => file + PanelEntry::Fs(FsEntry::File(file)) => file + .entry .path .file_name() .map(|name| name.to_string_lossy().len()) .unwrap_or_default(), - PanelEntry::FoldedDirs(_, dirs) => { - dirs.iter() + PanelEntry::FoldedDirs(folded_dirs) => { + folded_dirs + .entries + .iter() .map(|dir| { dir.path .file_name() @@ -4056,13 +4286,13 @@ impl OutlinePanel { .unwrap_or_default() }) .sum::() - + dirs.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len() + + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len() } - PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, _, range)) => self - .excerpt_label(*buffer_id, range, cx) + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self + .excerpt_label(excerpt.buffer_id, &excerpt.range, cx) .map(|label| label.len()) .unwrap_or_default(), - PanelEntry::Outline(OutlineEntry::Outline(_, _, outline)) => outline.text.len(), + PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(), PanelEntry::Search(search) => search .render_data .get() @@ -4136,38 +4366,25 @@ impl OutlinePanel { cached_entry.string_match.as_ref(), cx, )), - PanelEntry::FoldedDirs(worktree_id, entries) => { + PanelEntry::FoldedDirs(folded_dirs_entry) => { Some(outline_panel.render_folded_dirs( - worktree_id, - &entries, + &folded_dirs_entry, + cached_entry.depth, + cached_entry.string_match.as_ref(), + cx, + )) + } + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { + outline_panel.render_excerpt(&excerpt, cached_entry.depth, cx) + } + PanelEntry::Outline(OutlineEntry::Outline(entry)) => { + Some(outline_panel.render_outline( + &entry, cached_entry.depth, cached_entry.string_match.as_ref(), cx, )) } - PanelEntry::Outline(OutlineEntry::Excerpt( - buffer_id, - excerpt_id, - excerpt, - )) => outline_panel.render_excerpt( - buffer_id, - excerpt_id, - &excerpt, - cached_entry.depth, - cx, - ), - PanelEntry::Outline(OutlineEntry::Outline( - buffer_id, - excerpt_id, - outline, - )) => Some(outline_panel.render_outline( - buffer_id, - excerpt_id, - &outline, - cached_entry.depth, - cached_entry.string_match.as_ref(), - cx, - )), PanelEntry::Search(SearchEntry { match_range, render_data, @@ -4314,23 +4531,24 @@ impl OutlinePanel { self.fs_entries .iter() .skip_while(|fs_entry| match fs_entry { - FsEntry::Directory(worktree_id, entry) => { - *worktree_id != dir_worktree || entry != dir_entry + FsEntry::Directory(directory) => { + directory.worktree_id != dir_worktree || &directory.entry != dir_entry } _ => true, }) .skip(1) .take_while(|fs_entry| match fs_entry { FsEntry::ExternalFile(..) => false, - FsEntry::Directory(worktree_id, entry) => { - *worktree_id == dir_worktree && entry.path.starts_with(&dir_entry.path) + FsEntry::Directory(directory) => { + directory.worktree_id == dir_worktree + && directory.entry.path.starts_with(&dir_entry.path) } - FsEntry::File(worktree_id, entry, ..) => { - *worktree_id == dir_worktree && entry.path.starts_with(&dir_entry.path) + FsEntry::File(file) => { + file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path) } }) .filter_map(|fs_entry| match fs_entry { - FsEntry::File(_, _, buffer_id, _) => Some(*buffer_id), + FsEntry::File(file) => Some(file.buffer_id), _ => None, }) .collect() @@ -4674,14 +4892,14 @@ fn subscribe_for_editor_events( .fs_entries .iter() .find_map(|fs_entry| match fs_entry { - FsEntry::ExternalFile(buffer_id, _) => { - if *buffer_id == toggled_buffer_id { + FsEntry::ExternalFile(external) => { + if external.buffer_id == toggled_buffer_id { Some(fs_entry.clone()) } else { None } } - FsEntry::File(_, _, buffer_id, _) => { + FsEntry::File(FsEntryFile { buffer_id, .. }) => { if *buffer_id == toggled_buffer_id { Some(fs_entry.clone()) } else { @@ -5541,41 +5759,46 @@ mod tests { } display_string += &match &entry.entry { PanelEntry::Fs(entry) => match entry { - FsEntry::ExternalFile(_, _) => { + FsEntry::ExternalFile(_) => { panic!("Did not cover external files with tests") } - FsEntry::Directory(_, dir_entry) => format!( + FsEntry::Directory(directory) => format!( "{}/", - dir_entry + directory + .entry .path .file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_default() ), - FsEntry::File(_, file_entry, ..) => file_entry + FsEntry::File(file) => file + .entry .path .file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_default(), }, - PanelEntry::FoldedDirs(_, dirs) => dirs + PanelEntry::FoldedDirs(folded_dirs) => folded_dirs + .entries .iter() .filter_map(|dir| dir.path.file_name()) .map(|name| name.to_string_lossy().to_string() + "/") .collect(), PanelEntry::Outline(outline_entry) => match outline_entry { - OutlineEntry::Excerpt(_, _, _) => continue, - OutlineEntry::Outline(_, _, outline) => format!("outline: {}", outline.text), + OutlineEntry::Excerpt(_) => continue, + OutlineEntry::Outline(outline_entry) => { + format!("outline: {}", outline_entry.outline.text) + } }, - PanelEntry::Search(SearchEntry { - render_data, - match_range, - .. - }) => { + PanelEntry::Search(search_entry) => { format!( "search: {}", - render_data - .get_or_init(|| SearchData::new(match_range, &multi_buffer_snapshot)) + search_entry + .render_data + .get_or_init(|| SearchData::new( + &search_entry.match_range, + &multi_buffer_snapshot + )) .context_text ) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3def4506e8c6f..0083bd556146c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -39,7 +39,10 @@ use futures::{ pub use image_store::{ImageItem, ImageStore}; use image_store::{ImageItemEvent, ImageStoreEvent}; -use git::{blame::Blame, repository::GitRepository}; +use git::{ + blame::Blame, + repository::{GitFileStatus, GitRepository}, +}; use gpui::{ AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla, Model, ModelContext, SharedString, Task, WeakModel, WindowContext, @@ -1432,6 +1435,15 @@ impl Project { .unwrap_or(false) } + pub fn project_path_git_status( + &self, + project_path: &ProjectPath, + cx: &AppContext, + ) -> Option { + self.worktree_for_id(project_path.worktree_id, cx) + .and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path)) + } + pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option { paths .iter() @@ -4444,11 +4456,11 @@ impl Completion { } } -pub fn sort_worktree_entries(entries: &mut [Entry]) { +pub fn sort_worktree_entries(entries: &mut [(Entry, Option)]) { entries.sort_by(|entry_a, entry_b| { compare_paths( - (&entry_a.path, entry_a.is_file()), - (&entry_b.path, entry_b.is_file()), + (&entry_a.0.path, entry_a.0.is_file()), + (&entry_b.0.path, entry_b.0.is_file()), ) }); } diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 9fce4d90c9e28..64f31ee7bd7e4 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -379,7 +379,7 @@ impl WorktreeStore { cx.subscribe( worktree, - |this, _, event: &worktree::Event, cx| match event { + |_this, _, event: &worktree::Event, cx| match event { worktree::Event::UpdatedGitRepositories(_) => { cx.emit(WorktreeStoreEvent::GitRepositoryUpdated); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2053fbe6d4fea..20bab76334d42 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -76,7 +76,11 @@ pub struct ProjectPanel { // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's // hovered over the start/end of a list. hover_scroll_task: Option>, - visible_entries: Vec<(WorktreeId, Vec, OnceCell>>)>, + visible_entries: Vec<( + WorktreeId, + Vec<(Entry, Option)>, + OnceCell>>, + )>, /// Maps from leaf project entry ID to the currently selected ancestor. /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several /// project entries (and all non-leaf nodes are guaranteed to be directories). @@ -889,7 +893,7 @@ impl ProjectPanel { let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix]; let selection = SelectedEntry { worktree_id: *worktree_id, - entry_id: worktree_entries[entry_ix].id, + entry_id: worktree_entries[entry_ix].0.id, }; self.selection = Some(selection); if cx.modifiers().shift { @@ -1359,7 +1363,7 @@ impl ProjectPanel { let parent_entry = worktree.entry_for_path(parent_path)?; // Remove all siblings that are being deleted except the last marked entry - let mut siblings: Vec = worktree + let mut siblings: Vec<_> = worktree .snapshot() .child_entries(parent_path) .filter(|sibling| { @@ -1369,13 +1373,14 @@ impl ProjectPanel { entry_id: sibling.id, }) }) + .map(|sibling| &(*sibling, None)) .cloned() .collect(); project::sort_worktree_entries(&mut siblings); let sibling_entry_index = siblings .iter() - .position(|sibling| sibling.id == latest_entry.id)?; + .position(|sibling| sibling.0.id == latest_entry.0.id)?; if let Some(next_sibling) = sibling_entry_index .checked_add(1) @@ -1383,7 +1388,7 @@ impl ProjectPanel { { return Some(SelectedEntry { worktree_id, - entry_id: next_sibling.id, + entry_id: next_sibling.0.id, }); } if let Some(prev_sibling) = sibling_entry_index @@ -1392,7 +1397,7 @@ impl ProjectPanel { { return Some(SelectedEntry { worktree_id, - entry_id: prev_sibling.id, + entry_id: prev_sibling.0.id, }); } // No neighbour sibling found, fall back to parent @@ -1486,7 +1491,7 @@ impl ProjectPanel { if let Some(entry) = worktree_entries.get(entry_ix) { let selection = SelectedEntry { worktree_id: *worktree_id, - entry_id: entry.id, + entry_id: entry.0.id, }; self.selection = Some(selection); if cx.modifiers().shift { @@ -1506,7 +1511,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), true, - |entry, worktree_id| { + |entry, _, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1536,7 +1541,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), false, - |entry, worktree_id| { + |entry, _, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1566,7 +1571,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), true, - |entry, worktree_id| { + |entry, git_status, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1576,9 +1581,7 @@ impl ProjectPanel { } })) && entry.is_file() - && entry - .git_status - .is_some_and(|status| matches!(status, GitFileStatus::Modified)) + && git_status.is_some_and(|status| matches!(status, GitFileStatus::Modified)) }, cx, ); @@ -1646,7 +1649,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), true, - |entry, worktree_id| { + |entry, git_status, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1656,9 +1659,7 @@ impl ProjectPanel { } })) && entry.is_file() - && entry - .git_status - .is_some_and(|status| matches!(status, GitFileStatus::Modified)) + && git_status.is_some_and(|status| matches!(status, GitFileStatus::Modified)) }, cx, ); @@ -2109,7 +2110,7 @@ impl ProjectPanel { { if *worktree_id == selection.worktree_id { for entry in worktree_entries { - if entry.id == selection.entry_id { + if entry.0.id == selection.entry_id { return Some((worktree_index, entry_index, visible_entries_index)); } else { visible_entries_index += 1; @@ -2457,8 +2458,13 @@ impl ProjectPanel { entry_iter.advance(); } - snapshot.propagate_git_statuses(&mut visible_worktree_entries); + let git_statuses = snapshot.propagate_git_statuses(&mut visible_worktree_entries); + let mut visible_worktree_entries = visible_worktree_entries + .into_iter() + .zip(git_statuses.into_iter()) + .collect::>(); project::sort_worktree_entries(&mut visible_worktree_entries); + self.visible_entries .push((worktree_id, visible_worktree_entries, OnceCell::new())); } @@ -2469,7 +2475,7 @@ impl ProjectPanel { if worktree_id == *id { entries .iter() - .position(|entry| entry.id == project_entry_id) + .position(|entry| entry.0.id == project_entry_id) } else { visited_worktrees_length += entries.len(); None @@ -2648,19 +2654,19 @@ impl ProjectPanel { return visible_worktree_entries .iter() .enumerate() - .find(|(_, entry)| entry.id == entry_id) + .find(|(_, entry)| entry.0.id == entry_id) .map(|(ix, _)| (worktree_ix, ix, total_ix + ix)); } None } - fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, &Entry)> { + fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, Option, &Entry)> { let mut offset = 0; for (worktree_id, visible_worktree_entries, _) in &self.visible_entries { if visible_worktree_entries.len() > offset + index { return visible_worktree_entries .get(index) - .map(|entry| (*worktree_id, entry)); + .map(|(entry, git_file_status)| (*worktree_id, *git_file_status, entry)); } offset += visible_worktree_entries.len(); } @@ -2689,11 +2695,11 @@ impl ProjectPanel { let entries = entries_paths.get_or_init(|| { visible_worktree_entries .iter() - .map(|e| (e.path.clone())) + .map(|e| (e.0.path.clone())) .collect() }); for entry in visible_worktree_entries[entry_range].iter() { - callback(entry, entries, cx); + callback(&entry.0, entries, cx); } ix = end_ix; } @@ -2738,11 +2744,11 @@ impl ProjectPanel { let entries = entries_paths.get_or_init(|| { visible_worktree_entries .iter() - .map(|e| (e.path.clone())) + .map(|e| (e.0.path.clone())) .collect() }); - for entry in visible_worktree_entries[entry_range].iter() { - let status = git_status_setting.then_some(entry.git_status).flatten(); + for (entry, git_status) in visible_worktree_entries[entry_range].iter() { + let status = git_status_setting.then_some(*git_status).flatten(); let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); let icon = match entry.kind { EntryKind::File => { @@ -2762,7 +2768,7 @@ impl ProjectPanel { }; let (depth, difference) = - ProjectPanel::calculate_depth_and_difference(entry, entries); + ProjectPanel::calculate_depth_and_difference(&entry, entries); let filename = match difference { diff if diff > 1 => entry @@ -2891,7 +2897,7 @@ impl ProjectPanel { worktree_id: WorktreeId, reverse_search: bool, only_visible_entries: bool, - predicate: impl Fn(&Entry, WorktreeId) -> bool, + predicate: impl Fn(&Entry, Option, WorktreeId) -> bool, cx: &mut ViewContext, ) -> Option { if only_visible_entries { @@ -2908,15 +2914,19 @@ impl ProjectPanel { .clone(); return utils::ReversibleIterable::new(entries.iter(), reverse_search) - .find(|ele| predicate(ele, worktree_id)) - .cloned(); + .find(|ele| predicate(&ele.0, ele.1, worktree_id)) + .map(|ele| ele.0); } let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; worktree.update(cx, |tree, _| { - utils::ReversibleIterable::new(tree.entries(true, 0usize), reverse_search) - .find_single_ended(|ele| predicate(ele, worktree_id)) - .cloned() + utils::ReversibleIterable::new( + tree.entries(true, 0usize) + .with_git_statuses(&tree.snapshot()), + reverse_search, + ) + .find_single_ended(|ele| predicate(&ele.0, ele.1, worktree_id)) + .map(|ele| ele.0) }) } @@ -2924,7 +2934,7 @@ impl ProjectPanel { &self, start: Option<&SelectedEntry>, reverse_search: bool, - predicate: impl Fn(&Entry, WorktreeId) -> bool, + predicate: impl Fn(&Entry, Option, WorktreeId) -> bool, cx: &mut ViewContext, ) -> Option { let mut worktree_ids: Vec<_> = self @@ -2946,7 +2956,11 @@ impl ProjectPanel { let root_entry = tree.root_entry()?; let tree_id = tree.id(); - let mut first_iter = tree.traverse_from_path(true, true, true, entry.path.as_ref()); + // TOOD: Expose the all statuses cursor, as a wrapper over a Traversal + // The co-iterates the GitEntries as the file entries come through + let mut first_iter = tree + .traverse_from_path(true, true, true, entry.path.as_ref()) + .with_git_statuses(tree); if reverse_search { first_iter.next(); @@ -2954,9 +2968,9 @@ impl ProjectPanel { let first = first_iter .enumerate() - .take_until(|(count, ele)| *ele == root_entry && *count != 0usize) + .take_until(|(count, ele)| ele.0 == root_entry && *count != 0usize) .map(|(_, ele)| ele) - .find(|ele| predicate(ele, tree_id)) + .find(|ele| predicate(ele.0, ele.1 tree_id)) .cloned(); let second_iter = tree.entries(true, 0usize); @@ -4012,7 +4026,8 @@ impl Render for ProjectPanel { if cx.modifiers().secondary() { let ix = active_indent_guide.offset.y; let Some((target_entry, worktree)) = maybe!({ - let (worktree_id, entry) = this.entry_at_index(ix)?; + let (worktree_id, _git_status, entry) = + this.entry_at_index(ix)?; let worktree = this .project .read(cx) diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 5d02884472eaa..c4223d09c6a76 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -358,13 +358,14 @@ impl PickerDelegate for TabSwitcherDelegate { .item .project_path(cx) .as_ref() - .and_then(|path| self.project.read(cx).entry_for_path(path, cx)) - .map(|entry| { - entry_git_aware_label_color( - entry.git_status, - entry.is_ignored, - selected, - ) + .and_then(|path| { + let project = self.project.read(cx); + let entry = project.entry_for_path(path, cx)?; + let git_status = project.project_path_git_status(path, cx); + Some((entry, git_status)) + }) + .map(|(entry, git_status)| { + entry_git_aware_label_color(git_status, entry.is_ignored, selected) }) }) .flatten(); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 3623b97aa8a9f..a76192a81742e 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -284,6 +284,14 @@ impl Default for RepositoryWorkDirectory { } } +impl Deref for RepositoryWorkDirectory { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + impl AsRef for RepositoryWorkDirectory { fn as_ref(&self) -> &Path { self.0.as_ref() @@ -5631,6 +5639,45 @@ impl<'a> Default for TraversalProgress<'a> { } } +pub struct GitTraversal<'a> { + // statuses: /AllStatusesCursor<'a, I>, + traversal: Traversal<'a>, +} + +pub struct EntryWithGitStatus { + entry: Entry, + git_status: Option, +} + +impl Deref for EntryWithGitStatus { + type Target = Entry; + + fn deref(&self) -> &Self::Target { + &self.entry + } +} + +impl< + 'a, + // I: Iterator + FusedIterator, + > Iterator for GitTraversal<'a> +{ + type Item = (Entry, Option); + fn next(&mut self) -> Option { + todo!() + } +} + +impl< + 'a, + // I: Iterator + FusedIterator, + > DoubleEndedIterator for GitTraversal<'a> +{ + fn next_back(&mut self) -> Option { + todo!() + } +} + pub struct Traversal<'a> { cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>, include_ignored: bool, @@ -5659,6 +5706,14 @@ impl<'a> Traversal<'a> { } traversal } + + pub fn with_git_statuses(self, snapshot: &'a Snapshot) -> GitTraversal<'a> { + GitTraversal { + // statuses: all_statuses_cursor(snapshot), + traversal: self, + } + } + pub fn advance(&mut self) -> bool { self.advance_by(1) } From 3ddd92632c9443a295f2c4cae5bb2d190b7e07f6 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 21 Dec 2024 09:27:51 -0800 Subject: [PATCH 17/22] WIP: Switch to using a sumtree for repository entries --- crates/sum_tree/src/sum_tree.rs | 24 ++++ crates/worktree/src/worktree.rs | 190 +++++++++++++++++++++----------- 2 files changed, 148 insertions(+), 66 deletions(-) diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index fbfe3b06f3ab4..b1eea6103cf46 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -761,6 +761,30 @@ impl SumTree { None } } + + pub fn update( + &mut self, + key: &T::Key, + cx: &::Context, + f: F, + ) -> Option + where + F: FnOnce(&mut T) -> R, + { + let mut cursor = self.cursor::(cx); + let mut new_tree = cursor.slice(key, Bias::Left, cx); + let mut result = None; + if Ord::cmp(key, &cursor.end(cx)) == Ordering::Equal { + let mut updated = cursor.item().unwrap().clone(); + result = Some(f(&mut updated)); + new_tree.push(updated, cx); + cursor.next(cx); + } + new_tree.append(cursor.suffix(cx), cx); + drop(cursor); + *self = new_tree; + result + } } impl Default for SumTree diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a76192a81742e..155ce70289021 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -155,7 +155,7 @@ pub struct Snapshot { entries_by_path: SumTree, entries_by_id: SumTree, always_included_entries: Vec>, - repository_entries: TreeMap, + repository_entries: SumTree, /// A number that increases every time the worktree begins scanning /// a set of paths from the filesystem. This scanning could be caused @@ -192,7 +192,7 @@ pub struct RepositoryEntry { /// - my_sub_folder_1/project_root/changed_file_1 /// - my_sub_folder_2/changed_file_2 pub(crate) git_entries_by_path: SumTree, - pub(crate) work_directory: WorkDirectoryEntry, + pub(crate) work_directory: RepositoryWorkDirectory, pub(crate) branch: Option>, /// If location_in_repo is set, it means the .git folder is external @@ -1264,7 +1264,7 @@ impl LocalWorktree { if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) { let old_repo = old_snapshot .repository_entries - .get(&RepositoryWorkDirectory(entry.path.clone())) + .get(&PathKey(entry.path.clone()), &()) .cloned(); changes.push(( entry.path.clone(), @@ -1281,7 +1281,7 @@ impl LocalWorktree { if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) { let old_repo = old_snapshot .repository_entries - .get(&RepositoryWorkDirectory(entry.path.clone())) + .get(&RepositoryWorkDirectory(entry.path.clone()), &()) .cloned(); changes.push(( entry.path.clone(), @@ -1309,7 +1309,7 @@ impl LocalWorktree { if let Some(entry) = old_snapshot.entry_for_id(entry_id) { let old_repo = old_snapshot .repository_entries - .get(&RepositoryWorkDirectory(entry.path.clone())) + .get(&RepositoryWorkDirectory(entry.path.clone()), &()) .cloned(); changes.push(( entry.path.clone(), @@ -2317,7 +2317,7 @@ impl Snapshot { if let Some(entry) = self.entry_for_id(*work_directory_entry) { let work_directory = RepositoryWorkDirectory(entry.path.clone()); - if self.repository_entries.get(&work_directory).is_some() { + if self.repository_entries.get(&work_directory, &()).is_some() { self.repository_entries.update(&work_directory, |repo| { repo.branch = repository.branch.map(Into::into); }); @@ -2431,7 +2431,7 @@ impl Snapshot { pub fn repositories( &self, ) -> impl Iterator { - self.repository_entries.iter() + self.repository_entries.self.repository_entries.iter() } /// Get the repository whose work directory contains the given path. @@ -2439,7 +2439,7 @@ impl Snapshot { &self, path: &RepositoryWorkDirectory, ) -> Option { - self.repository_entries.get(path).cloned() + self.repository_entries.get(path, &()).cloned() } /// Get the repository whose work directory contains the given path. @@ -2524,7 +2524,7 @@ impl Snapshot { }; if let Some((entry_ix, prev_statuses)) = entry_to_finish { - cursor.seek_forward(&GitEntryTraversalTarget::PathSuccessor( + cursor.seek_forward(&PathSummaryTraversalTarget::PathSuccessor( &entries[entry_ix].path, )); @@ -2541,7 +2541,7 @@ impl Snapshot { }; } else { if entries[entry_ix].is_dir() { - cursor.seek_forward(&GitEntryTraversalTarget::Path(&entries[entry_ix].path)); + cursor.seek_forward(&PathSummaryTraversalTarget::Path(&entries[entry_ix].path)); entry_stack.push((entry_ix, cursor.start())); } entry_ix += 1; @@ -2590,18 +2590,18 @@ impl Snapshot { pub fn root_git_entry(&self) -> Option { self.repository_entries - .get(&RepositoryWorkDirectory(Path::new("").into())) + .get(&PathKey(Path::new("").into()), &()) .map(|entry| entry.to_owned()) } pub fn git_entry(&self, work_directory_path: Arc) -> Option { self.repository_entries - .get(&RepositoryWorkDirectory(work_directory_path)) + .get(&PathKey(work_directory_path), &()) .map(|entry| entry.to_owned()) } pub fn git_entries(&self) -> impl Iterator { - self.repository_entries.values() + self.repository_entries.iter() } pub fn scan_id(&self) -> usize { @@ -2672,9 +2672,7 @@ impl LocalSnapshot { } for (work_dir_path, change) in repo_changes.iter() { - let new_repo = self - .repository_entries - .get(&RepositoryWorkDirectory(work_dir_path.clone())); + let new_repo = self.repository_entries.get(&PathKey(work_dir_path.clone())); match (&change.old_repository, new_repo) { (Some(old_repo), Some(new_repo)) => { updated_repositories.push(new_repo.build_update(old_repo)); @@ -3557,34 +3555,96 @@ pub struct GitEntry { } #[derive(Clone, Debug)] -pub struct GitEntrySummary { +struct PathItem(I); + +#[derive(Clone, Debug)] +struct PathSummary { max_path: Arc, - statuses: GitStatuses, + item_summary: S, } -impl sum_tree::Summary for GitEntrySummary { +impl Summary for PathSummary { type Context = (); - fn zero(_cx: &Self::Context) -> Self { - GitEntrySummary { - max_path: Arc::from(Path::new("")), - statuses: Default::default(), + fn zero(cx: &Self::Context) -> Self { + Self { + max_path: Path::new("").into(), + item_summary: S::zero(&()), } } - fn add_summary(&mut self, rhs: &Self, _: &Self::Context) { + fn add_summary(&mut self, rhs: &Self, cx: &Self::Context) { self.max_path = rhs.max_path.clone(); - self.statuses += rhs.statuses; + self.item_summary.add_summary(&rhs.item_summary, &()); + } +} + +impl sum_tree::Item for PathItem +where + I: sum_tree::Item + AsRef> + Clone, +{ + type Summary = PathSummary; + + fn summary(&self, cx: &::Context) -> Self::Summary { + PathSummary { + max_path: self.0.as_ref().clone(), + item_summary: self.0.summary(cx), + } + } +} + +// This type exists because we can't implement Summary for () +#[derive(Clone)] +struct Nothing; + +impl Summary for Nothing { + type Context = (); + + fn zero(_: &()) -> Self { + Nothing + } + + fn add_summary(&mut self, _: &Self, _: &()) {} +} + +impl sum_tree::Item for RepositoryEntry { + type Summary = PathSummary; + + fn summary(&self, _: &::Context) -> Self::Summary { + PathSummary { + max_path: self.work_directory.0.clone(), + item_summary: Nothing, + } + } +} + +impl sum_tree::KeyedItem for RepositoryEntry { + type Key = PathKey; + + fn key(&self) -> Self::Key { + PathKey(self.work_directory.0.clone()) + } +} + +impl sum_tree::Summary for GitStatuses { + type Context = (); + + fn zero(_: &Self::Context) -> Self { + Default::default() + } + + fn add_summary(&mut self, rhs: &Self, cx: &Self::Context) { + *self += rhs; } } impl sum_tree::Item for GitEntry { - type Summary = GitEntrySummary; + type Summary = PathSummary; fn summary(&self, _: &::Context) -> Self::Summary { - GitEntrySummary { + PathSummary { max_path: self.path.0.clone(), - statuses: match self.git_status { + item_summary: match self.git_status { GitFileStatus::Added => GitStatuses { added: 1, ..Default::default() @@ -3658,56 +3718,56 @@ impl std::ops::Sub for GitStatuses { } } -impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for GitStatuses { +impl<'a> sum_tree::Dimension<'a, PathSummary> for GitStatuses { fn zero(_cx: &()) -> Self { Default::default() } - fn add_summary(&mut self, summary: &'a GitEntrySummary, _: &()) { - *self += summary.statuses + fn add_summary(&mut self, summary: &'a PathSummary, _: &()) { + *self += summary.item_summary } } -impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for PathKey { - fn zero(_cx: &()) -> Self { +impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for PathKey { + fn zero(_: &()) -> Self { Default::default() } - fn add_summary(&mut self, summary: &'a GitEntrySummary, _: &()) { + fn add_summary(&mut self, summary: &'a PathSummary, _: &()) { self.0 = summary.max_path.clone(); } } -impl<'a> sum_tree::Dimension<'a, GitEntrySummary> for TraversalProgress<'a> { +impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for TraversalProgress<'a> { fn zero(_cx: &()) -> Self { Default::default() } - fn add_summary(&mut self, summary: &'a GitEntrySummary, _: &()) { + fn add_summary(&mut self, summary: &'a PathSummary, _: &()) { self.max_path = summary.max_path.as_ref(); } } #[derive(Clone, Copy, Debug)] -enum GitEntryTraversalTarget<'a> { +enum PathSummaryTraversalTarget<'a> { PathSuccessor(&'a Path), Path(&'a Path), } -impl<'a> GitEntryTraversalTarget<'a> { +impl<'a> PathSummaryTraversalTarget<'a> { fn path(&self) -> &'a Path { match self { - GitEntryTraversalTarget::Path(path) => path, - GitEntryTraversalTarget::PathSuccessor(path) => path, + PathSummaryTraversalTarget::Path(path) => path, + PathSummaryTraversalTarget::PathSuccessor(path) => path, } } - fn with_path(self, path: &Path) -> GitEntryTraversalTarget<'_> { + fn with_path(self, path: &Path) -> PathSummaryTraversalTarget<'_> { match self { - GitEntryTraversalTarget::PathSuccessor(_) => { - GitEntryTraversalTarget::PathSuccessor(path) + PathSummaryTraversalTarget::PathSuccessor(_) => { + PathSummaryTraversalTarget::PathSuccessor(path) } - GitEntryTraversalTarget::Path(_) => GitEntryTraversalTarget::Path(path), + PathSummaryTraversalTarget::Path(_) => PathSummaryTraversalTarget::Path(path), } } @@ -3722,13 +3782,13 @@ impl<'a> GitEntryTraversalTarget<'a> { } } -impl<'a, 'b> SeekTarget<'a, GitEntrySummary, TraversalProgress<'a>> - for GitEntryTraversalTarget<'b> +impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary, TraversalProgress<'a>> + for PathSummaryTraversalTarget<'b> { fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { match self { - GitEntryTraversalTarget::Path(path) => path.cmp(&cursor_location.max_path), - GitEntryTraversalTarget::PathSuccessor(path) => { + PathSummaryTraversalTarget::Path(path) => path.cmp(&cursor_location.max_path), + PathSummaryTraversalTarget::PathSuccessor(path) => { if cursor_location.max_path.starts_with(path) { Ordering::Greater } else { @@ -3739,13 +3799,13 @@ impl<'a, 'b> SeekTarget<'a, GitEntrySummary, TraversalProgress<'a>> } } -impl<'a, 'b> SeekTarget<'a, GitEntrySummary, (TraversalProgress<'a>, GitStatuses)> - for GitEntryTraversalTarget<'b> +impl<'a, 'b> SeekTarget<'a, PathSummary, (TraversalProgress<'a>, GitStatuses)> + for PathSummaryTraversalTarget<'b> { fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering { match self { - GitEntryTraversalTarget::Path(path) => path.cmp(&cursor_location.0.max_path), - GitEntryTraversalTarget::PathSuccessor(path) => { + PathSummaryTraversalTarget::Path(path) => path.cmp(&cursor_location.0.max_path), + PathSummaryTraversalTarget::PathSuccessor(path) => { if cursor_location.0.max_path.starts_with(path) { Ordering::Greater } else { @@ -3769,7 +3829,7 @@ impl<'a, I> AllStatusesCursor<'a, I> where I: Iterator + FusedIterator, { - fn seek_forward(&mut self, target: &GitEntryTraversalTarget<'_>) { + fn seek_forward(&mut self, target: &PathSummaryTraversalTarget<'_>) { loop { let (work_dir, cursor) = match &mut self.current_location { Some(location) => location, @@ -4809,11 +4869,12 @@ impl BackgroundScanner { changed_path_statuses.push(Edit::Remove(PathKey(path.0))); } state.snapshot.repository_entries.update( - &work_directory, + &PathKey(work_directory.0), + &(), move |repository_entry| { repository_entry .git_entries_by_path - .edit(changed_path_statuses, &()) + .edit(changed_path_statuses, &()); }, ); } @@ -4891,7 +4952,7 @@ impl BackgroundScanner { if let Some(repository) = snapshot.repository_for_work_directory(path) { let entry = repository.work_directory.0; snapshot.git_repositories.remove(&entry); - snapshot.snapshot.repository_entries.remove(path); + snapshot.snapshot.repository_entries.remove(path, &()); return Some(()); } } @@ -5118,7 +5179,7 @@ impl BackgroundScanner { location_in_repo: state .snapshot .repository_entries - .get(&work_directory) + .get(&work_directory, &()) .and_then(|repo| repo.location_in_repo.clone()) .clone(), work_directory, @@ -5640,13 +5701,14 @@ impl<'a> Default for TraversalProgress<'a> { } pub struct GitTraversal<'a> { - // statuses: /AllStatusesCursor<'a, I>, + // TODO + // statuses: Cursor<> traversal: Traversal<'a>, } pub struct EntryWithGitStatus { - entry: Entry, - git_status: Option, + pub entry: Entry, + pub git_status: Option, } impl Deref for EntryWithGitStatus { @@ -5657,12 +5719,8 @@ impl Deref for EntryWithGitStatus { } } -impl< - 'a, - // I: Iterator + FusedIterator, - > Iterator for GitTraversal<'a> -{ - type Item = (Entry, Option); +impl<'a> Iterator for GitTraversal<'a> { + type Item = EntryWithGitStatus; fn next(&mut self) -> Option { todo!() } From d7eaa23441ae02dc9821e96831e5dc53833ca6e6 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 21 Dec 2024 19:12:54 -0800 Subject: [PATCH 18/22] Finish shredding worktree, add the work directory data to the local and repository entries, and enhance the RepositoryWorkDirectory type to make the worktree code more ergonomic. --- crates/editor/src/git/project_diff.rs | 9 +- crates/git_ui/src/git_panel.rs | 5 +- crates/project/src/buffer_store.rs | 23 +- crates/sum_tree/src/sum_tree.rs | 40 ++ crates/worktree/src/worktree.rs | 546 +++++++++++++------------- crates/worktree/src/worktree_tests.rs | 59 +-- 6 files changed, 355 insertions(+), 327 deletions(-) diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 5ae26da03db6b..28cee78d937fd 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -197,12 +197,9 @@ impl ProjectDiffEditor { let snapshot = worktree.read(cx).snapshot(); let applicable_entries = snapshot .repositories() - .filter_map(|(work_dir, _entry)| { - Some((work_dir, snapshot.git_status(work_dir)?)) - }) - .flat_map(|(work_dir, statuses)| { - statuses.into_iter().map(|git_entry| { - (git_entry.git_status, work_dir.join(git_entry.path)) + .flat_map(|entry| { + entry.status().map(|git_entry| { + (git_entry.git_status, entry.join(git_entry.path)) }) }) .filter_map(|(status, path)| { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ea0fafcb16996..bc6ce873aedb6 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -394,9 +394,8 @@ impl GitPanel { let mut visible_worktree_entries = Vec::new(); let repositories = snapshot.repositories().take(1); // Only use the first for now - for (work_dir, _) in repositories { - visible_worktree_entries - .extend(snapshot.git_status(&work_dir).unwrap_or(Vec::new())); + for repository in repositories { + visible_worktree_entries.extend(repository.status()); } // let statuses = snapshot.propagate_git_statuses(&visible_worktree_entries); diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 750302bf7994a..7812dff8fff1d 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -569,15 +569,9 @@ impl LocalBufferStore { buffer_change_sets .into_iter() .filter_map(|(change_set, buffer_snapshot, path)| { - let repo_fields = snapshot.repo_for_path(&path)?; - let relative_path = repo_fields - .repository_entry - .relativize(&snapshot, &path) - .ok()?; - let base_text = repo_fields - .local_entry - .repo() - .load_index_text(&relative_path); + let local_repo = snapshot.local_repo_for_path(&path)?; + let relative_path = local_repo.relativize(&path).ok()?; + let base_text = local_repo.repo().load_index_text(&relative_path); Some((change_set, buffer_snapshot, base_text)) }) .collect::>() @@ -1167,17 +1161,16 @@ impl BufferStore { Worktree::Local(worktree) => { let worktree = worktree.snapshot(); let blame_params = maybe!({ - let repo_fields = match worktree.repo_for_path(&file.path) { + let local_repo = match worktree.local_repo_for_path(&file.path) { Some(repo_for_path) => repo_for_path, None => return Ok(None), }; - let relative_path = repo_fields - .repository_entry - .relativize(&worktree, &file.path) + let relative_path = local_repo + .relativize(&file.path) .context("failed to relativize buffer path")?; - let repo = repo_fields.local_entry.repo().clone(); + let repo = local_repo.repo().clone(); let content = match version { Some(version) => buffer.rope_for_version(&version).clone(), @@ -1254,7 +1247,7 @@ impl BufferStore { }); }; - let path = match repo_entry.relativize(worktree, file.path()) { + let path = match repo_entry.relativize(file.path()) { Ok(RepoPath(path)) => path, Err(e) => return Task::ready(Err(e)), }; diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index b1eea6103cf46..9c661ed39f5cb 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -42,6 +42,21 @@ pub trait Summary: Clone { fn add_summary(&mut self, summary: &Self, cx: &Self::Context); } +/// This type exists because we can't implement Summary for () without causing +/// type resolution errors +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +struct Nothing; + +impl Summary for Nothing { + type Context = (); + + fn zero(_: &()) -> Self { + Nothing + } + + fn add_summary(&mut self, _: &Self, _: &()) {} +} + /// Each [`Summary`] type can have more than one [`Dimension`] type that it measures. /// /// You can use dimensions to seek to a specific location in the [`SumTree`] @@ -762,6 +777,11 @@ impl SumTree { } } + #[inline] + pub fn contains(&self, key: &T::Key, cx: &::Context) -> bool { + self.get(key, cx).is_some() + } + pub fn update( &mut self, key: &T::Key, @@ -785,6 +805,26 @@ impl SumTree { *self = new_tree; result } + + pub fn retain bool>( + &mut self, + cx: &::Context, + mut predicate: F, + ) { + let mut new_map = SumTree::new(cx); + + let mut cursor = self.cursor::(cx); + cursor.next(cx); + while let Some(item) = cursor.item() { + if predicate(&item) { + new_map.push(item.clone(), cx); + } + cursor.next(cx); + } + drop(cursor); + + *self = new_map; + } } impl Default for SumTree diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 155ce70289021..33a1b2f71352e 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -64,7 +64,7 @@ use std::{ }, time::{Duration, Instant}, }; -use sum_tree::{Bias, Cursor, Edit, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; +use sum_tree::{Bias, Cursor, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ paths::{home_dir, PathMatcher, SanitizedPath}, @@ -155,7 +155,7 @@ pub struct Snapshot { entries_by_path: SumTree, entries_by_id: SumTree, always_included_entries: Vec>, - repository_entries: SumTree, + repositories: SumTree, /// A number that increases every time the worktree begins scanning /// a set of paths from the filesystem. This scanning could be caused @@ -192,8 +192,60 @@ pub struct RepositoryEntry { /// - my_sub_folder_1/project_root/changed_file_1 /// - my_sub_folder_2/changed_file_2 pub(crate) git_entries_by_path: SumTree, - pub(crate) work_directory: RepositoryWorkDirectory, + pub(crate) work_directory_id: ProjectEntryId, + pub(crate) work_directory: WorkDirectory, pub(crate) branch: Option>, +} + +impl Deref for RepositoryEntry { + type Target = WorkDirectory; + + fn deref(&self) -> &Self::Target { + &self.work_directory + } +} + +impl AsRef for RepositoryEntry { + fn as_ref(&self) -> &Path { + &self.path + } +} + +impl RepositoryEntry { + pub fn branch(&self) -> Option> { + self.branch.clone() + } + + pub fn work_directory_id(&self) -> ProjectEntryId { + self.work_directory_id + } + + pub fn build_update(&self, _: &Self) -> proto::RepositoryEntry { + self.into() + } + + pub fn status(&self) -> impl Iterator + '_ { + self.git_entries_by_path.iter().cloned() + } +} + +impl From<&RepositoryEntry> for proto::RepositoryEntry { + fn from(value: &RepositoryEntry) -> Self { + proto::RepositoryEntry { + work_directory_id: value.work_directory_id.to_proto(), + branch: value.branch.as_ref().map(|str| str.to_string()), + } + } +} + +/// This path corresponds to the 'content path' of a repository in relation +/// to Zed's project root. +/// In the majority of the cases, this is the folder that contains the .git folder. +/// But if a sub-folder of a git repository is opened, this corresponds to the +/// project root and the .git folder is located in a parent directory. +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct WorkDirectory { + path: Arc, /// If location_in_repo is set, it means the .git folder is external /// and in a parent folder of the project root. @@ -216,23 +268,14 @@ pub struct RepositoryEntry { pub(crate) location_in_repo: Option>, } -impl RepositoryEntry { - pub fn branch(&self) -> Option> { - self.branch.clone() - } - - pub fn work_directory_id(&self) -> ProjectEntryId { - *self.work_directory - } - - pub fn work_directory(&self, snapshot: &Snapshot) -> Option { - snapshot - .entry_for_id(self.work_directory_id()) - .map(|entry| RepositoryWorkDirectory(entry.path.clone())) +impl WorkDirectory { + pub fn path_key(&self) -> PathKey { + PathKey(self.path.clone()) } - pub fn build_update(&self, _: &Self) -> proto::RepositoryEntry { - self.into() + pub fn contains(&self, path: impl AsRef) -> bool { + let path = path.as_ref(); + path.starts_with(&self.path) } /// relativize returns the given project path relative to the root folder of the @@ -240,15 +283,11 @@ impl RepositoryEntry { /// If the root of the repository (and its .git folder) are located in a parent folder /// of the project root folder, then the returned RepoPath is relative to the root /// of the repository and not a valid path inside the project. - pub fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result { + pub fn relativize(&self, path: &Path) -> Result { let relativize_path = |path: &Path| { - let entry = worktree - .entry_for_id(self.work_directory.0) - .ok_or_else(|| anyhow!("entry not found"))?; - let relativized_path = path - .strip_prefix(&entry.path) - .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?; + .strip_prefix(&self.path) + .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))?; Ok(relativized_path.into()) }; @@ -261,30 +300,16 @@ impl RepositoryEntry { } } -impl From<&RepositoryEntry> for proto::RepositoryEntry { - fn from(value: &RepositoryEntry) -> Self { - proto::RepositoryEntry { - work_directory_id: value.work_directory.to_proto(), - branch: value.branch.as_ref().map(|str| str.to_string()), - } - } -} - -/// This path corresponds to the 'content path' of a repository in relation -/// to Zed's project root. -/// In the majority of the cases, this is the folder that contains the .git folder. -/// But if a sub-folder of a git repository is opened, this corresponds to the -/// project root and the .git folder is located in a parent directory. -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct RepositoryWorkDirectory(pub(crate) Arc); - -impl Default for RepositoryWorkDirectory { +impl Default for WorkDirectory { fn default() -> Self { - RepositoryWorkDirectory(Arc::from(Path::new(""))) + Self { + path: Arc::from(Path::new("")), + location_in_repo: None, + } } } -impl Deref for RepositoryWorkDirectory { +impl Deref for WorkDirectory { type Target = Path; fn deref(&self) -> &Self::Target { @@ -292,9 +317,9 @@ impl Deref for RepositoryWorkDirectory { } } -impl AsRef for RepositoryWorkDirectory { +impl AsRef for WorkDirectory { fn as_ref(&self) -> &Path { - self.0.as_ref() + self.path.as_ref() } } @@ -346,6 +371,7 @@ struct BackgroundScannerState { #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { + pub(crate) work_directory: WorkDirectory, pub(crate) git_dir_scan_id: usize, pub(crate) repo_ptr: Arc, /// Absolute path to the actual .git folder. @@ -355,12 +381,39 @@ pub struct LocalRepositoryEntry { pub(crate) dot_git_worktree_abs_path: Option>, } +impl sum_tree::Item for LocalRepositoryEntry { + type Summary = PathSummary; + + fn summary(&self, _: &::Context) -> Self::Summary { + PathSummary { + max_path: self.work_directory.path.clone(), + item_summary: Nothing, + } + } +} + +impl KeyedItem for LocalRepositoryEntry { + type Key = PathKey; + + fn key(&self) -> Self::Key { + PathKey(self.work_directory.path.clone()) + } +} + impl LocalRepositoryEntry { pub fn repo(&self) -> &Arc { &self.repo_ptr } } +impl Deref for LocalRepositoryEntry { + type Target = WorkDirectory; + + fn deref(&self) -> &Self::Target { + &self.work_directory + } +} + impl Deref for LocalSnapshot { type Target = Snapshot; @@ -745,9 +798,9 @@ impl Worktree { let snapshot = this.snapshot(); cx.background_executor().spawn(async move { if let Some(repo) = snapshot.repository_for_path(&path) { - if let Some(repo_path) = repo.relativize(&snapshot, &path).log_err() { + if let Some(repo_path) = repo.relativize(&path).log_err() { if let Some(git_repo) = - snapshot.git_repositories.get(&*repo.work_directory) + snapshot.git_repositories.get(&repo.work_directory_id) { return Ok(git_repo.repo_ptr.load_index_text(&repo_path)); } @@ -1263,7 +1316,7 @@ impl LocalWorktree { if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id { if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) { let old_repo = old_snapshot - .repository_entries + .repositories .get(&PathKey(entry.path.clone()), &()) .cloned(); changes.push(( @@ -1280,8 +1333,8 @@ impl LocalWorktree { Ordering::Greater => { if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) { let old_repo = old_snapshot - .repository_entries - .get(&RepositoryWorkDirectory(entry.path.clone()), &()) + .repositories + .get(&PathKey(entry.path.clone()), &()) .cloned(); changes.push(( entry.path.clone(), @@ -1308,8 +1361,8 @@ impl LocalWorktree { (None, Some((entry_id, _))) => { if let Some(entry) = old_snapshot.entry_for_id(entry_id) { let old_repo = old_snapshot - .repository_entries - .get(&RepositoryWorkDirectory(entry.path.clone()), &()) + .repositories + .get(&PathKey(entry.path.clone()), &()) .cloned(); changes.push(( entry.path.clone(), @@ -1354,12 +1407,12 @@ impl LocalWorktree { } pub fn local_git_repo(&self, path: &Path) -> Option> { - self.repo_for_path(path) - .map(|fields| fields.local_entry.repo_ptr.clone()) + self.local_repo_for_path(path) + .map(|local_repo| local_repo.repo_ptr.clone()) } pub fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { - self.git_repositories.get(&repo.work_directory.0) + self.git_repositories.get(&repo.work_directory_id) } fn load_binary_file( @@ -2115,7 +2168,7 @@ impl Snapshot { always_included_entries: Default::default(), entries_by_path: Default::default(), entries_by_id: Default::default(), - repository_entries: Default::default(), + repositories: Default::default(), scan_id: 1, completed_scan_id: 0, } @@ -2150,8 +2203,8 @@ impl Snapshot { updated_entries.sort_unstable_by_key(|e| e.id); let mut updated_repositories = self - .repository_entries - .values() + .repositories + .iter() .map(proto::RepositoryEntry::from) .collect::>(); updated_repositories.sort_unstable_by_key(|e| e.work_directory_id); @@ -2231,16 +2284,10 @@ impl Snapshot { Some(removed_entry.path) } - pub fn git_status<'a>(&'a self, work_dir: &'a impl AsRef) -> Option> { - let path = work_dir.as_ref(); - self.repository_for_path(&path) - .map(|repo| repo.git_entries_by_path.iter().cloned().collect()) - } - pub fn status_for_file(&self, path: impl AsRef) -> Option { let path = path.as_ref(); self.repository_for_path(path).and_then(|repo| { - let repo_path = repo.relativize(self, path).unwrap(); + let repo_path = repo.relativize(path).unwrap(); repo.git_entries_by_path .get(&PathKey(repo_path.0), &()) .map(|entry| entry.git_status) @@ -2304,36 +2351,40 @@ impl Snapshot { self.entries_by_id.edit(entries_by_id_edits, &()); update.removed_repositories.sort_unstable(); - self.repository_entries.retain(|_, entry| { + self.repositories.retain(&(), |entry: &RepositoryEntry| { update .removed_repositories - .binary_search(&entry.work_directory.to_proto()) + .binary_search(&entry.work_directory_id.to_proto()) .is_err() }); for repository in update.updated_repositories { - let work_directory_entry: WorkDirectoryEntry = - ProjectEntryId::from_proto(repository.work_directory_id).into(); - - if let Some(entry) = self.entry_for_id(*work_directory_entry) { - let work_directory = RepositoryWorkDirectory(entry.path.clone()); - if self.repository_entries.get(&work_directory, &()).is_some() { - self.repository_entries.update(&work_directory, |repo| { - repo.branch = repository.branch.map(Into::into); - }); + let work_directory_id = ProjectEntryId::from_proto(repository.work_directory_id); + if let Some(work_dir_entry) = self.entry_for_id(work_directory_id) { + if self + .repositories + .contains(&PathKey(work_dir_entry.path.clone()), &()) + { + self.repositories + .update(&PathKey(work_dir_entry.path.clone()), &(), |repo| { + repo.branch = repository.branch.map(Into::into); + }); } else { - self.repository_entries.insert( - work_directory, + self.repositories.insert_or_replace( RepositoryEntry { - work_directory: work_directory_entry, + work_directory_id, + work_directory: WorkDirectory { + path: work_dir_entry.path.clone(), + // When syncing repository entries from a peer, we don't need + // the location_in_repo field, since git operations don't happen locally + // anyway. + location_in_repo: None, + }, branch: repository.branch.map(Into::into), git_entries_by_path: Default::default(), - // When syncing repository entries from a peer, we don't need - // the location_in_repo field, since git operations don't happen locally - // anyway. - location_in_repo: None, }, - ) + &(), + ); } } else { log::error!("no work directory entry for repository {:?}", repository) @@ -2428,35 +2479,29 @@ impl Snapshot { self.traverse_from_offset(true, true, include_ignored, start) } - pub fn repositories( - &self, - ) -> impl Iterator { - self.repository_entries.self.repository_entries.iter() + #[cfg(any(feature = "test-support", test))] + pub fn git_satus(&self, work_dir: &Path) -> Option> { + self.repositories + .get(&PathKey(work_dir.into()), &()) + .map(|repo| repo.status().collect()) } - /// Get the repository whose work directory contains the given path. - pub fn repository_for_work_directory( - &self, - path: &RepositoryWorkDirectory, - ) -> Option { - self.repository_entries.get(path, &()).cloned() + pub fn repositories(&self) -> impl Iterator { + self.repositories.iter() } - /// Get the repository whose work directory contains the given path. - pub fn repository_for_path(&self, path: &Path) -> Option { - self.repository_and_work_directory_for_path(path) - .map(|e| e.1) + /// Get the repository whose work directory corresponds to the given path. + pub(crate) fn repository(&self, work_directory: PathKey) -> Option { + self.repositories.get(&work_directory, &()).cloned() } - pub fn repository_and_work_directory_for_path( - &self, - path: &Path, - ) -> Option<(RepositoryWorkDirectory, RepositoryEntry)> { - self.repository_entries + /// Get the repository whose work directory contains the given path. + pub fn repository_for_path(&self, path: &Path) -> Option { + self.repositories .iter() - .filter(|(workdir_path, _)| path.starts_with(workdir_path)) + .filter(|repo| repo.contains(path)) .last() - .map(|(path, repo)| (path.clone(), repo.clone())) + .map(|repo| repo.clone()) } /// Given an ordered iterator of entries, returns an iterator of those entries, @@ -2465,24 +2510,24 @@ impl Snapshot { &'a self, entries: impl 'a + Iterator, ) -> impl 'a + Iterator)> { - let mut containing_repos = Vec::<(&RepositoryWorkDirectory, &RepositoryEntry)>::new(); + let mut containing_repos = Vec::<&RepositoryEntry>::new(); let mut repositories = self.repositories().peekable(); entries.map(move |entry| { - while let Some((repo_path, _)) = containing_repos.last() { - if entry.path.starts_with(repo_path) { + while let Some(repository) = containing_repos.last() { + if repository.contains(&entry.path) { break; } else { containing_repos.pop(); } } - while let Some((repo_path, _)) = repositories.peek() { - if entry.path.starts_with(repo_path) { + while let Some(repository) = repositories.peek() { + if repository.contains(&entry.path) { containing_repos.push(repositories.next().unwrap()); } else { break; } } - let repo = containing_repos.last().map(|(_, repo)| *repo); + let repo = containing_repos.last().copied(); (entry, repo) }) } @@ -2498,9 +2543,9 @@ impl Snapshot { return None; } - let (_, repo_entry) = self.repository_and_work_directory_for_path(&entry.path)?; - let RepoPath(path) = repo_entry.relativize(self, &entry.path).ok()?; - let git_entry = repo_entry.git_entries_by_path.get(&PathKey(path), &())?; + let repository = self.repository_for_path(&entry.path)?; + let RepoPath(path) = repository.relativize(&entry.path).ok()?; + let git_entry = repository.git_entries_by_path.get(&PathKey(path), &())?; Some(git_entry.git_status) }) .collect::>(); @@ -2589,19 +2634,19 @@ impl Snapshot { } pub fn root_git_entry(&self) -> Option { - self.repository_entries + self.repositories .get(&PathKey(Path::new("").into()), &()) .map(|entry| entry.to_owned()) } pub fn git_entry(&self, work_directory_path: Arc) -> Option { - self.repository_entries + self.repositories .get(&PathKey(work_directory_path), &()) .map(|entry| entry.to_owned()) } pub fn git_entries(&self) -> impl Iterator { - self.repository_entries.iter() + self.repositories.iter() } pub fn scan_id(&self) -> usize { @@ -2631,24 +2676,11 @@ impl Snapshot { } } -// TODO: Bad name -> Bad code structure, refactor to remove this type -pub struct LocalRepositoryFields<'a> { - pub work_directory: RepositoryWorkDirectory, - pub repository_entry: RepositoryEntry, - pub local_entry: &'a LocalRepositoryEntry, -} - impl LocalSnapshot { - pub fn repo_for_path(&self, path: &Path) -> Option> { - let (work_directory, repository_entry) = - self.repository_and_work_directory_for_path(path)?; + pub fn local_repo_for_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> { + let repository_entry = self.repository_for_path(path)?; let work_directory_id = repository_entry.work_directory_id(); - - Some(LocalRepositoryFields { - work_directory, - repository_entry, - local_entry: self.git_repositories.get(&work_directory_id)?, - }) + self.git_repositories.get(&work_directory_id) } fn build_update( @@ -2672,7 +2704,7 @@ impl LocalSnapshot { } for (work_dir_path, change) in repo_changes.iter() { - let new_repo = self.repository_entries.get(&PathKey(work_dir_path.clone())); + let new_repo = self.repositories.get(&PathKey(work_dir_path.clone()), &()); match (&change.old_repository, new_repo) { (Some(old_repo), Some(new_repo)) => { updated_repositories.push(new_repo.build_update(old_repo)); @@ -2681,7 +2713,7 @@ impl LocalSnapshot { updated_repositories.push(proto::RepositoryEntry::from(new_repo)); } (Some(old_repo), None) => { - removed_repositories.push(old_repo.work_directory.0.to_proto()); + removed_repositories.push(old_repo.work_directory_id.to_proto()); } _ => {} } @@ -2884,15 +2916,15 @@ impl LocalSnapshot { .map(|repo| repo.1.dot_git_dir_abs_path.clone()) .collect::>(); let work_dir_paths = self - .repository_entries + .repositories .iter() - .map(|repo| repo.0.clone().0) + .map(|repo| repo.work_directory.path.clone()) .collect::>(); assert_eq!(dotgit_paths.len(), work_dir_paths.len()); - assert_eq!(self.repository_entries.iter().count(), work_dir_paths.len()); + assert_eq!(self.repositories.iter().count(), work_dir_paths.len()); assert_eq!(self.git_repositories.iter().count(), work_dir_paths.len()); - for (_, entry) in self.repository_entries.iter() { - self.git_repositories.get(&entry.work_directory).unwrap(); + for entry in self.repositories.iter() { + self.git_repositories.get(&entry.work_directory_id).unwrap(); } } @@ -3093,9 +3125,9 @@ impl BackgroundScannerState { self.snapshot .git_repositories .retain(|id, _| removed_ids.binary_search(id).is_err()); - self.snapshot - .repository_entries - .retain(|repo_path, _| !repo_path.0.starts_with(path)); + self.snapshot.repositories.retain(&(), |repository| { + !repository.work_directory.starts_with(path) + }); #[cfg(test)] self.snapshot.check_invariants(false); @@ -3106,7 +3138,7 @@ impl BackgroundScannerState { dot_git_path: Arc, fs: &dyn Fs, watcher: &dyn Watcher, - ) -> Option<(RepositoryWorkDirectory, Arc)> { + ) -> Option { let work_dir_path: Arc = match dot_git_path.parent() { Some(parent_dir) => { // Guard against repositories inside the repository metadata @@ -3142,7 +3174,7 @@ impl BackgroundScannerState { location_in_repo: Option>, fs: &dyn Fs, watcher: &dyn Watcher, - ) -> Option<(RepositoryWorkDirectory, Arc)> { + ) -> Option { let work_dir_id = self .snapshot .entry_for_path(work_dir_path.clone()) @@ -3174,7 +3206,10 @@ impl BackgroundScannerState { }; log::trace!("constructed libgit2 repo in {:?}", t0.elapsed()); - let work_directory = RepositoryWorkDirectory(work_dir_path.clone()); + let work_directory = WorkDirectory { + path: work_dir_path.clone(), + location_in_repo, + }; if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() { git_hosting_providers::register_additional_providers( @@ -3183,26 +3218,29 @@ impl BackgroundScannerState { ); } - self.snapshot.repository_entries.insert( - work_directory.clone(), + self.snapshot.repositories.insert_or_replace( RepositoryEntry { - work_directory: work_dir_id.into(), + work_directory_id: work_dir_id.into(), + work_directory: work_directory.clone(), branch: repository.branch_name().map(Into::into), git_entries_by_path: Default::default(), - location_in_repo, - }, - ); - self.snapshot.git_repositories.insert( - work_dir_id, - LocalRepositoryEntry { - git_dir_scan_id: 0, - repo_ptr: repository.clone(), - dot_git_dir_abs_path: actual_dot_git_dir_abs_path, - dot_git_worktree_abs_path, }, + &(), ); - Some((work_directory, repository)) + let local_repository = LocalRepositoryEntry { + work_directory: work_directory.clone(), + git_dir_scan_id: 0, + repo_ptr: repository.clone(), + dot_git_dir_abs_path: actual_dot_git_dir_abs_path, + dot_git_worktree_abs_path, + }; + + self.snapshot + .git_repositories + .insert(work_dir_id, local_repository.clone()); + + Some(local_repository) } } @@ -3558,24 +3596,24 @@ pub struct GitEntry { struct PathItem(I); #[derive(Clone, Debug)] -struct PathSummary { +pub struct PathSummary { max_path: Arc, item_summary: S, } impl Summary for PathSummary { - type Context = (); + type Context = S::Context; fn zero(cx: &Self::Context) -> Self { Self { max_path: Path::new("").into(), - item_summary: S::zero(&()), + item_summary: S::zero(cx), } } fn add_summary(&mut self, rhs: &Self, cx: &Self::Context) { self.max_path = rhs.max_path.clone(); - self.item_summary.add_summary(&rhs.item_summary, &()); + self.item_summary.add_summary(&rhs.item_summary, cx); } } @@ -3595,7 +3633,7 @@ where // This type exists because we can't implement Summary for () #[derive(Clone)] -struct Nothing; +pub struct Nothing; impl Summary for Nothing { type Context = (); @@ -3612,7 +3650,7 @@ impl sum_tree::Item for RepositoryEntry { fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { - max_path: self.work_directory.0.clone(), + max_path: self.work_directory.path.clone(), item_summary: Nothing, } } @@ -3622,7 +3660,7 @@ impl sum_tree::KeyedItem for RepositoryEntry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.work_directory.0.clone()) + PathKey(self.work_directory.path.clone()) } } @@ -3633,8 +3671,8 @@ impl sum_tree::Summary for GitStatuses { Default::default() } - fn add_summary(&mut self, rhs: &Self, cx: &Self::Context) { - *self += rhs; + fn add_summary(&mut self, rhs: &Self, _: &Self::Context) { + *self += *rhs; } } @@ -3676,7 +3714,7 @@ impl sum_tree::KeyedItem for GitEntry { } #[derive(Clone, Debug, Default, Copy)] -struct GitStatuses { +pub struct GitStatuses { added: usize, modified: usize, conflict: usize, @@ -3729,21 +3767,21 @@ impl<'a> sum_tree::Dimension<'a, PathSummary> for GitStatuses { } impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for PathKey { - fn zero(_: &()) -> Self { + fn zero(_: &S::Context) -> Self { Default::default() } - fn add_summary(&mut self, summary: &'a PathSummary, _: &()) { + fn add_summary(&mut self, summary: &'a PathSummary, _: &S::Context) { self.0 = summary.max_path.clone(); } } impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for TraversalProgress<'a> { - fn zero(_cx: &()) -> Self { + fn zero(_cx: &S::Context) -> Self { Default::default() } - fn add_summary(&mut self, summary: &'a PathSummary, _: &()) { + fn add_summary(&mut self, summary: &'a PathSummary, _: &S::Context) { self.max_path = summary.max_path.as_ref(); } } @@ -3772,7 +3810,8 @@ impl<'a> PathSummaryTraversalTarget<'a> { } fn cmp_path(&self, other: &Path) -> std::cmp::Ordering { - self.cmp( + SeekTarget::, TraversalProgress>::cmp( + self, &TraversalProgress { max_path: other, ..Default::default() @@ -3785,7 +3824,7 @@ impl<'a> PathSummaryTraversalTarget<'a> { impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary, TraversalProgress<'a>> for PathSummaryTraversalTarget<'b> { - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { + fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &S::Context) -> Ordering { match self { PathSummaryTraversalTarget::Path(path) => path.cmp(&cursor_location.max_path), PathSummaryTraversalTarget::PathSuccessor(path) => { @@ -3819,7 +3858,7 @@ impl<'a, 'b> SeekTarget<'a, PathSummary, (TraversalProgress<'a>, Gi struct AllStatusesCursor<'a, I> { repos: I, current_location: Option<( - &'a RepositoryWorkDirectory, + &'a WorkDirectory, Cursor<'a, GitEntry, (TraversalProgress<'a>, GitStatuses)>, )>, statuses_before_current_repo: GitStatuses, @@ -3827,19 +3866,19 @@ struct AllStatusesCursor<'a, I> { impl<'a, I> AllStatusesCursor<'a, I> where - I: Iterator + FusedIterator, + I: Iterator + FusedIterator, { fn seek_forward(&mut self, target: &PathSummaryTraversalTarget<'_>) { loop { let (work_dir, cursor) = match &mut self.current_location { Some(location) => location, None => { - let Some((work_dir, entry)) = self.repos.next() else { + let Some(entry) = self.repos.next() else { break; }; self.current_location.insert(( - work_dir, + &entry.work_directory, entry .git_entries_by_path .cursor::<(TraversalProgress<'_>, GitStatuses)>(&()), @@ -3847,13 +3886,13 @@ where } }; - if let Some(repo_path) = target.path().strip_prefix(&work_dir.0).ok() { + if let Some(repo_path) = work_dir.relativize(target.path()).ok() { let target = &target.with_path(&repo_path); cursor.seek_forward(target, Bias::Left, &()); if let Some(_) = cursor.item() { break; } - } else if target.cmp_path(&work_dir.0).is_gt() { + } else if target.cmp_path(&work_dir.path).is_gt() { // Fill the cursor with everything from this intermediary repository cursor.seek_forward(target, Bias::Right, &()); } else { @@ -3876,10 +3915,7 @@ where fn all_statuses_cursor<'a>( snapshot: &'a Snapshot, -) -> AllStatusesCursor< - 'a, - impl Iterator + FusedIterator, -> { +) -> AllStatusesCursor<'a, impl Iterator + FusedIterator> { let repos = snapshot.repositories().fuse(); AllStatusesCursor { repos, @@ -4623,11 +4659,9 @@ impl BackgroundScanner { self.watcher.as_ref(), ); - if let Some((work_directory, repository)) = repo { + if let Some(local_repo) = repo { self.update_git_statuses(UpdateGitStatusesJob { - location_in_repo: None, - work_directory, - repository, + local_repository: local_repo, }); } } else if child_name == *GITIGNORE { @@ -4836,17 +4870,12 @@ impl BackgroundScanner { // Group all relative paths by their git repository. let mut paths_by_git_repo = HashMap::default(); for relative_path in relative_paths.iter() { - if let Some(LocalRepositoryFields { - work_directory, - repository_entry, - local_entry, - }) = state.snapshot.repo_for_path(relative_path) - { - if let Ok(repo_path) = repository_entry.relativize(&state.snapshot, relative_path) { + if let Some(local_repo) = state.snapshot.local_repo_for_path(relative_path) { + if let Ok(repo_path) = local_repo.relativize(relative_path) { paths_by_git_repo - .entry(work_directory) + .entry(local_repo.work_directory.clone()) .or_insert_with(|| RepoPaths { - repo: local_entry.repo_ptr.clone(), + repo: local_repo.repo_ptr.clone(), repo_paths: Default::default(), }) .add_path(repo_path); @@ -4868,8 +4897,8 @@ impl BackgroundScanner { for path in paths.repo_paths { changed_path_statuses.push(Edit::Remove(PathKey(path.0))); } - state.snapshot.repository_entries.update( - &PathKey(work_directory.0), + state.snapshot.repositories.update( + &work_directory.path_key(), &(), move |repository_entry| { repository_entry @@ -4920,10 +4949,7 @@ impl BackgroundScanner { state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); } Ok(None) => { - self.remove_repo_path( - &RepositoryWorkDirectory(path.clone()), - &mut state.snapshot, - ); + self.remove_repo_path(path, &mut state.snapshot); } Err(err) => { log::error!("error reading file {abs_path:?} on event: {err:#}"); @@ -4939,20 +4965,19 @@ impl BackgroundScanner { ); } - fn remove_repo_path( - &self, - path: &RepositoryWorkDirectory, - snapshot: &mut LocalSnapshot, - ) -> Option<()> { + fn remove_repo_path(&self, path: &Arc, snapshot: &mut LocalSnapshot) -> Option<()> { if !path - .0 .components() .any(|component| component.as_os_str() == *DOT_GIT) { - if let Some(repository) = snapshot.repository_for_work_directory(path) { - let entry = repository.work_directory.0; - snapshot.git_repositories.remove(&entry); - snapshot.snapshot.repository_entries.remove(path, &()); + if let Some(repository) = snapshot.repository(PathKey(path.clone())) { + snapshot + .git_repositories + .remove(&repository.work_directory_id); + snapshot + .snapshot + .repositories + .remove(&PathKey(repository.work_directory.path.clone()), &()); return Some(()); } } @@ -5135,7 +5160,7 @@ impl BackgroundScanner { } }); - let (work_directory, repository) = match existing_repository_entry { + let local_repository = match existing_repository_entry { None => { match state.insert_git_repository( dot_git_dir.into(), @@ -5146,45 +5171,36 @@ impl BackgroundScanner { None => continue, } } - Some((entry_id, repository)) => { - if repository.git_dir_scan_id == scan_id { + Some((entry_id, local_repository)) => { + if local_repository.git_dir_scan_id == scan_id { continue; } let Some(work_dir) = state .snapshot .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + .map(|entry| entry.path.clone()) else { continue; }; - let repo = &repository.repo_ptr; - let branch = repo.branch_name(); - repo.reload_index(); + let branch = local_repository.repo_ptr.branch_name(); + local_repository.repo_ptr.reload_index(); state .snapshot .git_repositories .update(&entry_id, |entry| entry.git_dir_scan_id = scan_id); - state - .snapshot - .snapshot - .repository_entries - .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - (work_dir, repository.repo_ptr.clone()) + state.snapshot.snapshot.repositories.update( + &PathKey(work_dir.clone()), + &(), + |entry| entry.branch = branch.map(Into::into), + ); + + local_repository } }; - repo_updates.push(UpdateGitStatusesJob { - location_in_repo: state - .snapshot - .repository_entries - .get(&work_directory, &()) - .and_then(|repo| repo.location_in_repo.clone()) - .clone(), - work_directory, - repository, - }); + repo_updates.push(UpdateGitStatusesJob { local_repository }); } // Remove any git repositories whose .git entry no longer exists. @@ -5210,9 +5226,9 @@ impl BackgroundScanner { snapshot .git_repositories .retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id)); - snapshot - .repository_entries - .retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0)); + snapshot.repositories.retain(&(), |entry| { + ids_to_preserve.contains(&entry.work_directory_id) + }); } let (mut updates_done_tx, mut updates_done_rx) = barrier::channel(); @@ -5246,11 +5262,15 @@ impl BackgroundScanner { /// Update the git statuses for a given batch of entries. fn update_git_statuses(&self, job: UpdateGitStatusesJob) { - log::trace!("updating git statuses for repo {:?}", job.work_directory.0); + log::trace!( + "updating git statuses for repo {:?}", + job.local_repository.work_directory.path + ); let t0 = Instant::now(); let Some(statuses) = job - .repository + .local_repository + .repo() .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()]) .log_err() else { @@ -5258,7 +5278,7 @@ impl BackgroundScanner { }; log::trace!( "computed git statuses for repo {:?} in {:?}", - job.work_directory.0, + job.local_repository.work_directory.path, t0.elapsed() ); @@ -5266,8 +5286,8 @@ impl BackgroundScanner { let mut changed_paths = Vec::new(); let snapshot = self.state.lock().snapshot.snapshot.clone(); - let Some(mut repository_entry) = - snapshot.repository_for_work_directory(&job.work_directory) + let Some(mut repository) = + snapshot.repository(job.local_repository.work_directory.path_key()) else { log::error!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot"); debug_assert!(false); @@ -5276,13 +5296,14 @@ impl BackgroundScanner { let mut new_entries_by_path = SumTree::new(&()); for (path, status) in statuses.entries.iter() { - let project_path: Option> = if let Some(location) = &job.location_in_repo { - // If we fail to strip the prefix, that means this status entry is - // external to this worktree, and we definitely won't have an entry_id - path.strip_prefix(location).ok().map(Into::into) - } else { - Some(job.work_directory.0.join(path).into()) - }; + let project_path: Option> = + if let Some(location) = &job.local_repository.location_in_repo { + // If we fail to strip the prefix, that means this status entry is + // external to this worktree, and we definitely won't have an entry_id + path.strip_prefix(location).ok().map(Into::into) + } else { + Some(job.local_repository.work_directory.path.join(path).into()) + }; new_entries_by_path.insert_or_replace( GitEntry { @@ -5297,12 +5318,12 @@ impl BackgroundScanner { } } - repository_entry.git_entries_by_path = new_entries_by_path; + repository.git_entries_by_path = new_entries_by_path; let mut state = self.state.lock(); state .snapshot - .repository_entries - .insert(job.work_directory.clone(), repository_entry); + .repositories + .insert_or_replace(repository, &()); util::extend_sorted( &mut state.changed_paths, @@ -5313,7 +5334,7 @@ impl BackgroundScanner { log::trace!( "applied git status updates for repo {:?} in {:?}", - job.work_directory.0, + job.local_repository.work_directory.path, t0.elapsed(), ); } @@ -5525,9 +5546,7 @@ struct UpdateIgnoreStatusJob { } struct UpdateGitStatusesJob { - work_directory: RepositoryWorkDirectory, - location_in_repo: Option>, - repository: Arc, + local_repository: LocalRepositoryEntry, } pub trait WorktreeModelHandle { @@ -5702,7 +5721,7 @@ impl<'a> Default for TraversalProgress<'a> { pub struct GitTraversal<'a> { // TODO - // statuses: Cursor<> + // statuses: I, traversal: Traversal<'a>, } @@ -5947,7 +5966,6 @@ impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry { is_ignored: entry.is_ignored, is_always_included: always_included.is_match(path.as_ref()), is_external: entry.is_external, - // git_status: git_status_from_proto(entry.git_status), is_private: false, char_bag, is_fifo: entry.is_fifo, diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index ff7c74dcd90d4..d765416ac9f5f 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1498,8 +1498,6 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); - dbg!(snapshot.git_status(&Path::new(""))); - check_propagated_statuses( &snapshot, &[ @@ -2181,8 +2179,8 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { cx.read(|cx| { let tree = tree.read(cx); - let (work_dir, _) = tree.repositories().next().unwrap(); - assert_eq!(work_dir.as_ref(), Path::new("projects/project1")); + let repo = tree.repositories().next().unwrap(); + assert_eq!(repo.path.as_ref(), Path::new("projects/project1")); assert_eq!( tree.status_for_file(Path::new("projects/project1/a")), Some(GitFileStatus::Modified) @@ -2202,8 +2200,8 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { cx.read(|cx| { let tree = tree.read(cx); - let (work_dir, _) = tree.repositories().next().unwrap(); - assert_eq!(work_dir.as_ref(), Path::new("projects/project2")); + let repo = tree.repositories().next().unwrap(); + assert_eq!(repo.path.as_ref(), Path::new("projects/project2")); assert_eq!( tree.status_for_file(Path::new("projects/project2/a")), Some(GitFileStatus::Modified) @@ -2256,23 +2254,13 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { assert!(tree.repository_for_path("c.txt".as_ref()).is_none()); - let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap(); - assert_eq!( - entry - .work_directory(tree) - .map(|directory| directory.as_ref().to_owned()), - Some(Path::new("dir1").to_owned()) - ); + let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap(); + assert_eq!(repo.path.as_ref(), Path::new("dir1")); - let entry = tree + let repo = tree .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref()) .unwrap(); - assert_eq!( - entry - .work_directory(tree) - .map(|directory| directory.as_ref().to_owned()), - Some(Path::new("dir1/deps/dep1").to_owned()) - ); + assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1")); let entries = tree.files(false, 0); @@ -2281,10 +2269,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { .map(|(entry, repo)| { ( entry.path.as_ref(), - repo.and_then(|repo| { - repo.work_directory(tree) - .map(|work_directory| work_directory.0.to_path_buf()) - }), + repo.map(|repo| repo.path.to_path_buf()), ) }) .collect::>(); @@ -2396,8 +2381,8 @@ async fn test_git_status(cx: &mut TestAppContext) { tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); assert_eq!(snapshot.repositories().count(), 1); - let (dir, repo_entry) = snapshot.repositories().next().unwrap(); - assert_eq!(dir.as_ref(), Path::new("project")); + let repo_entry = snapshot.repositories().next().unwrap(); + assert_eq!(repo_entry.path.as_ref(), Path::new("project")); assert!(repo_entry.location_in_repo.is_none()); assert_eq!( @@ -2465,7 +2450,6 @@ async fn test_git_status(cx: &mut TestAppContext) { Some(GitFileStatus::Modified) ); }); - dbg!("***********************************"); std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); std::fs::remove_dir_all(work_dir.join("c")).unwrap(); @@ -2570,9 +2554,8 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { // Check that the right git state is observed on startup tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - let (dir, _) = snapshot.repositories().next().unwrap(); - - let entries = snapshot.git_status(dir).unwrap(); + let repo = snapshot.repositories().next().unwrap(); + let entries = repo.status().collect::>(); assert_eq!(entries.len(), 3); assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); @@ -2593,10 +2576,8 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - let (dir, _) = snapshot.repositories().next().unwrap(); - - // Takes a work directory, and returns all file entries with a git status. - let entries = snapshot.git_status(dir).unwrap(); + let repository = snapshot.repositories().next().unwrap(); + let entries = repository.status().collect::>(); std::assert_eq!(entries.len(), 4, "entries: {entries:?}"); assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); @@ -2628,8 +2609,8 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - let (dir, _) = snapshot.repositories().next().unwrap(); - let entries = snapshot.git_status(dir).unwrap(); + let repo = snapshot.repositories().next().unwrap(); + let entries = repo.status().collect::>(); // Deleting an untracked entry, b.txt, should leave no status // a.txt was tracked, and so should have a status @@ -2696,15 +2677,15 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); assert_eq!(snapshot.repositories().count(), 1); - let (dir, repo_entry) = snapshot.repositories().next().unwrap(); + let repo = snapshot.repositories().next().unwrap(); // Path is blank because the working directory of // the git repository is located at the root of the project - assert_eq!(dir.as_ref(), Path::new("")); + assert_eq!(repo.path.as_ref(), Path::new("")); // This is the missing path between the root of the project (sub-folder-2) and its // location relative to the root of the repository. assert_eq!( - repo_entry.location_in_repo, + repo.location_in_repo, Some(Arc::from(Path::new("sub-folder-1/sub-folder-2"))) ); From 108a53affcc8fbac983e2bdb89f37c32b3a5a105 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 22 Dec 2024 01:08:33 -0800 Subject: [PATCH 19/22] Take a first pass at implementing the git status join APIs feel good, concept feels good.... now we just need to make it work. --- crates/sum_tree/src/sum_tree.rs | 6 +- crates/worktree/src/worktree.rs | 403 ++++++++++++++++---------- crates/worktree/src/worktree_tests.rs | 124 +++++++- 3 files changed, 376 insertions(+), 157 deletions(-) diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 9c661ed39f5cb..fa37c6759948d 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -45,13 +45,13 @@ pub trait Summary: Clone { /// This type exists because we can't implement Summary for () without causing /// type resolution errors #[derive(Copy, Clone, PartialEq, Eq, Debug)] -struct Nothing; +pub struct Unit; -impl Summary for Nothing { +impl Summary for Unit { type Context = (); fn zero(_: &()) -> Self { - Nothing + Unit } fn add_summary(&mut self, _: &Self, _: &()) {} diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 33a1b2f71352e..a92e95aedca69 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -64,7 +64,9 @@ use std::{ }, time::{Duration, Instant}, }; -use sum_tree::{Bias, Cursor, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; +use sum_tree::{ + Bias, Cursor, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit, +}; use text::{LineEnding, Rope}; use util::{ paths::{home_dir, PathMatcher, SanitizedPath}, @@ -284,18 +286,14 @@ impl WorkDirectory { /// of the project root folder, then the returned RepoPath is relative to the root /// of the repository and not a valid path inside the project. pub fn relativize(&self, path: &Path) -> Result { - let relativize_path = |path: &Path| { + if let Some(location_in_repo) = &self.location_in_repo { + Ok(location_in_repo.join(path).into()) + } else { let relativized_path = path .strip_prefix(&self.path) .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))?; Ok(relativized_path.into()) - }; - - if let Some(location_in_repo) = &self.location_in_repo { - relativize_path(&location_in_repo.join(path)) - } else { - relativize_path(path) } } } @@ -382,12 +380,12 @@ pub struct LocalRepositoryEntry { } impl sum_tree::Item for LocalRepositoryEntry { - type Summary = PathSummary; + type Summary = PathSummary; fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { max_path: self.work_directory.path.clone(), - item_summary: Nothing, + item_summary: Unit, } } } @@ -2268,7 +2266,7 @@ impl Snapshot { self.entries_by_path = { let mut cursor = self.entries_by_path.cursor::(&()); let mut new_entries_by_path = - cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &()); + cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left, &()); while let Some(entry) = cursor.item() { if entry.path.starts_with(&removed_entry.path) { self.entries_by_id.remove(&entry.id, &()); @@ -2444,6 +2442,7 @@ impl Snapshot { &(), ); Traversal { + repositories: &self.repositories, cursor, include_files, include_dirs, @@ -2459,6 +2458,7 @@ impl Snapshot { path: &Path, ) -> Traversal { Traversal::new( + &self.repositories, &self.entries_by_path, include_files, include_dirs, @@ -2569,9 +2569,7 @@ impl Snapshot { }; if let Some((entry_ix, prev_statuses)) = entry_to_finish { - cursor.seek_forward(&PathSummaryTraversalTarget::PathSuccessor( - &entries[entry_ix].path, - )); + cursor.seek_forward(&PathTarget::Successor(&entries[entry_ix].path)); let statuses = cursor.start() - prev_statuses; @@ -2586,7 +2584,7 @@ impl Snapshot { }; } else { if entries[entry_ix].is_dir() { - cursor.seek_forward(&PathSummaryTraversalTarget::Path(&entries[entry_ix].path)); + cursor.seek_forward(&PathTarget::Path(&entries[entry_ix].path)); entry_stack.push((entry_ix, cursor.start())); } entry_ix += 1; @@ -2606,9 +2604,10 @@ impl Snapshot { pub fn child_entries<'a>(&'a self, parent_path: &'a Path) -> ChildEntriesIter<'a> { let mut cursor = self.entries_by_path.cursor(&()); - cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &()); + cursor.seek(&TraversalTarget::path(parent_path), Bias::Right, &()); let traversal = Traversal { cursor, + repositories: &self.repositories, include_files: true, include_dirs: true, include_ignored: true, @@ -3078,8 +3077,8 @@ impl BackgroundScannerState { .snapshot .entries_by_path .cursor::(&()); - new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &()); - removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &()); + new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left, &()); + removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left, &()); new_entries.append(cursor.suffix(&()), &()); } self.snapshot.entries_by_path = new_entries; @@ -3593,7 +3592,9 @@ pub struct GitEntry { } #[derive(Clone, Debug)] -struct PathItem(I); +struct PathProgress<'a> { + max_path: &'a Path, +} #[derive(Clone, Debug)] pub struct PathSummary { @@ -3617,41 +3618,29 @@ impl Summary for PathSummary { } } -impl sum_tree::Item for PathItem -where - I: sum_tree::Item + AsRef> + Clone, -{ - type Summary = PathSummary; - - fn summary(&self, cx: &::Context) -> Self::Summary { - PathSummary { - max_path: self.0.as_ref().clone(), - item_summary: self.0.summary(cx), +impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for PathProgress<'a> { + fn zero(_: & as Summary>::Context) -> Self { + Self { + max_path: Path::new(""), } } -} - -// This type exists because we can't implement Summary for () -#[derive(Clone)] -pub struct Nothing; - -impl Summary for Nothing { - type Context = (); - fn zero(_: &()) -> Self { - Nothing + fn add_summary( + &mut self, + summary: &'a PathSummary, + _: & as Summary>::Context, + ) { + self.max_path = summary.max_path.as_ref() } - - fn add_summary(&mut self, _: &Self, _: &()) {} } impl sum_tree::Item for RepositoryEntry { - type Summary = PathSummary; + type Summary = PathSummary; fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { max_path: self.work_directory.path.clone(), - item_summary: Nothing, + item_summary: Unit, } } } @@ -3786,75 +3775,6 @@ impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for TraversalProgre } } -#[derive(Clone, Copy, Debug)] -enum PathSummaryTraversalTarget<'a> { - PathSuccessor(&'a Path), - Path(&'a Path), -} - -impl<'a> PathSummaryTraversalTarget<'a> { - fn path(&self) -> &'a Path { - match self { - PathSummaryTraversalTarget::Path(path) => path, - PathSummaryTraversalTarget::PathSuccessor(path) => path, - } - } - - fn with_path(self, path: &Path) -> PathSummaryTraversalTarget<'_> { - match self { - PathSummaryTraversalTarget::PathSuccessor(_) => { - PathSummaryTraversalTarget::PathSuccessor(path) - } - PathSummaryTraversalTarget::Path(_) => PathSummaryTraversalTarget::Path(path), - } - } - - fn cmp_path(&self, other: &Path) -> std::cmp::Ordering { - SeekTarget::, TraversalProgress>::cmp( - self, - &TraversalProgress { - max_path: other, - ..Default::default() - }, - &(), - ) - } -} - -impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary, TraversalProgress<'a>> - for PathSummaryTraversalTarget<'b> -{ - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &S::Context) -> Ordering { - match self { - PathSummaryTraversalTarget::Path(path) => path.cmp(&cursor_location.max_path), - PathSummaryTraversalTarget::PathSuccessor(path) => { - if cursor_location.max_path.starts_with(path) { - Ordering::Greater - } else { - Ordering::Equal - } - } - } - } -} - -impl<'a, 'b> SeekTarget<'a, PathSummary, (TraversalProgress<'a>, GitStatuses)> - for PathSummaryTraversalTarget<'b> -{ - fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering { - match self { - PathSummaryTraversalTarget::Path(path) => path.cmp(&cursor_location.0.max_path), - PathSummaryTraversalTarget::PathSuccessor(path) => { - if cursor_location.0.max_path.starts_with(path) { - Ordering::Greater - } else { - Ordering::Equal - } - } - } - } -} - struct AllStatusesCursor<'a, I> { repos: I, current_location: Option<( @@ -3868,7 +3788,7 @@ impl<'a, I> AllStatusesCursor<'a, I> where I: Iterator + FusedIterator, { - fn seek_forward(&mut self, target: &PathSummaryTraversalTarget<'_>) { + fn seek_forward(&mut self, target: &PathTarget<'_>) { loop { let (work_dir, cursor) = match &mut self.current_location { Some(location) => location, @@ -5719,10 +5639,33 @@ impl<'a> Default for TraversalProgress<'a> { } } -pub struct GitTraversal<'a> { - // TODO - // statuses: I, - traversal: Traversal<'a>, +pub struct EntryWithGitStatusRef<'a> { + pub entry: &'a Entry, + pub git_status: Option, +} + +impl<'a> EntryWithGitStatusRef<'a> { + fn entry(entry: &'a Entry) -> Self { + Self { + entry, + git_status: None, + } + } + + pub fn to_owned(&self) -> EntryWithGitStatus { + EntryWithGitStatus { + entry: self.entry.clone(), + git_status: self.git_status.clone(), + } + } +} + +impl<'a> Deref for EntryWithGitStatusRef<'a> { + type Target = Entry; + + fn deref(&self) -> &Self::Target { + &self.entry + } } pub struct EntryWithGitStatus { @@ -5738,24 +5681,104 @@ impl Deref for EntryWithGitStatus { } } -impl<'a> Iterator for GitTraversal<'a> { - type Item = EntryWithGitStatus; - fn next(&mut self) -> Option { - todo!() +pub struct GitTraversal<'a> { + traversal: Traversal<'a>, + reset: bool, + repositories: Cursor<'a, RepositoryEntry, PathProgress<'a>>, + statuses: Option>>, +} + +impl<'a> GitTraversal<'a> { + pub fn advance(&mut self) -> bool { + self.advance_by(1) + } + + pub fn advance_by(&mut self, count: usize) -> bool { + self.traversal.advance_by(count) + } + + pub fn advance_to_sibling(&mut self) -> bool { + self.traversal.advance_to_sibling() + } + + pub fn back_to_parent(&mut self) -> bool { + if self.traversal.back_to_parent() { + self.reset = true; + true + } else { + false + } + } + + pub fn start_offset(&self) -> usize { + self.traversal.start_offset() + } + + pub fn end_offset(&self) -> usize { + self.traversal.end_offset() + } + + pub fn entry(&mut self) -> Option> { + let reset = mem::take(&mut self.reset); + let entry = self.traversal.cursor.item()?; + let current_repository_id = self + .repositories + .item() + .map(|repository| repository.work_directory_id); + + if reset { + self.repositories + .seek(&PathTarget::Path(&entry.path), Bias::Left, &()); + } else { + self.repositories + .seek_forward(&PathTarget::Path(&entry.path), Bias::Left, &()); + } + let Some(repository) = self.repositories.item() else { + self.statuses = None; + return Some(EntryWithGitStatusRef::entry(entry)); + }; + + if reset || Some(repository.work_directory_id) != current_repository_id { + self.statuses = Some(repository.git_entries_by_path.cursor::(&())); + } + + let Some(statuses) = self.statuses.as_mut() else { + return Some(EntryWithGitStatusRef::entry(entry)); + }; + let Some(repo_path) = repository.relativize(&entry.path).ok() else { + return Some(EntryWithGitStatusRef::entry(entry)); + }; + let found = statuses.seek_forward(&PathTarget::Path(&repo_path.0), Bias::Left, &()); + + if found { + let Some(status) = statuses.item() else { + return Some(EntryWithGitStatusRef::entry(entry)); + }; + + Some(EntryWithGitStatusRef { + entry, + git_status: Some(status.git_status), + }) + } else { + Some(EntryWithGitStatusRef::entry(entry)) + } } } -impl< - 'a, - // I: Iterator + FusedIterator, - > DoubleEndedIterator for GitTraversal<'a> -{ - fn next_back(&mut self) -> Option { - todo!() +impl<'a> Iterator for GitTraversal<'a> { + type Item = EntryWithGitStatusRef<'a>; + fn next(&mut self) -> Option { + if let Some(item) = self.entry() { + self.advance(); + Some(item) + } else { + None + } } } pub struct Traversal<'a> { + repositories: &'a SumTree, cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>, include_ignored: bool, include_files: bool, @@ -5764,6 +5787,7 @@ pub struct Traversal<'a> { impl<'a> Traversal<'a> { fn new( + repositories: &'a SumTree, entries: &'a SumTree, include_files: bool, include_dirs: bool, @@ -5771,8 +5795,9 @@ impl<'a> Traversal<'a> { start_path: &Path, ) -> Self { let mut cursor = entries.cursor(&()); - cursor.seek(&TraversalTarget::Path(start_path), Bias::Left, &()); + cursor.seek(&TraversalTarget::path(start_path), Bias::Left, &()); let mut traversal = Self { + repositories, cursor, include_files, include_dirs, @@ -5784,10 +5809,24 @@ impl<'a> Traversal<'a> { traversal } - pub fn with_git_statuses(self, snapshot: &'a Snapshot) -> GitTraversal<'a> { + pub fn with_git_statuses(self) -> GitTraversal<'a> { + let mut repositories = self.repositories.cursor::(&()); + if let Some(start_path) = self.cursor.item() { + repositories.seek(&PathTarget::Path(&start_path.path), Bias::Left, &()); + }; + let statuses = repositories.item().map(|repository| { + let mut statuses = repository.git_entries_by_path.cursor::(&()); + if let Some(start_path) = self.cursor.item() { + statuses.seek(&PathTarget::Path(&start_path.path), Bias::Left, &()); + } + statuses + }); + GitTraversal { - // statuses: all_statuses_cursor(snapshot), traversal: self, + repositories, + reset: false, + statuses, } } @@ -5810,11 +5849,8 @@ impl<'a> Traversal<'a> { pub fn advance_to_sibling(&mut self) -> bool { while let Some(entry) = self.cursor.item() { - self.cursor.seek_forward( - &TraversalTarget::PathSuccessor(&entry.path), - Bias::Left, - &(), - ); + self.cursor + .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left, &()); if let Some(entry) = self.cursor.item() { if (self.include_files || !entry.is_file()) && (self.include_dirs || !entry.is_dir()) @@ -5832,7 +5868,7 @@ impl<'a> Traversal<'a> { return false; }; self.cursor - .seek(&TraversalTarget::Path(parent_path), Bias::Left, &()) + .seek(&TraversalTarget::path(parent_path), Bias::Left, &()) } pub fn entry(&self) -> Option<&'a Entry> { @@ -5865,10 +5901,64 @@ impl<'a> Iterator for Traversal<'a> { } } +#[derive(Debug, Clone, Copy)] +enum PathTarget<'a> { + Path(&'a Path), + Successor(&'a Path), +} + +impl<'a> PathTarget<'a> { + fn path(&self) -> &'a Path { + match self { + PathTarget::Path(path) => path, + PathTarget::Successor(path) => path, + } + } + + fn with_path(self, path: &Path) -> PathTarget<'_> { + match self { + PathTarget::Successor(_) => PathTarget::Successor(path), + PathTarget::Path(_) => PathTarget::Path(path), + } + } + + fn cmp_path(&self, other: &Path) -> Ordering { + match self { + PathTarget::Path(path) => path.cmp(&other), + PathTarget::Successor(path) => { + if other.starts_with(path) { + Ordering::Greater + } else { + Ordering::Equal + } + } + } + } +} + +impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary, PathProgress<'a>> for PathTarget<'b> { + fn cmp(&self, cursor_location: &PathProgress<'a>, _: &S::Context) -> Ordering { + self.cmp_path(&cursor_location.max_path) + } +} + +impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary, TraversalProgress<'a>> for PathTarget<'b> { + fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &S::Context) -> Ordering { + self.cmp_path(&cursor_location.max_path) + } +} + +impl<'a, 'b> SeekTarget<'a, PathSummary, (TraversalProgress<'a>, GitStatuses)> + for PathTarget<'b> +{ + fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering { + self.cmp_path(&cursor_location.0.max_path) + } +} + #[derive(Debug)] enum TraversalTarget<'a> { - Path(&'a Path), - PathSuccessor(&'a Path), + Path(PathTarget<'a>), Count { count: usize, include_files: bool, @@ -5877,17 +5967,18 @@ enum TraversalTarget<'a> { }, } -impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'b> { - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { +impl<'a> TraversalTarget<'a> { + fn path(path: &'a Path) -> Self { + Self::Path(PathTarget::Path(path)) + } + + fn successor(path: &'a Path) -> Self { + Self::Path(PathTarget::Successor(path)) + } + + fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering { match self { - TraversalTarget::Path(path) => path.cmp(&cursor_location.max_path), - TraversalTarget::PathSuccessor(path) => { - if cursor_location.max_path.starts_with(path) { - Ordering::Greater - } else { - Ordering::Equal - } - } + TraversalTarget::Path(path) => path.cmp_path(&progress.max_path), TraversalTarget::Count { count, include_files, @@ -5895,12 +5986,24 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa include_ignored, } => Ord::cmp( count, - &cursor_location.count(*include_files, *include_dirs, *include_ignored), + &progress.count(*include_files, *include_dirs, *include_ignored), ), } } } +impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'b> { + fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { + self.cmp_progress(cursor_location) + } +} + +impl<'a, 'b> SeekTarget<'a, PathSummary, TraversalProgress<'a>> for TraversalTarget<'b> { + fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { + self.cmp_progress(cursor_location) + } +} + pub struct ChildEntriesIter<'a> { parent_path: &'a Path, traversal: Traversal<'a>, diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index d765416ac9f5f..94e3e7458c69e 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2322,7 +2322,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_git_status(cx: &mut TestAppContext) { +async fn test_file_status(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); const IGNORE_RULE: &str = "**/target"; @@ -2714,6 +2714,95 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_traverse_with_git_status(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "x": { + ".git": {}, + "x1.txt": "foo", + "x2.txt": "bar", + "y": { + ".git": {}, + "y1.txt": "baz", + "y2.txt": "qux" + }, + "z.txt": "sneaky..." + }, + "z": { + ".git": {}, + "z1.txt": "quux", + "z2.txt": "quuux" + } + }), + ) + .await; + + fs.set_status_for_repo_via_git_operation( + Path::new("/root/x/.git"), + &[ + (Path::new("x2.txt"), GitFileStatus::Modified), + (Path::new("z.txt"), GitFileStatus::Added), + ], + ); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/x/y/.git"), + &[(Path::new("y1.txt"), GitFileStatus::Conflict)], + ); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/z/.git"), + &[(Path::new("z2.txt"), GitFileStatus::Added)], + ); + + let tree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + tree.flush_fs_events(cx).await; + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + cx.executor().run_until_parked(); + + let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); + + dbg!(snapshot.git_satus(Path::new("x"))); + + let mut traversal = snapshot + .traverse_from_path(true, false, true, Path::new("x")) + .with_git_statuses(); + + let entry = traversal.next().unwrap(); + assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt")); + assert_eq!(entry.git_status, None); + let entry = traversal.next().unwrap(); + assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt")); + assert_eq!(entry.git_status, Some(GitFileStatus::Modified)); + let entry = traversal.next().unwrap(); + assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt")); + assert_eq!(entry.git_status, Some(GitFileStatus::Conflict)); + let entry = traversal.next().unwrap(); + assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt")); + assert_eq!(entry.git_status, None); + let entry = traversal.next().unwrap(); + assert_eq!(entry.path.as_ref(), Path::new("x/z.txt")); + assert_eq!(entry.git_status, Some(GitFileStatus::Added)); + let entry = traversal.next().unwrap(); + assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt")); + assert_eq!(entry.git_status, None); + let entry = traversal.next().unwrap(); + assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt")); + assert_eq!(entry.git_status, Some(GitFileStatus::Added)); +} + #[gpui::test] async fn test_propagate_git_statuses(cx: &mut TestAppContext) { init_test(cx); @@ -2945,21 +3034,35 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { ".git": {}, "y1.txt": "baz", "y2.txt": "qux" - } + }, + "z.txt": "sneaky..." }, + "z": { + ".git": {}, + "z1.txt": "quux", + "z2.txt": "quuux" + } }), ) .await; fs.set_status_for_repo_via_git_operation( Path::new("/root/x/.git"), - &[(Path::new("x2.txt"), GitFileStatus::Modified)], + &[ + (Path::new("x2.txt"), GitFileStatus::Modified), + (Path::new("z.txt"), GitFileStatus::Added), + ], ); fs.set_status_for_repo_via_git_operation( Path::new("/root/x/y/.git"), &[(Path::new("y1.txt"), GitFileStatus::Conflict)], ); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/z/.git"), + &[(Path::new("z2.txt"), GitFileStatus::Added)], + ); + let tree = Worktree::local( Path::new("/root"), true, @@ -2977,7 +3080,7 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); - // Sanity check the propagation for x/y + // Sanity check the propagation for x/y and z check_propagated_statuses( &snapshot, &[ @@ -2986,6 +3089,14 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { (Path::new("x/y/y2.txt"), None), ], ); + check_propagated_statuses( + &snapshot, + &[ + (Path::new("z"), Some(GitFileStatus::Added)), // the y git repository has conflict file in it, and so should have a conflict status + (Path::new("z/z1.txt"), None), + (Path::new("z/z2.txt"), Some(GitFileStatus::Added)), + ], + ); // Test one of the fundamental cases of propogation blocking, the transition from one git repository to another check_propagated_statuses( @@ -3007,6 +3118,7 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { (Path::new("x/y"), Some(GitFileStatus::Conflict)), (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)), (Path::new("x/y/y2.txt"), None), + (Path::new("x/z.txt"), Some(GitFileStatus::Added)), ], ); @@ -3031,6 +3143,10 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { (Path::new("x/y"), Some(GitFileStatus::Conflict)), (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)), (Path::new("x/y/y2.txt"), None), + (Path::new("x/z.txt"), Some(GitFileStatus::Added)), + (Path::new("z"), Some(GitFileStatus::Added)), + (Path::new("z/z1.txt"), None), + (Path::new("z/z2.txt"), Some(GitFileStatus::Added)), ], ); } From 2ff06cea6ee93b25e6d1d32ea247f70769207d82 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 22 Dec 2024 01:55:05 -0800 Subject: [PATCH 20/22] speculatively roll this out to API consumers --- crates/git_ui/src/git_panel.rs | 6 +- crates/outline_panel/src/outline_panel.rs | 82 ++++++------ crates/project/src/project.rs | 8 +- crates/project_panel/src/project_panel.rs | 149 +++++++++++----------- crates/worktree/src/worktree.rs | 103 +++++++++++---- crates/worktree/src/worktree_tests.rs | 9 +- 6 files changed, 200 insertions(+), 157 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index bc6ce873aedb6..8c3fe18607294 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -7,7 +7,7 @@ use std::{ sync::Arc, time::Duration, }; -use worktree::GitEntry; +use worktree::StatusEntry; use git::repository::{GitFileStatus, RepoPath}; @@ -90,7 +90,7 @@ pub struct GitPanel { // not hidden by folding or such visible_entries: Vec<( WorktreeId, - Vec, + Vec, OnceCell>, )>, width: Option, @@ -232,7 +232,7 @@ impl GitPanel { } fn calculate_depth_and_difference( - entry: &GitEntry, + entry: &StatusEntry, visible_worktree_entries: &HashSet, ) -> (usize, usize) { let (depth, difference) = entry diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 359970a142f8c..0052c81121f48 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -22,7 +22,6 @@ use editor::{ }; use file_icons::FileIcons; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; -use git::repository::GitFileStatus; use gpui::{ actions, anchored, deferred, div, point, px, size, uniform_list, Action, AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, @@ -57,7 +56,7 @@ use workspace::{ }, OpenInTerminal, WeakItemHandle, Workspace, }; -use worktree::{Entry, ProjectEntryId, WorktreeId}; +use worktree::{Entry, GitEntry, ProjectEntryId, WorktreeId}; actions!( outline_panel, @@ -352,8 +351,7 @@ enum ExcerptOutlines { #[derive(Clone, Debug, PartialEq, Eq)] struct FoldedDirsEntry { worktree_id: WorktreeId, - entries: Vec, - git_file_statuses: Vec>, + entries: Vec, } // TODO: collapse the inner enums into panel entry @@ -581,10 +579,9 @@ impl OutlineEntry { #[derive(Debug, Clone, Eq)] struct FsEntryFile { worktree_id: WorktreeId, - entry: Entry, + entry: GitEntry, buffer_id: BufferId, excerpts: Vec, - git_status: Option, } impl PartialEq for FsEntryFile { @@ -604,8 +601,7 @@ impl Hash for FsEntryFile { #[derive(Debug, Clone, Eq)] struct FsEntryDirectory { worktree_id: WorktreeId, - entry: Entry, - git_status: Option, + entry: GitEntry, } impl PartialEq for FsEntryDirectory { @@ -2087,13 +2083,11 @@ impl OutlinePanel { }; let (item_id, label_element, icon) = match rendered_entry { FsEntry::File(FsEntryFile { - worktree_id, - entry, - git_status, - .. + worktree_id, entry, .. }) => { let name = self.entry_name(worktree_id, entry, cx); - let color = entry_git_aware_label_color(*git_status, entry.is_ignored, is_active); + let color = + entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active); let icon = if settings.file_icons { FileIcons::get_icon(&entry.path, cx) .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) @@ -2113,18 +2107,18 @@ impl OutlinePanel { icon.unwrap_or_else(empty_icon), ) } - FsEntry::Directory(FsEntryDirectory { - worktree_id, - - entry, - git_status, - }) => { - let name = self.entry_name(worktree_id, entry, cx); - - let is_expanded = !self - .collapsed_entries - .contains(&CollapsedEntry::Dir(*worktree_id, entry.id)); - let color = entry_git_aware_label_color(*git_status, entry.is_ignored, is_active); + FsEntry::Directory(directory) => { + let name = self.entry_name(&directory.worktree_id, &directory.entry, cx); + + let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir( + directory.worktree_id, + directory.entry.id, + )); + let color = entry_git_aware_label_color( + directory.entry.git_status, + directory.entry.is_ignored, + is_active, + ); let icon = if settings.folder_icons { FileIcons::get_folder_icon(is_expanded, cx) } else { @@ -2133,7 +2127,7 @@ impl OutlinePanel { .map(Icon::from_path) .map(|icon| icon.color(color).into_any_element()); ( - ElementId::from(entry.id.to_proto() as usize), + ElementId::from(directory.entry.id.to_proto() as usize), HighlightedLabel::new( name, string_match @@ -2214,7 +2208,10 @@ impl OutlinePanel { .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id)) }); let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored); - let git_status = folded_dir.git_file_statuses.first().cloned().flatten(); + let git_status = folded_dir + .entries + .first() + .and_then(|entry| entry.git_status); let color = entry_git_aware_label_color(git_status, is_ignored, is_active); let icon = if settings.folder_icons { FileIcons::get_folder_icon(is_expanded, cx) @@ -2520,7 +2517,7 @@ impl OutlinePanel { let mut processed_external_buffers = HashSet::default(); let mut new_worktree_entries = HashMap::< WorktreeId, - (worktree::Snapshot, HashMap), + (worktree::Snapshot, HashMap), >::default(); let mut worktree_excerpts = HashMap::< WorktreeId, @@ -2561,12 +2558,13 @@ impl OutlinePanel { match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() { Some(entry) => { - let mut traversal = worktree.traverse_from_path( - true, - true, - true, - entry.path.as_ref(), - ); + let entry = GitEntry { + git_status: worktree.status_for_file(&entry.path), + entry, + }; + let mut traversal = worktree + .traverse_from_path(true, true, true, entry.path.as_ref()) + .with_git_statuses(); let mut entries_to_add = HashMap::default(); worktree_excerpts @@ -2598,7 +2596,7 @@ impl OutlinePanel { .is_none(); if new_entry_added && traversal.back_to_parent() { if let Some(parent_entry) = traversal.entry() { - current_entry = parent_entry.clone(); + current_entry = parent_entry.to_owned(); continue; } } @@ -2636,15 +2634,14 @@ impl OutlinePanel { let mut entries = entries.into_values().collect::>(); // For a proper git status propagation, we have to keep the entries sorted lexicographically. entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref())); - let statuses = worktree_snapshot.propagate_git_statuses(&entries); - let entries = entries.into_iter().zip(statuses).collect::>(); + worktree_snapshot.propagate_git_statuses(&mut entries); (worktree_id, entries) }) .flat_map(|(worktree_id, entries)| { { entries .into_iter() - .filter_map(|(entry, git_status)| { + .filter_map(|entry| { if auto_fold_dirs { if let Some(parent) = entry.path.parent() { let children = new_children_count @@ -2664,7 +2661,6 @@ impl OutlinePanel { Some(FsEntry::Directory(FsEntryDirectory { worktree_id, entry, - git_status, })) } else { let (buffer_id, excerpts) = worktree_excerpts @@ -2677,7 +2673,6 @@ impl OutlinePanel { buffer_id, entry, excerpts, - git_status, })) } }) @@ -3461,7 +3456,6 @@ impl OutlinePanel { FoldedDirsEntry { worktree_id: directory_entry.worktree_id, entries: vec![directory_entry.entry.clone()], - git_file_statuses: vec![directory_entry.git_status], }, )) }; @@ -3472,7 +3466,6 @@ impl OutlinePanel { FoldedDirsEntry { worktree_id: directory_entry.worktree_id, entries: vec![directory_entry.entry.clone()], - git_file_statuses: vec![directory_entry.git_status], }, )); } @@ -3715,7 +3708,6 @@ impl OutlinePanel { 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { worktree_id: folded_dirs_entry.worktree_id, entry: folded_dirs_entry.entries[0].clone(), - git_status: folded_dirs_entry.git_file_statuses[0], })), _ => entry, } @@ -3778,7 +3770,7 @@ impl OutlinePanel { fn dir_names_string( &self, - entries: &[Entry], + entries: &[GitEntry], worktree_id: WorktreeId, cx: &AppContext, ) -> String { @@ -4521,7 +4513,7 @@ impl OutlinePanel { fn buffers_inside_directory( &self, dir_worktree: WorktreeId, - dir_entry: &Entry, + dir_entry: &GitEntry, ) -> HashSet { if !dir_entry.is_dir() { debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}"); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0083bd556146c..e6672d53e5e2c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4456,11 +4456,13 @@ impl Completion { } } -pub fn sort_worktree_entries(entries: &mut [(Entry, Option)]) { +pub fn sort_worktree_entries(entries: &mut [impl AsRef]) { entries.sort_by(|entry_a, entry_b| { + let entry_a = entry_a.as_ref(); + let entry_b = entry_b.as_ref(); compare_paths( - (&entry_a.0.path, entry_a.0.is_file()), - (&entry_b.0.path, entry_b.0.is_file()), + (&entry_a.path, entry_a.is_file()), + (&entry_b.path, entry_b.is_file()), ) }); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 20bab76334d42..bfbb0890e8eb0 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -63,7 +63,7 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace, }; -use worktree::CreatedEntry; +use worktree::{CreatedEntry, GitEntry, GitEntryRef}; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -76,11 +76,7 @@ pub struct ProjectPanel { // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's // hovered over the start/end of a list. hover_scroll_task: Option>, - visible_entries: Vec<( - WorktreeId, - Vec<(Entry, Option)>, - OnceCell>>, - )>, + visible_entries: Vec<(WorktreeId, Vec, OnceCell>>)>, /// Maps from leaf project entry ID to the currently selected ancestor. /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several /// project entries (and all non-leaf nodes are guaranteed to be directories). @@ -893,7 +889,7 @@ impl ProjectPanel { let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix]; let selection = SelectedEntry { worktree_id: *worktree_id, - entry_id: worktree_entries[entry_ix].0.id, + entry_id: worktree_entries[entry_ix].id, }; self.selection = Some(selection); if cx.modifiers().shift { @@ -1366,6 +1362,7 @@ impl ProjectPanel { let mut siblings: Vec<_> = worktree .snapshot() .child_entries(parent_path) + .with_git_statuses() .filter(|sibling| { sibling.id == latest_entry.id || !marked_entries_in_worktree.contains(&&SelectedEntry { @@ -1373,14 +1370,13 @@ impl ProjectPanel { entry_id: sibling.id, }) }) - .map(|sibling| &(*sibling, None)) - .cloned() + .map(|entry| entry.to_owned()) .collect(); project::sort_worktree_entries(&mut siblings); let sibling_entry_index = siblings .iter() - .position(|sibling| sibling.0.id == latest_entry.0.id)?; + .position(|sibling| sibling.id == latest_entry.id)?; if let Some(next_sibling) = sibling_entry_index .checked_add(1) @@ -1388,7 +1384,7 @@ impl ProjectPanel { { return Some(SelectedEntry { worktree_id, - entry_id: next_sibling.0.id, + entry_id: next_sibling.id, }); } if let Some(prev_sibling) = sibling_entry_index @@ -1397,7 +1393,7 @@ impl ProjectPanel { { return Some(SelectedEntry { worktree_id, - entry_id: prev_sibling.0.id, + entry_id: prev_sibling.id, }); } // No neighbour sibling found, fall back to parent @@ -1491,7 +1487,7 @@ impl ProjectPanel { if let Some(entry) = worktree_entries.get(entry_ix) { let selection = SelectedEntry { worktree_id: *worktree_id, - entry_id: entry.0.id, + entry_id: entry.id, }; self.selection = Some(selection); if cx.modifiers().shift { @@ -1511,7 +1507,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), true, - |entry, _, worktree_id| { + |entry, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1541,7 +1537,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), false, - |entry, _, worktree_id| { + |entry, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1571,7 +1567,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), true, - |entry, git_status, worktree_id| { + |entry, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1581,7 +1577,9 @@ impl ProjectPanel { } })) && entry.is_file() - && git_status.is_some_and(|status| matches!(status, GitFileStatus::Modified)) + && entry + .git_status + .is_some_and(|status| matches!(status, GitFileStatus::Modified)) }, cx, ); @@ -1649,7 +1647,7 @@ impl ProjectPanel { let selection = self.find_entry( self.selection.as_ref(), true, - |entry, git_status, worktree_id| { + |entry, worktree_id| { (self.selection.is_none() || self.selection.is_some_and(|selection| { if selection.worktree_id == worktree_id { @@ -1659,7 +1657,9 @@ impl ProjectPanel { } })) && entry.is_file() - && git_status.is_some_and(|status| matches!(status, GitFileStatus::Modified)) + && entry + .git_status + .is_some_and(|status| matches!(status, GitFileStatus::Modified)) }, cx, ); @@ -2110,7 +2110,7 @@ impl ProjectPanel { { if *worktree_id == selection.worktree_id { for entry in worktree_entries { - if entry.0.id == selection.entry_id { + if entry.id == selection.entry_id { return Some((worktree_index, entry_index, visible_entries_index)); } else { visible_entries_index += 1; @@ -2308,7 +2308,7 @@ impl ProjectPanel { } let mut visible_worktree_entries = Vec::new(); - let mut entry_iter = snapshot.entries(true, 0); + let mut entry_iter = snapshot.entries(true, 0).with_git_statuses(); let mut auto_folded_ancestors = vec![]; while let Some(entry) = entry_iter.entry() { if auto_collapse_dirs && entry.kind.is_dir() { @@ -2350,7 +2350,7 @@ impl ProjectPanel { } } auto_folded_ancestors.clear(); - visible_worktree_entries.push(entry.clone()); + visible_worktree_entries.push(entry.to_owned()); let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id { entry.id == new_entry_id || { self.ancestors.get(&entry.id).map_or(false, |entries| { @@ -2364,24 +2364,27 @@ impl ProjectPanel { false }; if precedes_new_entry { - visible_worktree_entries.push(Entry { - id: NEW_ENTRY_ID, - kind: new_entry_kind, - path: entry.path.join("\0").into(), - inode: 0, - mtime: entry.mtime, - size: entry.size, - is_ignored: entry.is_ignored, - is_external: false, - is_private: false, - is_always_included: entry.is_always_included, - canonical_path: entry.canonical_path.clone(), - char_bag: entry.char_bag, - is_fifo: entry.is_fifo, + visible_worktree_entries.push(GitEntry { + entry: Entry { + id: NEW_ENTRY_ID, + kind: new_entry_kind, + path: entry.path.join("\0").into(), + inode: 0, + mtime: entry.mtime, + size: entry.size, + is_ignored: entry.is_ignored, + is_external: false, + is_private: false, + is_always_included: entry.is_always_included, + canonical_path: entry.canonical_path.clone(), + char_bag: entry.char_bag, + is_fifo: entry.is_fifo, + }, + git_status: entry.git_status, }); } let worktree_abs_path = worktree.read(cx).abs_path(); - let (depth, path) = if Some(entry) == worktree.read(cx).root_entry() { + let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() { let Some(path_name) = worktree_abs_path .file_name() .with_context(|| { @@ -2458,11 +2461,7 @@ impl ProjectPanel { entry_iter.advance(); } - let git_statuses = snapshot.propagate_git_statuses(&mut visible_worktree_entries); - let mut visible_worktree_entries = visible_worktree_entries - .into_iter() - .zip(git_statuses.into_iter()) - .collect::>(); + snapshot.propagate_git_statuses(&mut visible_worktree_entries); project::sort_worktree_entries(&mut visible_worktree_entries); self.visible_entries @@ -2475,7 +2474,7 @@ impl ProjectPanel { if worktree_id == *id { entries .iter() - .position(|entry| entry.0.id == project_entry_id) + .position(|entry| entry.id == project_entry_id) } else { visited_worktrees_length += entries.len(); None @@ -2654,19 +2653,19 @@ impl ProjectPanel { return visible_worktree_entries .iter() .enumerate() - .find(|(_, entry)| entry.0.id == entry_id) + .find(|(_, entry)| entry.id == entry_id) .map(|(ix, _)| (worktree_ix, ix, total_ix + ix)); } None } - fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, Option, &Entry)> { + fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> { let mut offset = 0; for (worktree_id, visible_worktree_entries, _) in &self.visible_entries { if visible_worktree_entries.len() > offset + index { return visible_worktree_entries .get(index) - .map(|(entry, git_file_status)| (*worktree_id, *git_file_status, entry)); + .map(|entry| (*worktree_id, entry.to_ref())); } offset += visible_worktree_entries.len(); } @@ -2695,11 +2694,11 @@ impl ProjectPanel { let entries = entries_paths.get_or_init(|| { visible_worktree_entries .iter() - .map(|e| (e.0.path.clone())) + .map(|e| (e.path.clone())) .collect() }); for entry in visible_worktree_entries[entry_range].iter() { - callback(&entry.0, entries, cx); + callback(&entry, entries, cx); } ix = end_ix; } @@ -2744,11 +2743,11 @@ impl ProjectPanel { let entries = entries_paths.get_or_init(|| { visible_worktree_entries .iter() - .map(|e| (e.0.path.clone())) + .map(|e| (e.path.clone())) .collect() }); - for (entry, git_status) in visible_worktree_entries[entry_range].iter() { - let status = git_status_setting.then_some(*git_status).flatten(); + for entry in visible_worktree_entries[entry_range].iter() { + let status = git_status_setting.then_some(entry.git_status).flatten(); let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); let icon = match entry.kind { EntryKind::File => { @@ -2897,9 +2896,9 @@ impl ProjectPanel { worktree_id: WorktreeId, reverse_search: bool, only_visible_entries: bool, - predicate: impl Fn(&Entry, Option, WorktreeId) -> bool, + predicate: impl Fn(GitEntryRef, WorktreeId) -> bool, cx: &mut ViewContext, - ) -> Option { + ) -> Option { if only_visible_entries { let entries = self .visible_entries @@ -2914,19 +2913,18 @@ impl ProjectPanel { .clone(); return utils::ReversibleIterable::new(entries.iter(), reverse_search) - .find(|ele| predicate(&ele.0, ele.1, worktree_id)) - .map(|ele| ele.0); + .find(|ele| predicate(ele.to_ref(), worktree_id)) + .cloned(); } let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; worktree.update(cx, |tree, _| { utils::ReversibleIterable::new( - tree.entries(true, 0usize) - .with_git_statuses(&tree.snapshot()), + tree.entries(true, 0usize).with_git_statuses(), reverse_search, ) - .find_single_ended(|ele| predicate(&ele.0, ele.1, worktree_id)) - .map(|ele| ele.0) + .find_single_ended(|ele| predicate(*ele, worktree_id)) + .map(|ele| ele.to_owned()) }) } @@ -2934,7 +2932,7 @@ impl ProjectPanel { &self, start: Option<&SelectedEntry>, reverse_search: bool, - predicate: impl Fn(&Entry, Option, WorktreeId) -> bool, + predicate: impl Fn(GitEntryRef, WorktreeId) -> bool, cx: &mut ViewContext, ) -> Option { let mut worktree_ids: Vec<_> = self @@ -2956,11 +2954,9 @@ impl ProjectPanel { let root_entry = tree.root_entry()?; let tree_id = tree.id(); - // TOOD: Expose the all statuses cursor, as a wrapper over a Traversal - // The co-iterates the GitEntries as the file entries come through let mut first_iter = tree .traverse_from_path(true, true, true, entry.path.as_ref()) - .with_git_statuses(tree); + .with_git_statuses(); if reverse_search { first_iter.next(); @@ -2968,25 +2964,25 @@ impl ProjectPanel { let first = first_iter .enumerate() - .take_until(|(count, ele)| ele.0 == root_entry && *count != 0usize) - .map(|(_, ele)| ele) - .find(|ele| predicate(ele.0, ele.1 tree_id)) - .cloned(); + .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize) + .map(|(_, entry)| entry) + .find(|ele| predicate(*ele, tree_id)) + .map(|ele| ele.to_owned()); - let second_iter = tree.entries(true, 0usize); + let second_iter = tree.entries(true, 0usize).with_git_statuses(); let second = if reverse_search { second_iter .take_until(|ele| ele.id == start.entry_id) - .filter(|ele| predicate(ele, tree_id)) + .filter(|ele| predicate(*ele, tree_id)) .last() - .cloned() + .map(|ele| ele.to_owned()) } else { second_iter .take_while(|ele| ele.id != start.entry_id) - .filter(|ele| predicate(ele, tree_id)) + .filter(|ele| predicate(*ele, tree_id)) .last() - .cloned() + .map(|ele| ele.to_owned()) }; if reverse_search { @@ -3043,7 +3039,7 @@ impl ProjectPanel { &self, start: Option<&SelectedEntry>, reverse_search: bool, - predicate: impl Fn(&Entry, WorktreeId) -> bool, + predicate: impl Fn(GitEntryRef, WorktreeId) -> bool, cx: &mut ViewContext, ) -> Option { let mut worktree_ids: Vec<_> = self @@ -3085,8 +3081,8 @@ impl ProjectPanel { ) }; - let first_search = first_iter.find(|ele| predicate(ele, start.worktree_id)); - let second_search = second_iter.find(|ele| predicate(ele, start.worktree_id)); + let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id)); + let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id)); if first_search.is_some() { return first_search.map(|entry| SelectedEntry { @@ -4026,8 +4022,7 @@ impl Render for ProjectPanel { if cx.modifiers().secondary() { let ix = active_indent_guide.offset.y; let Some((target_entry, worktree)) = maybe!({ - let (worktree_id, _git_status, entry) = - this.entry_at_index(ix)?; + let (worktree_id, entry) = this.entry_at_index(ix)?; let worktree = this .project .read(cx) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a92e95aedca69..93440ae531992 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -193,7 +193,7 @@ pub struct RepositoryEntry { /// With this setup, this field would contain 2 entries, like so: /// - my_sub_folder_1/project_root/changed_file_1 /// - my_sub_folder_2/changed_file_2 - pub(crate) git_entries_by_path: SumTree, + pub(crate) git_entries_by_path: SumTree, pub(crate) work_directory_id: ProjectEntryId, pub(crate) work_directory: WorkDirectory, pub(crate) branch: Option>, @@ -226,7 +226,7 @@ impl RepositoryEntry { self.into() } - pub fn status(&self) -> impl Iterator + '_ { + pub fn status(&self) -> impl Iterator + '_ { self.git_entries_by_path.iter().cloned() } } @@ -2480,7 +2480,7 @@ impl Snapshot { } #[cfg(any(feature = "test-support", test))] - pub fn git_satus(&self, work_dir: &Path) -> Option> { + pub fn git_satus(&self, work_dir: &Path) -> Option> { self.repositories .get(&PathKey(work_dir.into()), &()) .map(|repo| repo.status().collect()) @@ -2532,7 +2532,7 @@ impl Snapshot { }) } - pub fn propagate_git_statuses(&self, entries: &[Entry]) -> Vec> { + pub fn propagate_git_statuses(&self, entries: &mut [GitEntry]) -> Vec> { let mut cursor = all_statuses_cursor(self); let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); @@ -3586,7 +3586,7 @@ pub type UpdatedEntriesSet = Arc<[(Arc, ProjectEntryId, PathChange)]>; pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; #[derive(Clone, Debug, PartialEq, Eq)] -pub struct GitEntry { +pub struct StatusEntry { pub path: RepoPath, pub git_status: GitFileStatus, } @@ -3665,7 +3665,7 @@ impl sum_tree::Summary for GitStatuses { } } -impl sum_tree::Item for GitEntry { +impl sum_tree::Item for StatusEntry { type Summary = PathSummary; fn summary(&self, _: &::Context) -> Self::Summary { @@ -3694,7 +3694,7 @@ impl sum_tree::Item for GitEntry { } } -impl sum_tree::KeyedItem for GitEntry { +impl sum_tree::KeyedItem for StatusEntry { type Key = PathKey; fn key(&self) -> Self::Key { @@ -3779,7 +3779,7 @@ struct AllStatusesCursor<'a, I> { repos: I, current_location: Option<( &'a WorkDirectory, - Cursor<'a, GitEntry, (TraversalProgress<'a>, GitStatuses)>, + Cursor<'a, StatusEntry, (TraversalProgress<'a>, GitStatuses)>, )>, statuses_before_current_repo: GitStatuses, } @@ -4809,7 +4809,7 @@ impl BackgroundScanner { let mut changed_path_statuses = Vec::new(); for (repo_path, status) in &*status.entries { paths.remove_repo_path(repo_path); - changed_path_statuses.push(Edit::Insert(GitEntry { + changed_path_statuses.push(Edit::Insert(StatusEntry { path: repo_path.clone(), git_status: *status, })); @@ -5226,7 +5226,7 @@ impl BackgroundScanner { }; new_entries_by_path.insert_or_replace( - GitEntry { + StatusEntry { path: path.clone(), git_status: *status, }, @@ -5639,12 +5639,13 @@ impl<'a> Default for TraversalProgress<'a> { } } -pub struct EntryWithGitStatusRef<'a> { +#[derive(Debug, Clone, Copy)] +pub struct GitEntryRef<'a> { pub entry: &'a Entry, pub git_status: Option, } -impl<'a> EntryWithGitStatusRef<'a> { +impl<'a> GitEntryRef<'a> { fn entry(entry: &'a Entry) -> Self { Self { entry, @@ -5652,15 +5653,15 @@ impl<'a> EntryWithGitStatusRef<'a> { } } - pub fn to_owned(&self) -> EntryWithGitStatus { - EntryWithGitStatus { + pub fn to_owned(&self) -> GitEntry { + GitEntry { entry: self.entry.clone(), git_status: self.git_status.clone(), } } } -impl<'a> Deref for EntryWithGitStatusRef<'a> { +impl<'a> Deref for GitEntryRef<'a> { type Target = Entry; fn deref(&self) -> &Self::Target { @@ -5668,12 +5669,28 @@ impl<'a> Deref for EntryWithGitStatusRef<'a> { } } -pub struct EntryWithGitStatus { +impl<'a> AsRef for GitEntryRef<'a> { + fn as_ref(&self) -> &Entry { + self.entry + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GitEntry { pub entry: Entry, pub git_status: Option, } -impl Deref for EntryWithGitStatus { +impl GitEntry { + pub fn to_ref(&self) -> GitEntryRef { + GitEntryRef { + entry: &self.entry, + git_status: self.git_status.clone(), + } + } +} + +impl Deref for GitEntry { type Target = Entry; fn deref(&self) -> &Self::Target { @@ -5681,11 +5698,17 @@ impl Deref for EntryWithGitStatus { } } +impl<'a> AsRef for GitEntry { + fn as_ref(&self) -> &Entry { + &self.entry + } +} + pub struct GitTraversal<'a> { traversal: Traversal<'a>, reset: bool, repositories: Cursor<'a, RepositoryEntry, PathProgress<'a>>, - statuses: Option>>, + statuses: Option>>, } impl<'a> GitTraversal<'a> { @@ -5718,7 +5741,7 @@ impl<'a> GitTraversal<'a> { self.traversal.end_offset() } - pub fn entry(&mut self) -> Option> { + pub fn entry(&mut self) -> Option> { let reset = mem::take(&mut self.reset); let entry = self.traversal.cursor.item()?; let current_repository_id = self @@ -5735,7 +5758,7 @@ impl<'a> GitTraversal<'a> { } let Some(repository) = self.repositories.item() else { self.statuses = None; - return Some(EntryWithGitStatusRef::entry(entry)); + return Some(GitEntryRef::entry(entry)); }; if reset || Some(repository.work_directory_id) != current_repository_id { @@ -5743,30 +5766,30 @@ impl<'a> GitTraversal<'a> { } let Some(statuses) = self.statuses.as_mut() else { - return Some(EntryWithGitStatusRef::entry(entry)); + return Some(GitEntryRef::entry(entry)); }; let Some(repo_path) = repository.relativize(&entry.path).ok() else { - return Some(EntryWithGitStatusRef::entry(entry)); + return Some(GitEntryRef::entry(entry)); }; let found = statuses.seek_forward(&PathTarget::Path(&repo_path.0), Bias::Left, &()); if found { let Some(status) = statuses.item() else { - return Some(EntryWithGitStatusRef::entry(entry)); + return Some(GitEntryRef::entry(entry)); }; - Some(EntryWithGitStatusRef { + Some(GitEntryRef { entry, git_status: Some(status.git_status), }) } else { - Some(EntryWithGitStatusRef::entry(entry)) + Some(GitEntryRef::entry(entry)) } } } impl<'a> Iterator for GitTraversal<'a> { - type Item = EntryWithGitStatusRef<'a>; + type Item = GitEntryRef<'a>; fn next(&mut self) -> Option { if let Some(item) = self.entry() { self.advance(); @@ -6009,6 +6032,20 @@ pub struct ChildEntriesIter<'a> { traversal: Traversal<'a>, } +impl<'a> ChildEntriesIter<'a> { + pub fn with_git_statuses(self) -> ChildEntriesGitIter<'a> { + ChildEntriesGitIter { + parent_path: self.parent_path, + traversal: self.traversal.with_git_statuses(), + } + } +} + +pub struct ChildEntriesGitIter<'a> { + parent_path: &'a Path, + traversal: GitTraversal<'a>, +} + impl<'a> Iterator for ChildEntriesIter<'a> { type Item = &'a Entry; @@ -6023,6 +6060,20 @@ impl<'a> Iterator for ChildEntriesIter<'a> { } } +impl<'a> Iterator for ChildEntriesGitIter<'a> { + type Item = GitEntryRef<'a>; + + fn next(&mut self) -> Option { + if let Some(item) = self.traversal.entry() { + if item.path.starts_with(self.parent_path) { + self.traversal.advance_to_sibling(); + return Some(item); + } + } + None + } +} + impl<'a> From<&'a Entry> for proto::Entry { fn from(entry: &'a Entry) -> Self { Self { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 94e3e7458c69e..80772139d493b 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1,6 +1,6 @@ use crate::{ - worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree, - WorktreeModelHandle, + worktree_settings::WorktreeSettings, Entry, EntryKind, Event, GitEntry, PathChange, Snapshot, + Worktree, WorktreeModelHandle, }; use anyhow::Result; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; @@ -3183,7 +3183,10 @@ fn check_propagated_statuses( .iter() .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone()) .collect::>(); - let statuses = snapshot.propagate_git_statuses(&entries); + // TODO: recreate this + // let statuses = snapshot.propagate_git_statuses(&entries); + let statuses: Vec> = Vec::new(); + panic!("Redo git status propogation"); assert_eq!( entries .iter() From 7c90ba21f8eff42ba8e5f3a59513a9a954bcfb62 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 30 Dec 2024 15:19:43 -0500 Subject: [PATCH 21/22] Start fixing compiler errors --- crates/editor/src/git/project_diff.rs | 2 +- crates/git_ui/src/git_panel.rs | 69 ++++++++++++++++----------- crates/worktree/src/worktree.rs | 33 +++++++------ crates/worktree/src/worktree_tests.rs | 16 +++---- 4 files changed, 68 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 28cee78d937fd..2f791fb6b608f 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -199,7 +199,7 @@ impl ProjectDiffEditor { .repositories() .flat_map(|entry| { entry.status().map(|git_entry| { - (git_entry.git_status, entry.join(git_entry.path)) + (git_entry.git_status, entry.join(git_entry.repo_path)) }) }) .filter_map(|(status, path)| { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 87b6a73eb31e7..a00e8d4eb2c56 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,15 +1,15 @@ use crate::{git_status_icon, settings::GitPanelSettings}; use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll}; use anyhow::{Context as _, Result}; -use collections::HashMap; -use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE; use editor::{ scroll::{Autoscroll, AutoscrollStrategy}, Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT, }; -use git::repository::{GitFileStatus, RepoPath}; -use git::{diff::DiffHunk, repository::GitFileStatus}; +use git::{ + diff::DiffHunk, + repository::{GitFileStatus, RepoPath}, +}; use gpui::*; use gpui::{ actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent, @@ -19,11 +19,8 @@ use gpui::{ }; use language::{Buffer, BufferRow, OffsetRangeExt}; use menu::{SelectNext, SelectPrev}; -use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId}; -use project::{EntryKind, Fs, Project, ProjectEntryId, WorktreeId}; +use project::{EntryKind, Fs, Project, ProjectEntryId, ProjectPath, WorktreeId}; use serde::{Deserialize, Serialize}; -use serde::{Deserialize, Serialize}; -use settings::Settings as _; use settings::Settings as _; use std::{ cell::OnceCell, @@ -41,7 +38,6 @@ use ui::{ ScrollbarState, Tooltip, }; use util::{ResultExt, TryFutureExt}; -use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, ItemHandle, Workspace, @@ -76,7 +72,7 @@ pub struct GitStatusEntry {} struct EntryDetails { filename: String, display_name: String, - path: Arc, + path: RepoPath, kind: EntryKind, depth: usize, is_expanded: bool, @@ -106,7 +102,7 @@ pub struct GitPanel { project: Model, scroll_handle: UniformListScrollHandle, scrollbar_state: ScrollbarState, - _selected_item: Option, + selected_item: Option, show_scrollbar: bool, // todo!(): Reintroduce expanded directories, once we're deriving directories from paths // expanded_dir_ids: HashMap>, @@ -123,6 +119,8 @@ pub struct GitPanel { #[derive(Debug, Clone)] struct WorktreeEntries { worktree_id: WorktreeId, + // TODO support multiple repositories per worktree + work_directory: WorkDirectory, visible_entries: Vec, paths: Rc>>, } @@ -133,6 +131,14 @@ struct GitPanelEntry { hunks: Rc>>, } +impl GitPanelEntry { + fn project_path(&self) -> Option> { + self.entry + .work_directory + .unrelativize(&self.entry.repo_path) + } +} + impl Deref for GitPanelEntry { type Target = worktree::StatusEntry; @@ -146,7 +152,7 @@ impl WorktreeEntries { self.paths.get_or_init(|| { self.visible_entries .iter() - .map(|e| (e.entry.path.clone())) + .map(|e| (e.entry.repo_path.clone())) .collect() }) } @@ -176,7 +182,7 @@ impl GitPanel { project::Event::GitRepositoryUpdated => { this.update_visible_entries(None, None, cx); } - project::Event::WorktreeRemoved(id) => { + project::Event::WorktreeRemoved(_id) => { // this.expanded_dir_ids.remove(id); this.update_visible_entries(None, None, cx); cx.notify(); @@ -194,7 +200,7 @@ impl GitPanel { project::Event::Closed => { this.git_diff_editor_updates = Task::ready(()); this.reveal_in_editor = Task::ready(()); - this.expanded_dir_ids.clear(); + // this.expanded_dir_ids.clear(); this.visible_entries.clear(); this.git_diff_editor = None; } @@ -215,7 +221,7 @@ impl GitPanel { width: Some(px(360.)), scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()), scroll_handle, - _selected_item: None, + selected_item: None, show_scrollbar: !Self::should_autohide_scrollbar(cx), hide_scrollbar_task: None, git_diff_editor: Some(diff_display_editor(cx)), @@ -302,12 +308,12 @@ impl GitPanel { visible_worktree_entries: &HashSet, ) -> (usize, usize) { let (depth, difference) = entry - .path + .repo_path .ancestors() .skip(1) // Skip the entry itself .find_map(|ancestor| { if let Some(parent_entry) = visible_worktree_entries.get(ancestor) { - let entry_path_components_count = entry.path.components().count(); + let entry_path_components_count = entry.repo_path.components().count(); let parent_path_components_count = parent_entry.components().count(); let difference = entry_path_components_count - parent_path_components_count; let depth = parent_entry @@ -494,15 +500,15 @@ impl GitPanel { let filename = match difference { diff if diff > 1 => entry - .path + .repo_path .iter() - .skip(entry.path.components().count() - diff) + .skip(entry.repo_path.components().count() - diff) .collect::() .to_str() .unwrap_or_default() .to_string(), _ => entry - .path + .repo_path .file_name() .map(|name| name.to_string_lossy().into_owned()) .unwrap_or_else(|| root_name.to_string_lossy().to_string()), @@ -510,11 +516,12 @@ impl GitPanel { let details = EntryDetails { filename, - display_name: entry.path.to_string_lossy().into_owned(), - kind: entry.kind, + display_name: entry.repo_path.to_string_lossy().into_owned(), + // FIXME get it from StatusEntry? + kind: EntryKind::File, is_expanded, - path: entry.path.clone(), - status, + path: entry.repo_path.clone(), + status: Some(status), hunks: entry.hunks.clone(), depth, index, @@ -561,9 +568,12 @@ impl GitPanel { } let mut visible_worktree_entries = Vec::new(); - let repositories = snapshot.repositories().take(1); // Only use the first for now + // Only use the first repository for now + let repositories = snapshot.repositories().take(1); + let mut work_directory = None; for repository in repositories { visible_worktree_entries.extend(repository.status()); + work_directory = Some(repository.clone()); } // let mut visible_worktree_entries = snapshot @@ -578,6 +588,7 @@ impl GitPanel { if !visible_worktree_entries.is_empty() { self.visible_entries.push(WorktreeEntries { worktree_id, + work_directory: work_directory.unwrap(), visible_entries: visible_worktree_entries .into_iter() .map(|entry| GitPanelEntry { @@ -624,12 +635,14 @@ impl GitPanel { .visible_entries .iter() .filter_map(|entry| { - let git_status = entry.git_status()?; + let git_status = entry.git_status; let entry_hunks = entry.hunks.clone(); let (entry_path, unstaged_changes_task) = project.update(cx, |project, cx| { - let entry_path = - project.path_for_entry(entry.id, cx)?; + let entry_path = ProjectPath { + worktree_id: worktree_entries.worktree_id, + path: entry.project_path(), + }; let open_task = project.open_path(entry_path.clone(), cx); let unstaged_changes_task = diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 93440ae531992..493172bd9d678 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -296,6 +296,17 @@ impl WorkDirectory { Ok(relativized_path.into()) } } + + /// FIXME come up with a better name + pub fn unrelativize(&self, path: &RepoPath) -> Option> { + if let Some(location) = &self.location_in_repo { + // If we fail to strip the prefix, that means this status entry is + // external to this worktree, and we definitely won't have an entry_id + path.strip_prefix(location).ok().map(Into::into) + } else { + Some(self.path.join(path).into()) + } + } } impl Default for WorkDirectory { @@ -3587,7 +3598,7 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; #[derive(Clone, Debug, PartialEq, Eq)] pub struct StatusEntry { - pub path: RepoPath, + pub repo_path: RepoPath, pub git_status: GitFileStatus, } @@ -3670,7 +3681,7 @@ impl sum_tree::Item for StatusEntry { fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { - max_path: self.path.0.clone(), + max_path: self.repo_path.0.clone(), item_summary: match self.git_status { GitFileStatus::Added => GitStatuses { added: 1, @@ -3698,7 +3709,7 @@ impl sum_tree::KeyedItem for StatusEntry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.path.0.clone()) + PathKey(self.repo_path.0.clone()) } } @@ -4810,7 +4821,7 @@ impl BackgroundScanner { for (repo_path, status) in &*status.entries { paths.remove_repo_path(repo_path); changed_path_statuses.push(Edit::Insert(StatusEntry { - path: repo_path.clone(), + repo_path: repo_path.clone(), git_status: *status, })); } @@ -5215,19 +5226,11 @@ impl BackgroundScanner { }; let mut new_entries_by_path = SumTree::new(&()); - for (path, status) in statuses.entries.iter() { - let project_path: Option> = - if let Some(location) = &job.local_repository.location_in_repo { - // If we fail to strip the prefix, that means this status entry is - // external to this worktree, and we definitely won't have an entry_id - path.strip_prefix(location).ok().map(Into::into) - } else { - Some(job.local_repository.work_directory.path.join(path).into()) - }; - + for (repo_path, status) in statuses.entries.iter() { + let project_path = repository.work_directory.unrelativize(repo_path); new_entries_by_path.insert_or_replace( StatusEntry { - path: path.clone(), + repo_path: repo_path.clone(), git_status: *status, }, &(), diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 80772139d493b..4657267491c27 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2558,11 +2558,11 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { let entries = repo.status().collect::>(); assert_eq!(entries.len(), 3); - assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); + assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt")); assert_eq!(entries[0].git_status, GitFileStatus::Modified); - assert_eq!(entries[1].path.as_ref(), Path::new("b.txt")); + assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt")); assert_eq!(entries[1].git_status, GitFileStatus::Untracked); - assert_eq!(entries[2].path.as_ref(), Path::new("d.txt")); + assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt")); assert_eq!(entries[2].git_status, GitFileStatus::Deleted); }); @@ -2580,14 +2580,14 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { let entries = repository.status().collect::>(); std::assert_eq!(entries.len(), 4, "entries: {entries:?}"); - assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); + assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt")); assert_eq!(entries[0].git_status, GitFileStatus::Modified); - assert_eq!(entries[1].path.as_ref(), Path::new("b.txt")); + assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt")); assert_eq!(entries[1].git_status, GitFileStatus::Untracked); // Status updated - assert_eq!(entries[2].path.as_ref(), Path::new("c.txt")); + assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt")); assert_eq!(entries[2].git_status, GitFileStatus::Modified); - assert_eq!(entries[3].path.as_ref(), Path::new("d.txt")); + assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt")); assert_eq!(entries[3].git_status, GitFileStatus::Deleted); }); @@ -2620,7 +2620,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) { "Entries length was incorrect\n{:#?}", &entries ); - assert_eq!(entries[0].path.as_ref(), Path::new("a.txt")); + assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt")); assert_eq!(entries[0].git_status, GitFileStatus::Deleted); }); } From 92a19d9c505b323be5f7e9f6152777f9a4ca0468 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 30 Dec 2024 15:38:26 -0500 Subject: [PATCH 22/22] More fixes --- crates/git_ui/src/git_panel.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index a00e8d4eb2c56..b9dee054b575a 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -27,7 +27,7 @@ use std::{ collections::HashSet, ffi::OsStr, ops::{Deref, Range}, - path::{Path, PathBuf}, + path::PathBuf, rc::Rc, sync::Arc, time::Duration, @@ -120,7 +120,7 @@ pub struct GitPanel { struct WorktreeEntries { worktree_id: WorktreeId, // TODO support multiple repositories per worktree - work_directory: WorkDirectory, + work_directory: worktree::WorkDirectory, visible_entries: Vec, paths: Rc>>, } @@ -131,14 +131,6 @@ struct GitPanelEntry { hunks: Rc>>, } -impl GitPanelEntry { - fn project_path(&self) -> Option> { - self.entry - .work_directory - .unrelativize(&self.entry.repo_path) - } -} - impl Deref for GitPanelEntry { type Target = worktree::StatusEntry; @@ -573,7 +565,7 @@ impl GitPanel { let mut work_directory = None; for repository in repositories { visible_worktree_entries.extend(repository.status()); - work_directory = Some(repository.clone()); + work_directory = Some(worktree::WorkDirectory::clone(repository)); } // let mut visible_worktree_entries = snapshot @@ -641,7 +633,7 @@ impl GitPanel { project.update(cx, |project, cx| { let entry_path = ProjectPath { worktree_id: worktree_entries.worktree_id, - path: entry.project_path(), + path: worktree_entries.work_directory.unrelativize(&entry.repo_path)?, }; let open_task = project.open_path(entry_path.clone(), cx); @@ -707,8 +699,8 @@ impl GitPanel { ) .collect() } - // TODO support conflicts display - GitFileStatus::Conflict => Vec::new(), + // TODO support these + GitFileStatus::Conflict | GitFileStatus::Deleted | GitFileStatus::Untracked => Vec::new(), } }).clone() })?; @@ -1043,7 +1035,8 @@ impl GitPanel { this.child(git_status_icon(status)) }) .child( - ListItem::new(("label", id)) + // FIXME is it okay to use ix here? do we need a proper ID for git status entries? + ListItem::new(("label", ix)) .toggle_state(selected) .child(h_flex().gap_1p5().child(details.display_name.clone())) .on_click(move |e, cx| {