@@ -17,7 +17,7 @@ use futures::{
},
select_biased,
task::Poll,
- Stream, StreamExt,
+ FutureExt, Stream, StreamExt,
};
use fuzzy::CharBag;
use git::{DOT_GIT, GITIGNORE};
@@ -55,7 +55,7 @@ use std::{
time::{Duration, SystemTime},
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt};
+use util::{paths::HOME, ResultExt, TakeUntilExt};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
@@ -363,7 +363,7 @@ enum ScanState {
Started,
Updated {
snapshot: LocalSnapshot,
- changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+ changes: UpdatedEntriesSet,
barrier: Option<barrier::Sender>,
scanning: bool,
},
@@ -371,14 +371,15 @@ enum ScanState {
struct ShareState {
project_id: u64,
- snapshots_tx: watch::Sender<LocalSnapshot>,
+ snapshots_tx:
+ mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>,
resume_updates: watch::Sender<()>,
_maintain_remote_snapshot: Task<Option<()>>,
}
pub enum Event {
- UpdatedEntries(Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>),
- UpdatedGitRepositories(HashMap<Arc<Path>, LocalRepositoryEntry>),
+ UpdatedEntries(UpdatedEntriesSet),
+ UpdatedGitRepositories(UpdatedGitRepositoriesSet),
}
impl Entity for Worktree {
@@ -453,8 +454,7 @@ impl Worktree {
scanning,
} => {
*this.is_scanning.0.borrow_mut() = scanning;
- this.set_snapshot(snapshot, cx);
- cx.emit(Event::UpdatedEntries(changes));
+ this.set_snapshot(snapshot, changes, cx);
drop(barrier);
}
}
@@ -820,73 +820,109 @@ impl LocalWorktree {
Ok(!old_summary.is_empty() || !new_summary.is_empty())
}
- fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
- let updated_repos =
- self.changed_repos(&self.git_repositories, &new_snapshot.git_repositories);
+ fn set_snapshot(
+ &mut self,
+ new_snapshot: LocalSnapshot,
+ entry_changes: UpdatedEntriesSet,
+ cx: &mut ModelContext<Worktree>,
+ ) {
+ let repo_changes = self.changed_repos(&self.snapshot, &new_snapshot);
self.snapshot = new_snapshot;
if let Some(share) = self.share.as_mut() {
- *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
+ share
+ .snapshots_tx
+ .unbounded_send((
+ self.snapshot.clone(),
+ entry_changes.clone(),
+ repo_changes.clone(),
+ ))
+ .ok();
}
- if !updated_repos.is_empty() {
- cx.emit(Event::UpdatedGitRepositories(updated_repos));
+ if !entry_changes.is_empty() {
+ cx.emit(Event::UpdatedEntries(entry_changes));
+ }
+ if !repo_changes.is_empty() {
+ cx.emit(Event::UpdatedGitRepositories(repo_changes));
}
}
fn changed_repos(
&self,
- old_repos: &TreeMap<ProjectEntryId, LocalRepositoryEntry>,
- new_repos: &TreeMap<ProjectEntryId, LocalRepositoryEntry>,
- ) -> HashMap<Arc<Path>, LocalRepositoryEntry> {
- let mut diff = HashMap::default();
- let mut old_repos = old_repos.iter().peekable();
- let mut new_repos = new_repos.iter().peekable();
+ old_snapshot: &LocalSnapshot,
+ new_snapshot: &LocalSnapshot,
+ ) -> UpdatedGitRepositoriesSet {
+ let mut changes = Vec::new();
+ let mut old_repos = old_snapshot.git_repositories.iter().peekable();
+ let mut new_repos = new_snapshot.git_repositories.iter().peekable();
loop {
- match (old_repos.peek(), new_repos.peek()) {
- (Some((old_entry_id, old_repo)), Some((new_entry_id, new_repo))) => {
- match Ord::cmp(old_entry_id, new_entry_id) {
+ match (new_repos.peek().map(clone), old_repos.peek().map(clone)) {
+ (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => {
+ match Ord::cmp(&new_entry_id, &old_entry_id) {
Ordering::Less => {
- if let Some(entry) = self.entry_for_id(**old_entry_id) {
- diff.insert(entry.path.clone(), (*old_repo).clone());
+ if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
+ changes.push((entry.path.clone(), None));
}
- old_repos.next();
+ new_repos.next();
}
Ordering::Equal => {
- if old_repo.git_dir_scan_id != new_repo.git_dir_scan_id {
- if let Some(entry) = self.entry_for_id(**new_entry_id) {
- diff.insert(entry.path.clone(), (*new_repo).clone());
+ 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) {
+ changes.push((
+ entry.path.clone(),
+ old_snapshot
+ .repository_entries
+ .get(&RepositoryWorkDirectory(entry.path.clone()))
+ .cloned(),
+ ));
}
}
-
- old_repos.next();
new_repos.next();
+ old_repos.next();
}
Ordering::Greater => {
- if let Some(entry) = self.entry_for_id(**new_entry_id) {
- diff.insert(entry.path.clone(), (*new_repo).clone());
+ if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) {
+ changes.push((
+ entry.path.clone(),
+ old_snapshot
+ .repository_entries
+ .get(&RepositoryWorkDirectory(entry.path.clone()))
+ .cloned(),
+ ));
}
- new_repos.next();
+ old_repos.next();
}
}
}
- (Some((old_entry_id, old_repo)), None) => {
- if let Some(entry) = self.entry_for_id(**old_entry_id) {
- diff.insert(entry.path.clone(), (*old_repo).clone());
+ (Some((entry_id, _)), None) => {
+ if let Some(entry) = new_snapshot.entry_for_id(entry_id) {
+ changes.push((entry.path.clone(), None));
}
- old_repos.next();
+ new_repos.next();
}
- (None, Some((new_entry_id, new_repo))) => {
- if let Some(entry) = self.entry_for_id(**new_entry_id) {
- diff.insert(entry.path.clone(), (*new_repo).clone());
+ (None, Some((entry_id, _))) => {
+ if let Some(entry) = old_snapshot.entry_for_id(entry_id) {
+ changes.push((
+ entry.path.clone(),
+ old_snapshot
+ .repository_entries
+ .get(&RepositoryWorkDirectory(entry.path.clone()))
+ .cloned(),
+ ));
}
- new_repos.next();
+ old_repos.next();
}
(None, None) => break,
}
}
- diff
+
+ fn clone<T: Clone, U: Clone>(value: &(&T, &U)) -> (T, U) {
+ (value.0.clone(), value.1.clone())
+ }
+
+ changes.into()
}
pub fn scan_complete(&self) -> impl Future<Output = ()> {
@@ -1227,89 +1263,97 @@ impl LocalWorktree {
})
}
- pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
+ pub fn observe_updates<F, Fut>(
+ &mut self,
+ project_id: u64,
+ cx: &mut ModelContext<Worktree>,
+ callback: F,
+ ) -> oneshot::Receiver<()>
+ where
+ F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
+ Fut: Send + Future<Output = bool>,
+ {
+ #[cfg(any(test, feature = "test-support"))]
+ const MAX_CHUNK_SIZE: usize = 2;
+ #[cfg(not(any(test, feature = "test-support")))]
+ const MAX_CHUNK_SIZE: usize = 256;
+
let (share_tx, share_rx) = oneshot::channel();
if let Some(share) = self.share.as_mut() {
- let _ = share_tx.send(());
+ share_tx.send(()).ok();
*share.resume_updates.borrow_mut() = ();
- } else {
- let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot());
- let (resume_updates_tx, mut resume_updates_rx) = watch::channel();
- let worktree_id = cx.model_id() as u64;
+ return share_rx;
+ }
- for (path, summaries) in &self.diagnostic_summaries {
- for (&server_id, summary) in summaries {
- if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
- project_id,
- worktree_id,
- summary: Some(summary.to_proto(server_id, &path)),
- }) {
- return Task::ready(Err(e));
- }
+ let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>();
+ let (snapshots_tx, mut snapshots_rx) =
+ mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>();
+ snapshots_tx
+ .unbounded_send((self.snapshot(), Arc::from([]), Arc::from([])))
+ .ok();
+
+ let worktree_id = cx.model_id() as u64;
+ let _maintain_remote_snapshot = cx.background().spawn(async move {
+ let mut is_first = true;
+ while let Some((snapshot, entry_changes, repo_changes)) = snapshots_rx.next().await {
+ let update;
+ if is_first {
+ update = snapshot.build_initial_update(project_id, worktree_id);
+ is_first = false;
+ } else {
+ update =
+ snapshot.build_update(project_id, worktree_id, entry_changes, repo_changes);
}
- }
- let _maintain_remote_snapshot = cx.background().spawn({
- let client = self.client.clone();
- async move {
- let mut share_tx = Some(share_tx);
- let mut prev_snapshot = LocalSnapshot {
- ignores_by_parent_abs_path: Default::default(),
- git_repositories: Default::default(),
- snapshot: Snapshot {
- id: WorktreeId(worktree_id as usize),
- abs_path: Path::new("").into(),
- root_name: Default::default(),
- root_char_bag: Default::default(),
- entries_by_path: Default::default(),
- entries_by_id: Default::default(),
- repository_entries: Default::default(),
- scan_id: 0,
- completed_scan_id: 0,
- },
- };
- while let Some(snapshot) = snapshots_rx.recv().await {
- #[cfg(any(test, feature = "test-support"))]
- const MAX_CHUNK_SIZE: usize = 2;
- #[cfg(not(any(test, feature = "test-support")))]
- const MAX_CHUNK_SIZE: usize = 256;
-
- let update =
- snapshot.build_update(&prev_snapshot, project_id, worktree_id, true);
- for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) {
- let _ = resume_updates_rx.try_recv();
- while let Err(error) = client.request(update.clone()).await {
- log::error!("failed to send worktree update: {}", error);
- log::info!("waiting to resume updates");
- if resume_updates_rx.next().await.is_none() {
- return Ok(());
- }
+ for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) {
+ let _ = resume_updates_rx.try_recv();
+ loop {
+ let result = callback(update.clone());
+ if result.await {
+ break;
+ } else {
+ log::info!("waiting to resume updates");
+ if resume_updates_rx.next().await.is_none() {
+ return Some(());
}
}
+ }
+ }
+ }
+ share_tx.send(()).ok();
+ Some(())
+ });
- if let Some(share_tx) = share_tx.take() {
- let _ = share_tx.send(());
- }
+ self.share = Some(ShareState {
+ project_id,
+ snapshots_tx,
+ resume_updates: resume_updates_tx,
+ _maintain_remote_snapshot,
+ });
+ share_rx
+ }
- prev_snapshot = snapshot;
- }
+ pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
+ let client = self.client.clone();
- Ok::<_, anyhow::Error>(())
+ for (path, summaries) in &self.diagnostic_summaries {
+ for (&server_id, summary) in summaries {
+ if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
+ project_id,
+ worktree_id: cx.model_id() as u64,
+ summary: Some(summary.to_proto(server_id, &path)),
+ }) {
+ return Task::ready(Err(e));
}
- .log_err()
- });
-
- self.share = Some(ShareState {
- project_id,
- snapshots_tx,
- resume_updates: resume_updates_tx,
- _maintain_remote_snapshot,
- });
+ }
}
+ let rx = self.observe_updates(project_id, cx, move |update| {
+ client.request(update).map(|result| result.is_ok())
+ });
cx.foreground()
- .spawn(async move { share_rx.await.map_err(|_| anyhow!("share ended")) })
+ .spawn(async move { rx.await.map_err(|_| anyhow!("share ended")) })
}
pub fn unshare(&mut self) {
@@ -1518,10 +1562,12 @@ impl Snapshot {
pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> {
let mut entries_by_path_edits = Vec::new();
let mut entries_by_id_edits = Vec::new();
+
for entry_id in update.removed_entries {
- if let Some(entry) = self.entry_for_id(ProjectEntryId::from_proto(entry_id)) {
+ let entry_id = ProjectEntryId::from_proto(entry_id);
+ entries_by_id_edits.push(Edit::Remove(entry_id));
+ if let Some(entry) = self.entry_for_id(entry_id) {
entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
- entries_by_id_edits.push(Edit::Remove(entry.id));
}
}
@@ -1530,6 +1576,11 @@ impl Snapshot {
if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) {
entries_by_path_edits.push(Edit::Remove(PathKey(path.clone())));
}
+ if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) {
+ if old_entry.id != entry.id {
+ entries_by_id_edits.push(Edit::Remove(old_entry.id));
+ }
+ }
entries_by_id_edits.push(Edit::Insert(PathEntry {
id: entry.id,
path: entry.path.clone(),
@@ -1672,20 +1723,19 @@ impl Snapshot {
/// Get the repository whose work directory contains the given path.
pub fn repository_for_path(&self, path: &Path) -> Option<RepositoryEntry> {
- let mut max_len = 0;
- let mut current_candidate = None;
- for (work_directory, repo) in (&self.repository_entries).iter() {
- if path.starts_with(&work_directory.0) {
- if work_directory.0.as_os_str().len() >= max_len {
- current_candidate = Some(repo);
- max_len = work_directory.0.as_os_str().len();
- } else {
- break;
- }
- }
- }
+ self.repository_and_work_directory_for_path(path)
+ .map(|e| e.1)
+ }
- current_candidate.cloned()
+ pub fn repository_and_work_directory_for_path(
+ &self,
+ path: &Path,
+ ) -> Option<(RepositoryWorkDirectory, RepositoryEntry)> {
+ self.repository_entries
+ .iter()
+ .filter(|(workdir_path, _)| path.starts_with(workdir_path))
+ .last()
+ .map(|(path, repo)| (path.clone(), repo.clone()))
}
/// Given an ordered iterator of entries, returns an iterator of those entries,
@@ -1821,117 +1871,51 @@ impl LocalSnapshot {
.find(|(_, repo)| repo.in_dot_git(path))
}
- #[cfg(test)]
- pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
- let root_name = self.root_name.clone();
- proto::UpdateWorktree {
- project_id,
- worktree_id: self.id().to_proto(),
- abs_path: self.abs_path().to_string_lossy().into(),
- root_name,
- updated_entries: self.entries_by_path.iter().map(Into::into).collect(),
- removed_entries: Default::default(),
- scan_id: self.scan_id as u64,
- is_last_update: true,
- updated_repositories: self.repository_entries.values().map(Into::into).collect(),
- removed_repositories: Default::default(),
- }
- }
-
- pub(crate) fn build_update(
+ fn build_update(
&self,
- other: &Self,
project_id: u64,
worktree_id: u64,
- include_ignored: bool,
+ entry_changes: UpdatedEntriesSet,
+ repo_changes: UpdatedGitRepositoriesSet,
) -> proto::UpdateWorktree {
let mut updated_entries = Vec::new();
let mut removed_entries = Vec::new();
- let mut self_entries = self
- .entries_by_id
- .cursor::<()>()
- .filter(|e| include_ignored || !e.is_ignored)
- .peekable();
- let mut other_entries = other
- .entries_by_id
- .cursor::<()>()
- .filter(|e| include_ignored || !e.is_ignored)
- .peekable();
- loop {
- match (self_entries.peek(), other_entries.peek()) {
- (Some(self_entry), Some(other_entry)) => {
- match Ord::cmp(&self_entry.id, &other_entry.id) {
- Ordering::Less => {
- let entry = self.entry_for_id(self_entry.id).unwrap().into();
- updated_entries.push(entry);
- self_entries.next();
- }
- Ordering::Equal => {
- if self_entry.scan_id != other_entry.scan_id {
- let entry = self.entry_for_id(self_entry.id).unwrap().into();
- updated_entries.push(entry);
- }
+ let mut updated_repositories = Vec::new();
+ let mut removed_repositories = Vec::new();
- self_entries.next();
- other_entries.next();
- }
- Ordering::Greater => {
- removed_entries.push(other_entry.id.to_proto());
- other_entries.next();
- }
- }
- }
- (Some(self_entry), None) => {
- let entry = self.entry_for_id(self_entry.id).unwrap().into();
- updated_entries.push(entry);
- self_entries.next();
- }
- (None, Some(other_entry)) => {
- removed_entries.push(other_entry.id.to_proto());
- other_entries.next();
- }
- (None, None) => break,
+ for (_, entry_id, path_change) in entry_changes.iter() {
+ if let PathChange::Removed = path_change {
+ removed_entries.push(entry_id.0 as u64);
+ } else if let Some(entry) = self.entry_for_id(*entry_id) {
+ updated_entries.push(proto::Entry::from(entry));
}
}
-
- let mut updated_repositories: Vec<proto::RepositoryEntry> = Vec::new();
- let mut removed_repositories = Vec::new();
- let mut self_repos = self.snapshot.repository_entries.iter().peekable();
- let mut other_repos = other.snapshot.repository_entries.iter().peekable();
- loop {
- match (self_repos.peek(), other_repos.peek()) {
- (Some((self_work_dir, self_repo)), Some((other_work_dir, other_repo))) => {
- match Ord::cmp(self_work_dir, other_work_dir) {
- Ordering::Less => {
- updated_repositories.push((*self_repo).into());
- self_repos.next();
- }
- Ordering::Equal => {
- if self_repo != other_repo {
- updated_repositories.push(self_repo.build_update(other_repo));
- }
-
- self_repos.next();
- other_repos.next();
- }
- Ordering::Greater => {
- removed_repositories.push(other_repo.work_directory.to_proto());
- other_repos.next();
- }
- }
+ for (work_dir_path, old_repo) in repo_changes.iter() {
+ let new_repo = self
+ .repository_entries
+ .get(&RepositoryWorkDirectory(work_dir_path.clone()));
+ match (old_repo, new_repo) {
+ (Some(old_repo), Some(new_repo)) => {
+ updated_repositories.push(new_repo.build_update(old_repo));
}
- (Some((_, self_repo)), None) => {
- updated_repositories.push((*self_repo).into());
- self_repos.next();
+ (None, Some(new_repo)) => {
+ updated_repositories.push(proto::RepositoryEntry::from(new_repo));
}
- (None, Some((_, other_repo))) => {
- removed_repositories.push(other_repo.work_directory.to_proto());
- other_repos.next();
+ (Some(old_repo), None) => {
+ removed_repositories.push(old_repo.work_directory.0.to_proto());
}
- (None, None) => break,
+ _ => {}
}
}
+ removed_entries.sort_unstable();
+ updated_entries.sort_unstable_by_key(|e| e.id);
+ removed_repositories.sort_unstable();
+ updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
+
+ // TODO - optimize, knowing that removed_entries are sorted.
+ removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err());
+
proto::UpdateWorktree {
project_id,
worktree_id,
@@ -1946,6 +1930,35 @@ impl LocalSnapshot {
}
}
+ fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
+ let mut updated_entries = self
+ .entries_by_path
+ .iter()
+ .map(proto::Entry::from)
+ .collect::<Vec<_>>();
+ updated_entries.sort_unstable_by_key(|e| e.id);
+
+ let mut updated_repositories = self
+ .repository_entries
+ .values()
+ .map(proto::RepositoryEntry::from)
+ .collect::<Vec<_>>();
+ updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
+
+ proto::UpdateWorktree {
+ project_id,
+ worktree_id,
+ abs_path: self.abs_path().to_string_lossy().into(),
+ root_name: self.root_name().to_string(),
+ updated_entries,
+ removed_entries: Vec::new(),
+ scan_id: self.scan_id as u64,
+ is_last_update: self.completed_scan_id == self.scan_id,
+ updated_repositories,
+ removed_repositories: Vec::new(),
+ }
+ }
+
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
let abs_path = self.abs_path.join(&entry.path);
@@ -2481,6 +2494,9 @@ pub enum PathChange {
Loaded,
}
+pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
+pub type UpdatedGitRepositoriesSet = Arc<[(Arc<Path>, Option<RepositoryEntry>)]>;
+
impl Entry {
fn new(
path: Arc<Path>,
@@ -2896,11 +2912,13 @@ impl BackgroundScanner {
fn send_status_update(&self, scanning: bool, barrier: Option<barrier::Sender>) -> bool {
let mut state = self.state.lock();
+ if state.changed_paths.is_empty() && scanning {
+ return true;
+ }
+
let new_snapshot = state.snapshot.clone();
let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone());
-
- let changes =
- self.build_change_set(&old_snapshot, &new_snapshot.snapshot, &state.changed_paths);
+ let changes = self.build_change_set(&old_snapshot, &new_snapshot, &state.changed_paths);
state.changed_paths.clear();
self.status_updates_tx
@@ -3386,9 +3404,20 @@ impl BackgroundScanner {
}
}
- let snapshot = &mut self.state.lock().snapshot;
- snapshot.entries_by_path.edit(entries_by_path_edits, &());
- snapshot.entries_by_id.edit(entries_by_id_edits, &());
+ let state = &mut self.state.lock();
+ for edit in &entries_by_path_edits {
+ if let Edit::Insert(entry) = edit {
+ if let Err(ix) = state.changed_paths.binary_search(&entry.path) {
+ state.changed_paths.insert(ix, entry.path.clone());
+ }
+ }
+ }
+
+ state
+ .snapshot
+ .entries_by_path
+ .edit(entries_by_path_edits, &());
+ state.snapshot.entries_by_id.edit(entries_by_id_edits, &());
}
fn build_change_set(
@@ -3396,16 +3425,17 @@ impl BackgroundScanner {
old_snapshot: &Snapshot,
new_snapshot: &Snapshot,
event_paths: &[Arc<Path>],
- ) -> Arc<[(Arc<Path>, ProjectEntryId, PathChange)]> {
+ ) -> UpdatedEntriesSet {
use BackgroundScannerPhase::*;
use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated};
+ // Identify which paths have changed. Use the known set of changed
+ // parent paths to optimize the search.
+ let mut changes = Vec::new();
let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>();
let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>();
old_paths.next(&());
new_paths.next(&());
-
- let mut changes = Vec::new();
for path in event_paths {
let path = PathKey(path.clone());
if old_paths.item().map_or(false, |e| e.path < path.0) {
@@ -3441,7 +3471,10 @@ impl BackgroundScanner {
new_entry.id,
AddedOrUpdated,
));
- } else if old_entry.mtime != new_entry.mtime {
+ } else if old_entry.id != new_entry.id {
+ changes.push((old_entry.path.clone(), old_entry.id, Removed));
+ changes.push((new_entry.path.clone(), new_entry.id, Added));
+ } else if old_entry != new_entry {
changes.push((new_entry.path.clone(), new_entry.id, Updated));
}
old_paths.next(&());
@@ -3543,8 +3576,6 @@ impl WorktreeHandle for ModelHandle<Worktree> {
&self,
cx: &'a gpui::TestAppContext,
) -> futures::future::LocalBoxFuture<'a, ()> {
- use smol::future::FutureExt;
-
let filename = "fs-event-sentinel";
let tree = self.clone();
let (fs, root_path) = self.read_with(cx, |tree, _| {
@@ -4207,7 +4238,18 @@ mod tests {
.await
.unwrap();
- let mut snapshot1 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ let snapshot1 = tree.update(cx, |tree, cx| {
+ let tree = tree.as_local_mut().unwrap();
+ let snapshot = Arc::new(Mutex::new(tree.snapshot()));
+ let _ = tree.observe_updates(0, cx, {
+ let snapshot = snapshot.clone();
+ move |update| {
+ snapshot.lock().apply_remote_update(update).unwrap();
+ async { true }
+ }
+ });
+ snapshot
+ });
let entry = tree
.update(cx, |tree, cx| {
@@ -4225,9 +4267,10 @@ mod tests {
});
let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
- let update = snapshot2.build_update(&snapshot1, 0, 0, true);
- snapshot1.apply_remote_update(update).unwrap();
- assert_eq!(snapshot1.to_vec(true), snapshot2.to_vec(true),);
+ assert_eq!(
+ snapshot1.lock().entries(true).collect::<Vec<_>>(),
+ snapshot2.entries(true).collect::<Vec<_>>()
+ );
}
#[gpui::test(iterations = 100)]
@@ -4262,7 +4305,20 @@ mod tests {
.await
.unwrap();
- let mut snapshot = worktree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ let mut snapshots =
+ vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
+ let updates = Arc::new(Mutex::new(Vec::new()));
+ worktree.update(cx, |tree, cx| {
+ check_worktree_change_events(tree, cx);
+
+ let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
+ let updates = updates.clone();
+ move |update| {
+ updates.lock().push(update);
+ async { true }
+ }
+ });
+ });
for _ in 0..operations {
worktree
@@ -4276,35 +4332,39 @@ mod tests {
});
if rng.gen_bool(0.6) {
- let new_snapshot =
- worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
- let update = new_snapshot.build_update(&snapshot, 0, 0, true);
- snapshot.apply_remote_update(update.clone()).unwrap();
- assert_eq!(
- snapshot.to_vec(true),
- new_snapshot.to_vec(true),
- "incorrect snapshot after update {:?}",
- update
- );
+ snapshots
+ .push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
}
}
worktree
.update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
.await;
- worktree.read_with(cx, |tree, _| {
- tree.as_local().unwrap().snapshot.check_invariants()
+
+ cx.foreground().run_until_parked();
+
+ let final_snapshot = worktree.read_with(cx, |tree, _| {
+ let tree = tree.as_local().unwrap();
+ tree.snapshot.check_invariants();
+ tree.snapshot()
});
- let new_snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
- let update = new_snapshot.build_update(&snapshot, 0, 0, true);
- snapshot.apply_remote_update(update.clone()).unwrap();
- assert_eq!(
- snapshot.to_vec(true),
- new_snapshot.to_vec(true),
- "incorrect snapshot after update {:?}",
- update
- );
+ for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
+ let mut updated_snapshot = snapshot.clone();
+ for update in updates.lock().iter() {
+ if update.scan_id >= updated_snapshot.scan_id() as u64 {
+ updated_snapshot
+ .apply_remote_update(update.clone())
+ .unwrap();
+ }
+ }
+
+ assert_eq!(
+ updated_snapshot.entries(true).collect::<Vec<_>>(),
+ final_snapshot.entries(true).collect::<Vec<_>>(),
+ "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
+ );
+ }
}
#[gpui::test(iterations = 100)]
@@ -4336,55 +4396,17 @@ mod tests {
.await
.unwrap();
- // The worktree's `UpdatedEntries` event can be used to follow along with
- // all changes to the worktree's snapshot.
+ let updates = Arc::new(Mutex::new(Vec::new()));
worktree.update(cx, |tree, cx| {
- let mut paths = tree
- .entries(true)
- .map(|e| (e.path.clone(), e.mtime))
- .collect::<Vec<_>>();
+ check_worktree_change_events(tree, cx);
- cx.subscribe(&worktree, move |tree, _, event, _| {
- if let Event::UpdatedEntries(changes) = event {
- for (path, _, change_type) in changes.iter() {
- let mtime = tree.entry_for_path(&path).map(|e| e.mtime);
- let path = path.clone();
- let ix = match paths.binary_search_by_key(&&path, |e| &e.0) {
- Ok(ix) | Err(ix) => ix,
- };
- match change_type {
- PathChange::Loaded => {
- paths.insert(ix, (path, mtime.unwrap()));
- }
- PathChange::Added => {
- paths.insert(ix, (path, mtime.unwrap()));
- }
- PathChange::Removed => {
- paths.remove(ix);
- }
- PathChange::Updated => {
- let entry = paths.get_mut(ix).unwrap();
- assert_eq!(entry.0, path);
- entry.1 = mtime.unwrap();
- }
- PathChange::AddedOrUpdated => {
- if paths.get(ix).map(|e| &e.0) == Some(&path) {
- paths.get_mut(ix).unwrap().1 = mtime.unwrap();
- } else {
- paths.insert(ix, (path, mtime.unwrap()));
- }
- }
- }
- }
-
- let new_paths = tree
- .entries(true)
- .map(|e| (e.path.clone(), e.mtime))
- .collect::<Vec<_>>();
- assert_eq!(paths, new_paths, "incorrect changes: {:?}", changes);
+ let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
+ let updates = updates.clone();
+ move |update| {
+ updates.lock().push(update);
+ async { true }
}
- })
- .detach();
+ });
});
worktree
@@ -4447,40 +4469,66 @@ mod tests {
.await;
let new_snapshot =
new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
- assert_eq!(snapshot.to_vec(true), new_snapshot.to_vec(true));
+ assert_eq!(
+ snapshot.entries_without_ids(true),
+ new_snapshot.entries_without_ids(true)
+ );
}
- for (i, mut prev_snapshot) in snapshots.into_iter().enumerate() {
- let include_ignored = rng.gen::<bool>();
- if !include_ignored {
- let mut entries_by_path_edits = Vec::new();
- let mut entries_by_id_edits = Vec::new();
- for entry in prev_snapshot
- .entries_by_id
- .cursor::<()>()
- .filter(|e| e.is_ignored)
- {
- entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
- entries_by_id_edits.push(Edit::Remove(entry.id));
+ for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
+ for update in updates.lock().iter() {
+ if update.scan_id >= prev_snapshot.scan_id() as u64 {
+ prev_snapshot.apply_remote_update(update.clone()).unwrap();
}
-
- prev_snapshot
- .entries_by_path
- .edit(entries_by_path_edits, &());
- prev_snapshot.entries_by_id.edit(entries_by_id_edits, &());
}
- let update = snapshot.build_update(&prev_snapshot, 0, 0, include_ignored);
- prev_snapshot.apply_remote_update(update.clone()).unwrap();
assert_eq!(
- prev_snapshot.to_vec(include_ignored),
- snapshot.to_vec(include_ignored),
- "wrong update for snapshot {i}. update: {:?}",
- update
+ prev_snapshot.entries(true).collect::<Vec<_>>(),
+ snapshot.entries(true).collect::<Vec<_>>(),
+ "wrong updates after snapshot {i}: {updates:#?}",
);
}
}
+ // The worktree's `UpdatedEntries` event can be used to follow along with
+ // all changes to the worktree's snapshot.
+ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
+ let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
+ cx.subscribe(&cx.handle(), move |tree, _, event, _| {
+ if let Event::UpdatedEntries(changes) = event {
+ for (path, _, change_type) in changes.iter() {
+ let entry = tree.entry_for_path(&path).cloned();
+ let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
+ Ok(ix) | Err(ix) => ix,
+ };
+ match change_type {
+ PathChange::Loaded => entries.insert(ix, entry.unwrap()),
+ PathChange::Added => entries.insert(ix, entry.unwrap()),
+ PathChange::Removed => drop(entries.remove(ix)),
+ PathChange::Updated => {
+ let entry = entry.unwrap();
+ let existing_entry = entries.get_mut(ix).unwrap();
+ assert_eq!(existing_entry.path, entry.path);
+ *existing_entry = entry;
+ }
+ PathChange::AddedOrUpdated => {
+ let entry = entry.unwrap();
+ if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
+ *entries.get_mut(ix).unwrap() = entry;
+ } else {
+ entries.insert(ix, entry);
+ }
+ }
+ }
+ }
+
+ let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
+ assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
+ }
+ })
+ .detach();
+ }
+
fn randomly_mutate_worktree(
worktree: &mut Worktree,
rng: &mut impl Rng,
@@ -4772,7 +4820,7 @@ mod tests {
}
}
- fn to_vec(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> {
+ fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> {
let mut paths = Vec::new();
for entry in self.entries_by_path.cursor::<()>() {
if include_ignored || !entry.is_ignored {
@@ -4964,8 +5012,8 @@ mod tests {
assert_eq!(
repo_update_events.lock()[0]
- .keys()
- .cloned()
+ .iter()
+ .map(|e| e.0.clone())
.collect::<Vec<Arc<Path>>>(),
vec![Path::new("dir1").into()]
);