From 5c859da4572d892a7df712dde1a0eb37d68f83b7 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 4 May 2023 12:03:10 -0400 Subject: [PATCH 01/97] Only update changed local worktree buffers Co-Authored-By: Antonio Scandurra --- crates/project/src/project.rs | 165 ++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 67 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b3d432763e..f4b5e728fb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -122,6 +122,7 @@ pub struct Project { loading_local_worktrees: HashMap, Shared, Arc>>>>, opened_buffers: HashMap, + local_buffer_ids_by_path: HashMap, /// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it. /// Used for re-issuing buffer requests when peers temporarily disconnect incomplete_remote_buffers: HashMap>>, @@ -449,6 +450,7 @@ impl Project { incomplete_remote_buffers: Default::default(), loading_buffers_by_path: Default::default(), loading_local_worktrees: Default::default(), + local_buffer_ids_by_path: Default::default(), buffer_snapshots: Default::default(), join_project_response_message_id: 0, client_state: None, @@ -517,6 +519,7 @@ impl Project { shared_buffers: Default::default(), incomplete_remote_buffers: Default::default(), loading_local_worktrees: Default::default(), + local_buffer_ids_by_path: Default::default(), active_entry: None, collaborators: Default::default(), join_project_response_message_id: response.message_id, @@ -1628,6 +1631,18 @@ impl Project { }) .detach(); + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if file.is_local { + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); + } + } + self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_servers(buffer, cx); self.register_buffer_with_copilot(buffer, cx); @@ -4525,7 +4540,7 @@ impl Project { if worktree.read(cx).is_local() { cx.subscribe(worktree, |this, worktree, event, cx| match event { worktree::Event::UpdatedEntries(changes) => { - this.update_local_worktree_buffers(&worktree, cx); + this.update_local_worktree_buffers(&worktree, &changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx); } worktree::Event::UpdatedGitRepositories(updated_repos) => { @@ -4559,82 +4574,98 @@ impl Project { fn update_local_worktree_buffers( &mut self, worktree_handle: &ModelHandle, + changes: &HashMap, PathChange>, cx: &mut ModelContext, ) { let snapshot = worktree_handle.read(cx).snapshot(); - let mut buffers_to_delete = Vec::new(); let mut renamed_buffers = Vec::new(); + for path in changes.keys() { + let worktree_id = worktree_handle.read(cx).id(); + let project_path = ProjectPath { + worktree_id, + path: path.clone(), + }; - for (buffer_id, buffer) in &self.opened_buffers { - if let Some(buffer) = buffer.upgrade(cx) { - buffer.update(cx, |buffer, cx| { - if let Some(old_file) = File::from_dyn(buffer.file()) { - if old_file.worktree != *worktree_handle { - return; + if let Some(&buffer_id) = self.local_buffer_ids_by_path.get(&project_path) { + if let Some(buffer) = self + .opened_buffers + .get(&buffer_id) + .and_then(|buffer| buffer.upgrade(cx)) + { + buffer.update(cx, |buffer, cx| { + if let Some(old_file) = File::from_dyn(buffer.file()) { + if old_file.worktree != *worktree_handle { + return; + } + + let new_file = + if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, + } + } else if let Some(entry) = + snapshot.entry_for_path(old_file.path().as_ref()) + { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, + } + } else { + File { + is_local: true, + entry_id: old_file.entry_id, + path: old_file.path().clone(), + mtime: old_file.mtime(), + worktree: worktree_handle.clone(), + is_deleted: true, + } + }; + + let old_path = old_file.abs_path(cx); + if new_file.abs_path(cx) != old_path { + renamed_buffers.push((cx.handle(), old_file.clone())); + self.local_buffer_ids_by_path.remove(&project_path); + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id, + path: path.clone(), + }, + buffer_id, + ); + } + + if new_file != *old_file { + if let Some(project_id) = self.remote_id() { + self.client + .send(proto::UpdateBufferFile { + project_id, + buffer_id: buffer_id as u64, + file: Some(new_file.to_proto()), + }) + .log_err(); + } + + buffer.file_updated(Arc::new(new_file), cx).detach(); + } } - - let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) - { - File { - is_local: true, - entry_id: entry.id, - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree_handle.clone(), - is_deleted: false, - } - } else if let Some(entry) = - snapshot.entry_for_path(old_file.path().as_ref()) - { - File { - is_local: true, - entry_id: entry.id, - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree_handle.clone(), - is_deleted: false, - } - } else { - File { - is_local: true, - entry_id: old_file.entry_id, - path: old_file.path().clone(), - mtime: old_file.mtime(), - worktree: worktree_handle.clone(), - is_deleted: true, - } - }; - - let old_path = old_file.abs_path(cx); - if new_file.abs_path(cx) != old_path { - renamed_buffers.push((cx.handle(), old_file.clone())); - } - - if new_file != *old_file { - if let Some(project_id) = self.remote_id() { - self.client - .send(proto::UpdateBufferFile { - project_id, - buffer_id: *buffer_id as u64, - file: Some(new_file.to_proto()), - }) - .log_err(); - } - - buffer.file_updated(Arc::new(new_file), cx).detach(); - } - } - }); - } else { - buffers_to_delete.push(*buffer_id); + }); + } else { + self.opened_buffers.remove(&buffer_id); + self.local_buffer_ids_by_path.remove(&project_path); + } } } - for buffer_id in buffers_to_delete { - self.opened_buffers.remove(&buffer_id); - } - for (buffer, old_file) in renamed_buffers { self.unregister_buffer_from_language_servers(&buffer, &old_file, cx); self.detect_language_for_buffer(&buffer, cx); From 7f27d72b200cfd6acd2d0fc93de5c58d016b35a6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 9 May 2023 16:55:03 +0200 Subject: [PATCH 02/97] Deliver file-system change events in batches in randomized worktree test Co-Authored-By: Julia Risley --- crates/project/src/worktree.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 554304f3d3..cc326690ec 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3944,6 +3944,7 @@ mod tests { let mut snapshots = Vec::new(); let mut mutations_len = operations; + fs.as_fake().pause_events().await; while mutations_len > 1 { randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; let buffered_event_count = fs.as_fake().buffered_event_count().await; From 48ad3866b7ea01b0eb8662192d8d3ffefbb80ab1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 9 May 2023 17:01:11 +0200 Subject: [PATCH 03/97] Randomly mutate worktree in addition to mutating the file-system This ensures that we test the code path that refreshes entries. Co-Authored-By: Julia Risley --- crates/project/src/worktree.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index cc326690ec..bdbfff9a06 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3946,7 +3946,17 @@ mod tests { let mut mutations_len = operations; fs.as_fake().pause_events().await; while mutations_len > 1 { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; + if rng.gen_bool(0.2) { + worktree + .update(cx, |worktree, cx| { + randomly_mutate_worktree(worktree, &mut rng, cx) + }) + .await + .unwrap(); + } else { + randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; + } + let buffered_event_count = fs.as_fake().buffered_event_count().await; if buffered_event_count > 0 && rng.gen_bool(0.3) { let len = rng.gen_range(0..=buffered_event_count); From 2bc7be9a764b0cd5014fb74d9918e30b82f2c0e5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 9 May 2023 17:14:33 +0200 Subject: [PATCH 04/97] WIP --- crates/project/src/worktree.rs | 40 ++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index bdbfff9a06..9d03169072 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2399,10 +2399,15 @@ struct BackgroundScanner { status_updates_tx: UnboundedSender, executor: Arc, refresh_requests_rx: channel::Receiver<(Vec, barrier::Sender)>, - prev_state: Mutex<(Snapshot, Vec>)>, + prev_state: Mutex, finished_initial_scan: bool, } +struct BackgroundScannerState { + snapshot: Snapshot, + event_paths: Vec>, +} + impl BackgroundScanner { fn new( snapshot: LocalSnapshot, @@ -2416,7 +2421,10 @@ impl BackgroundScanner { status_updates_tx, executor, refresh_requests_rx, - prev_state: Mutex::new((snapshot.snapshot.clone(), Vec::new())), + prev_state: Mutex::new(BackgroundScannerState { + snapshot: snapshot.snapshot.clone(), + event_paths: Default::default(), + }), snapshot: Mutex::new(snapshot), finished_initial_scan: false, } @@ -2526,7 +2534,12 @@ impl BackgroundScanner { .await { paths.sort_unstable(); - util::extend_sorted(&mut self.prev_state.lock().1, paths, usize::MAX, Ord::cmp); + util::extend_sorted( + &mut self.prev_state.lock().event_paths, + paths, + usize::MAX, + Ord::cmp, + ); } drop(scan_job_tx); self.scan_dirs(false, scan_job_rx).await; @@ -2560,6 +2573,7 @@ impl BackgroundScanner { drop(snapshot); self.send_status_update(false, None); + self.prev_state.lock().event_paths.clear(); } async fn scan_dirs( @@ -2637,14 +2651,18 @@ impl BackgroundScanner { fn send_status_update(&self, scanning: bool, barrier: Option) -> bool { let mut prev_state = self.prev_state.lock(); - let snapshot = self.snapshot.lock().clone(); - let mut old_snapshot = snapshot.snapshot.clone(); - mem::swap(&mut old_snapshot, &mut prev_state.0); - let changed_paths = mem::take(&mut prev_state.1); - let changes = self.build_change_set(&old_snapshot, &snapshot.snapshot, changed_paths); + let new_snapshot = self.snapshot.lock().clone(); + let old_snapshot = mem::replace(&mut prev_state.snapshot, new_snapshot.snapshot.clone()); + + let changes = self.build_change_set( + &old_snapshot, + &new_snapshot.snapshot, + &prev_state.event_paths, + ); + self.status_updates_tx .unbounded_send(ScanState::Updated { - snapshot, + snapshot: new_snapshot, changes, scanning, barrier, @@ -3012,7 +3030,7 @@ impl BackgroundScanner { &self, old_snapshot: &Snapshot, new_snapshot: &Snapshot, - event_paths: Vec>, + event_paths: &[Arc], ) -> HashMap, PathChange> { use PathChange::{Added, AddedOrUpdated, Removed, Updated}; @@ -3022,7 +3040,7 @@ impl BackgroundScanner { let received_before_initialized = !self.finished_initial_scan; for path in event_paths { - let path = PathKey(path); + let path = PathKey(path.clone()); old_paths.seek(&path, Bias::Left, &()); new_paths.seek(&path, Bias::Left, &()); From 9405b4995767cec489ef3c33aaec3d4afc293d29 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Wed, 10 May 2023 16:47:09 -0400 Subject: [PATCH 05/97] v0.87.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eee0873e5b..99b0479d1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8531,7 +8531,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zed" -version = "0.86.0" +version = "0.87.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 70c71cc18e..b74057c907 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.86.0" +version = "0.87.0" publish = false [lib] From 6385e51957590fc43b073bb6c48d2eeb3e5594f7 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Wed, 10 May 2023 18:16:20 -0400 Subject: [PATCH 06/97] collab 0.12.1 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99b0479d1d..bef04fce14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1189,7 +1189,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.12.0" +version = "0.12.1" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a980fdc13e..d4941438a0 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.12.0" +version = "0.12.1" publish = false [[bin]] From 7169f5c7609a93ad282aa0d4b5f9969ea6f447cc Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 08:36:43 -0700 Subject: [PATCH 07/97] Add git status to the file system abstraction co-authored-by: petros --- Cargo.lock | 1 + crates/fs/Cargo.toml | 1 + crates/fs/src/repository.rs | 82 +++++++++++++++++++++++++++++++++- crates/project/src/worktree.rs | 37 +++++---------- 4 files changed, 94 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bef04fce14..bd1dd4f33b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2350,6 +2350,7 @@ dependencies = [ "serde_derive", "serde_json", "smol", + "sum_tree", "tempfile", "util", ] diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index d080fe3cd1..54c6ce362a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -13,6 +13,7 @@ gpui = { path = "../gpui" } lsp = { path = "../lsp" } rope = { path = "../rope" } util = { path = "../util" } +sum_tree = { path = "../sum_tree" } anyhow.workspace = true async-trait.workspace = true futures.workspace = true diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 5624ce42f1..14e7e75a3d 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,9 +1,10 @@ use anyhow::Result; use collections::HashMap; use parking_lot::Mutex; +use sum_tree::TreeMap; use std::{ path::{Component, Path, PathBuf}, - sync::Arc, + sync::Arc, ffi::OsStr, os::unix::prelude::OsStrExt, }; use util::ResultExt; @@ -16,6 +17,8 @@ pub trait GitRepository: Send { fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; + + fn statuses(&self) -> Option>; } impl std::fmt::Debug for dyn GitRepository { @@ -61,6 +64,79 @@ impl GitRepository for LibGitRepository { let branch = String::from_utf8_lossy(head.shorthand_bytes()); Some(branch.to_string()) } + + fn statuses(&self) -> Option> { + let statuses = self.statuses(None).log_err()?; + + let mut map = TreeMap::default(); + + for status in statuses.iter() { + let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); + + let status_data = status.status(); + + let status = if status_data.contains(git2::Status::CONFLICTED) { + GitStatus::Conflict + } else if status_data.intersects(git2::Status::INDEX_MODIFIED + | git2::Status::WT_MODIFIED + | git2::Status::INDEX_RENAMED + | git2::Status::WT_RENAMED) { + GitStatus::Modified + } else if status_data.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) { + GitStatus::Added + } else { + GitStatus::Untracked + }; + + map.insert(path, status) + } + + Some(map) + } +} + +#[derive(Debug, Clone, Default)] +pub enum GitStatus { + Added, + Modified, + Conflict, + #[default] + Untracked, +} + +#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] +pub struct RepoPath(PathBuf); + +impl From<&Path> for RepoPath { + fn from(value: &Path) -> Self { + RepoPath(value.to_path_buf()) + } +} + +impl From for RepoPath { + fn from(value: PathBuf) -> Self { + RepoPath(value) + } +} + +impl Default for RepoPath { + fn default() -> Self { + RepoPath(PathBuf::new()) + } +} + +impl AsRef for RepoPath { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl std::ops::Deref for RepoPath { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } } #[derive(Debug, Clone, Default)] @@ -93,6 +169,10 @@ impl GitRepository for FakeGitRepository { let state = self.state.lock(); state.branch_name.clone() } + + fn statuses(&self) -> Option>{ + todo!() + } } fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 554304f3d3..e236d18efd 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; -use fs::{repository::GitRepository, Fs, LineEnding}; +use fs::{repository::{GitRepository, RepoPath, GitStatus}, Fs, LineEnding}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -117,10 +117,11 @@ pub struct Snapshot { completed_scan_id: usize, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, + // pub(crate) statuses: TreeMap } impl RepositoryEntry { @@ -162,6 +163,13 @@ impl Default for RepositoryWorkDirectory { } } +impl AsRef for RepositoryWorkDirectory { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + + #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct WorkDirectoryEntry(ProjectEntryId); @@ -178,7 +186,7 @@ impl WorkDirectoryEntry { worktree.entry_for_id(self.0).and_then(|entry| { path.strip_prefix(&entry.path) .ok() - .map(move |path| RepoPath(path.to_owned())) + .map(move |path| path.into()) }) } } @@ -197,29 +205,6 @@ impl<'a> From for WorkDirectoryEntry { } } -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub struct RepoPath(PathBuf); - -impl AsRef for RepoPath { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - -impl Deref for RepoPath { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef for RepositoryWorkDirectory { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - #[derive(Debug, Clone)] pub struct LocalSnapshot { ignores_by_parent_abs_path: HashMap, (Arc, usize)>, From 67491632cbce955038e907360c35027e094e4028 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 10:02:58 -0700 Subject: [PATCH 08/97] WIP: Track live entry status in repository co-authored-by: petros --- crates/fs/src/repository.rs | 152 +++++++++++++++++++------------- crates/project/src/worktree.rs | 95 +++++++++++++++----- crates/sum_tree/src/tree_map.rs | 4 +- 3 files changed, 165 insertions(+), 86 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 14e7e75a3d..626fbf9e12 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,11 +1,14 @@ use anyhow::Result; use collections::HashMap; +use git2::Status; use parking_lot::Mutex; -use sum_tree::TreeMap; use std::{ + ffi::OsStr, + os::unix::prelude::OsStrExt, path::{Component, Path, PathBuf}, - sync::Arc, ffi::OsStr, os::unix::prelude::OsStrExt, + sync::Arc, }; +use sum_tree::TreeMap; use util::ResultExt; pub use git2::Repository as LibGitRepository; @@ -19,6 +22,8 @@ pub trait GitRepository: Send { fn branch_name(&self) -> Option; fn statuses(&self) -> Option>; + + fn file_status(&self, path: &RepoPath) -> Option; } impl std::fmt::Debug for dyn GitRepository { @@ -70,72 +75,22 @@ impl GitRepository for LibGitRepository { let mut map = TreeMap::default(); - for status in statuses.iter() { + for status in statuses + .iter() + .filter(|status| !status.status().contains(git2::Status::IGNORED)) + { let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); - let status_data = status.status(); - - let status = if status_data.contains(git2::Status::CONFLICTED) { - GitStatus::Conflict - } else if status_data.intersects(git2::Status::INDEX_MODIFIED - | git2::Status::WT_MODIFIED - | git2::Status::INDEX_RENAMED - | git2::Status::WT_RENAMED) { - GitStatus::Modified - } else if status_data.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) { - GitStatus::Added - } else { - GitStatus::Untracked - }; - - map.insert(path, status) + map.insert(path, status.status().into()) } Some(map) } -} -#[derive(Debug, Clone, Default)] -pub enum GitStatus { - Added, - Modified, - Conflict, - #[default] - Untracked, -} + fn file_status(&self, path: &RepoPath) -> Option { + let status = self.status_file(path).log_err()?; -#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] -pub struct RepoPath(PathBuf); - -impl From<&Path> for RepoPath { - fn from(value: &Path) -> Self { - RepoPath(value.to_path_buf()) - } -} - -impl From for RepoPath { - fn from(value: PathBuf) -> Self { - RepoPath(value) - } -} - -impl Default for RepoPath { - fn default() -> Self { - RepoPath(PathBuf::new()) - } -} - -impl AsRef for RepoPath { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - -impl std::ops::Deref for RepoPath { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &self.0 + Some(status.into()) } } @@ -170,7 +125,11 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> Option>{ + fn statuses(&self) -> Option> { + todo!() + } + + fn file_status(&self, _: &RepoPath) -> Option { todo!() } } @@ -203,3 +162,74 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { _ => Ok(()), } } + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum GitStatus { + Added, + Modified, + Conflict, + #[default] + Untracked, +} + +impl From for GitStatus { + fn from(value: Status) -> Self { + if value.contains(git2::Status::CONFLICTED) { + GitStatus::Conflict + } else if value.intersects( + git2::Status::INDEX_MODIFIED + | git2::Status::WT_MODIFIED + | git2::Status::INDEX_RENAMED + | git2::Status::WT_RENAMED, + ) { + GitStatus::Modified + } else if value.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) { + GitStatus::Added + } else { + GitStatus::Untracked + } + } +} + +#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] +pub struct RepoPath(PathBuf); + +impl RepoPath { + fn new(path: PathBuf) -> Self { + debug_assert!(path.is_relative(), "Repo paths must be relative"); + + RepoPath(path) + } +} + +impl From<&Path> for RepoPath { + fn from(value: &Path) -> Self { + RepoPath::new(value.to_path_buf()) + } +} + +impl From for RepoPath { + fn from(value: PathBuf) -> Self { + RepoPath::new(value) + } +} + +impl Default for RepoPath { + fn default() -> Self { + RepoPath(PathBuf::new()) + } +} + +impl AsRef for RepoPath { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl std::ops::Deref for RepoPath { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e236d18efd..e43ab9257b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -6,7 +6,10 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; -use fs::{repository::{GitRepository, RepoPath, GitStatus}, Fs, LineEnding}; +use fs::{ + repository::{GitRepository, GitStatus, RepoPath}, + Fs, LineEnding, +}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -121,7 +124,7 @@ pub struct Snapshot { pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, - // pub(crate) statuses: TreeMap + pub(crate) statuses: TreeMap, } impl RepositoryEntry { @@ -169,7 +172,6 @@ impl AsRef for RepositoryWorkDirectory { } } - #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct WorkDirectoryEntry(ProjectEntryId); @@ -219,6 +221,7 @@ pub struct LocalSnapshot { #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { pub(crate) scan_id: usize, + pub(crate) full_scan_id: usize, pub(crate) repo_ptr: Arc>, /// Path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file @@ -1412,6 +1415,8 @@ impl Snapshot { let repository = RepositoryEntry { work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(), branch: repository.branch.map(Into::into), + // TODO: status + statuses: Default::default(), }; if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { self.repository_entries @@ -1572,6 +1577,10 @@ impl LocalSnapshot { current_candidate.map(|entry| entry.to_owned()) } + pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { + self.git_repositories.get(&repo.work_directory.0) + } + pub(crate) fn repo_for_metadata( &self, path: &Path, @@ -1842,6 +1851,7 @@ impl LocalSnapshot { RepositoryEntry { work_directory: work_dir_id.into(), branch: repo_lock.branch_name().map(Into::into), + statuses: repo_lock.statuses().unwrap_or_default(), }, ); drop(repo_lock); @@ -1850,6 +1860,7 @@ impl LocalSnapshot { work_dir_id, LocalRepositoryEntry { scan_id, + full_scan_id: scan_id, repo_ptr: repo, git_dir_path: parent_path.clone(), }, @@ -2825,26 +2836,7 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry, self.fs.as_ref()); - let scan_id = snapshot.scan_id; - - let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path); - if let Some((entry_id, repo)) = repo_with_path_in_dotgit { - let work_dir = snapshot - .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; - - let repo = repo.lock(); - repo.reload_index(); - let branch = repo.branch_name(); - - snapshot.git_repositories.update(&entry_id, |entry| { - entry.scan_id = scan_id; - }); - - snapshot - .repository_entries - .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - } + self.reload_repo_for_path(&path, &mut snapshot); if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); @@ -2872,6 +2864,63 @@ impl BackgroundScanner { Some(event_paths) } + fn reload_repo_for_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { + let scan_id = snapshot.scan_id; + + if path + .components() + .any(|component| component.as_os_str() == *DOT_GIT) + { + let (entry_id, repo) = snapshot.repo_for_metadata(&path)?; + + let work_dir = snapshot + .entry_for_id(entry_id) + .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; + + let repo = repo.lock(); + repo.reload_index(); + let branch = repo.branch_name(); + let statuses = repo.statuses().unwrap_or_default(); + + snapshot.git_repositories.update(&entry_id, |entry| { + entry.scan_id = scan_id; + entry.full_scan_id = scan_id; + }); + + snapshot.repository_entries.update(&work_dir, |entry| { + entry.branch = branch.map(Into::into); + entry.statuses = statuses; + }); + } else if let Some(repo) = snapshot.repo_for(&path) { + let status = { + let local_repo = snapshot.get_local_repo(&repo)?; + // Short circuit if we've already scanned everything + if local_repo.full_scan_id == scan_id { + return None; + } + + let repo_path = repo.work_directory.relativize(&snapshot, &path)?; + let git_ptr = local_repo.repo_ptr.lock(); + git_ptr.file_status(&repo_path)? + }; + + if status != GitStatus::Untracked { + let work_dir = repo.work_directory(snapshot)?; + let work_dir_id = repo.work_directory; + + snapshot + .git_repositories + .update(&work_dir_id, |entry| entry.scan_id = scan_id); + + snapshot + .repository_entries + .update(&work_dir, |entry| entry.statuses.insert(repo_path, status)); + } + } + + Some(()) + } + async fn update_ignore_statuses(&self) { use futures::FutureExt as _; diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 1b97cbec9f..ab37d2577a 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,13 +2,13 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeMap(SumTree>) where K: Clone + Debug + Default + Ord, V: Clone + Debug; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct MapEntry { key: K, value: V, From bd98f7810148076a9743fe1a5313f79c2797e29a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 10:04:44 -0700 Subject: [PATCH 09/97] Fix compile error --- crates/project/src/worktree.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e43ab9257b..570ff94f4e 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2892,6 +2892,8 @@ impl BackgroundScanner { entry.statuses = statuses; }); } else if let Some(repo) = snapshot.repo_for(&path) { + let repo_path = repo.work_directory.relativize(&snapshot, &path)?; + let status = { let local_repo = snapshot.get_local_repo(&repo)?; // Short circuit if we've already scanned everything @@ -2899,7 +2901,6 @@ impl BackgroundScanner { return None; } - let repo_path = repo.work_directory.relativize(&snapshot, &path)?; let git_ptr = local_repo.repo_ptr.lock(); git_ptr.file_status(&repo_path)? }; From 93f57430dad33b14c8e77d6239c5d4391ab651f9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 10:13:22 -0700 Subject: [PATCH 10/97] Track live entry status in repository --- crates/project/src/worktree.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 570ff94f4e..fb8a0ce9e7 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2853,7 +2853,9 @@ impl BackgroundScanner { } } } - Ok(None) => {} + Ok(None) => { + self.remove_repo_path(&path, &mut snapshot); + } Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this log::error!("error reading file on event {:?}", err); @@ -2864,6 +2866,31 @@ impl BackgroundScanner { Some(event_paths) } + fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { + if !path + .components() + .any(|component| component.as_os_str() == *DOT_GIT) + { + let scan_id = snapshot.scan_id; + let repo = snapshot.repo_for(&path)?; + + let repo_path = repo.work_directory.relativize(&snapshot, &path)?; + + let work_dir = repo.work_directory(snapshot)?; + let work_dir_id = repo.work_directory; + + snapshot + .git_repositories + .update(&work_dir_id, |entry| entry.scan_id = scan_id); + + snapshot + .repository_entries + .update(&work_dir, |entry| entry.statuses.remove(&repo_path)); + } + + Some(()) + } + fn reload_repo_for_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { let scan_id = snapshot.scan_id; @@ -2891,11 +2918,14 @@ impl BackgroundScanner { entry.branch = branch.map(Into::into); entry.statuses = statuses; }); - } else if let Some(repo) = snapshot.repo_for(&path) { + } else { + let repo = snapshot.repo_for(&path)?; + let repo_path = repo.work_directory.relativize(&snapshot, &path)?; let status = { let local_repo = snapshot.get_local_repo(&repo)?; + // Short circuit if we've already scanned everything if local_repo.full_scan_id == scan_id { return None; From e98507d8bf088895936ed7fb85ed3302c9e6639f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 14:42:51 -0700 Subject: [PATCH 11/97] Added git status to the project panel, added worktree test --- Cargo.lock | 1 + crates/project/Cargo.toml | 1 + crates/project/src/worktree.rs | 240 ++++++++++++++++++++-- crates/project_panel/src/project_panel.rs | 23 ++- 4 files changed, 246 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd1dd4f33b..0190b4d8f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4717,6 +4717,7 @@ dependencies = [ "futures 0.3.25", "fuzzy", "git", + "git2", "glob", "gpui", "ignore", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 2b4892aab9..85a302bdd7 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -74,5 +74,6 @@ lsp = { path = "../lsp", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } +git2 = { version = "0.15", default-features = false } tempdir.workspace = true unindent.workspace = true diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index fb8a0ce9e7..cf116d188f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -120,6 +120,25 @@ pub struct Snapshot { completed_scan_id: usize, } +impl Snapshot { + pub fn repo_for(&self, path: &Path) -> Option { + let mut max_len = 0; + let mut current_candidate = None; + for (work_directory, repo) in (&self.repository_entries).iter() { + if repo.contains(self, path) { + 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; + } + } + } + + current_candidate.map(|entry| entry.to_owned()) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, @@ -145,6 +164,13 @@ impl RepositoryEntry { pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool { self.work_directory.contains(snapshot, path) } + + pub fn status_for(&self, snapshot: &Snapshot, path: &Path) -> Option { + self.work_directory + .relativize(snapshot, path) + .and_then(|repo_path| self.statuses.get(&repo_path)) + .cloned() + } } impl From<&RepositoryEntry> for proto::RepositoryEntry { @@ -1560,23 +1586,6 @@ impl Snapshot { } impl LocalSnapshot { - pub(crate) fn repo_for(&self, path: &Path) -> Option { - let mut max_len = 0; - let mut current_candidate = None; - for (work_directory, repo) in (&self.repository_entries).iter() { - if repo.contains(self, path) { - 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; - } - } - } - - current_candidate.map(|entry| entry.to_owned()) - } - pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { self.git_repositories.get(&repo.work_directory.0) } @@ -3751,6 +3760,203 @@ mod tests { }); } + #[gpui::test] + async fn test_git_status(cx: &mut TestAppContext) { + #[track_caller] + fn git_init(path: &Path) -> git2::Repository { + git2::Repository::init(path).expect("Failed to initialize git repository") + } + + #[track_caller] + fn git_add(path: &Path, repo: &git2::Repository) { + let mut index = repo.index().expect("Failed to get index"); + index.add_path(path).expect("Failed to add a.txt"); + 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.write().expect("Failed to write index"); + } + + #[track_caller] + fn git_commit(msg: &'static str, repo: &git2::Repository) { + let signature = repo.signature().unwrap(); + let oid = repo.index().unwrap().write_tree().unwrap(); + let tree = repo.find_tree(oid).unwrap(); + if let Some(head) = repo.head().ok() { + let parent_obj = head + .peel(git2::ObjectType::Commit) + .unwrap(); + + let parent_commit = parent_obj + .as_commit() + .unwrap(); + + + repo.commit( + Some("HEAD"), + &signature, + &signature, + msg, + &tree, + &[parent_commit], + ) + .expect("Failed to commit with parent"); + } else { + repo.commit( + Some("HEAD"), + &signature, + &signature, + msg, + &tree, + &[], + ) + .expect("Failed to commit"); + } + } + + #[track_caller] + fn git_stash(repo: &mut git2::Repository) { + let signature = repo.signature().unwrap(); + repo.stash_save(&signature, "N/A", None) + .expect("Failed to stash"); + } + + #[track_caller] + fn git_reset(offset: usize, repo: &git2::Repository) { + let head = repo.head().expect("Couldn't get repo head"); + let object = head.peel(git2::ObjectType::Commit).unwrap(); + let commit = object.as_commit().unwrap(); + let new_head = commit + .parents() + .inspect(|parnet| { + parnet.message(); + }) + .skip(offset) + .next() + .expect("Not enough history"); + repo.reset(&new_head.as_object(), git2::ResetType::Soft, None) + .expect("Could not reset"); + } + + #[track_caller] + fn git_status(repo: &git2::Repository) -> HashMap { + repo.statuses(None) + .unwrap() + .iter() + .map(|status| { + (status.path().unwrap().to_string(), status.status()) + }) + .collect() + } + + let root = temp_tree(json!({ + "project": { + "a.txt": "a", + "b.txt": "bb", + }, + + })); + + let http_client = FakeHttpClient::with_404_response(); + let client = cx.read(|cx| Client::new(http_client, cx)); + let tree = Worktree::local( + client, + root.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + const A_TXT: &'static str = "a.txt"; + const B_TXT: &'static str = "b.txt"; + let work_dir = root.path().join("project"); + + let mut repo = git_init(work_dir.as_path()); + git_add(Path::new(A_TXT), &repo); + git_commit("Initial commit", &repo); + + std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + + // Check that the right git state is observed on startup + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + assert_eq!(snapshot.repository_entries.iter().count(), 1); + let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); + assert_eq!(dir.0.as_ref(), Path::new("project")); + + assert_eq!(repo.statuses.iter().count(), 2); + assert_eq!( + repo.statuses.get(&Path::new(A_TXT).into()), + Some(&GitStatus::Modified) + ); + assert_eq!( + repo.statuses.get(&Path::new(B_TXT).into()), + Some(&GitStatus::Added) + ); + }); + + git_add(Path::new(A_TXT), &repo); + git_add(Path::new(B_TXT), &repo); + git_commit("Committing modified and added", &repo); + tree.flush_fs_events(cx).await; + + // Check that repo only changes are tracked + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 0); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); + }); + + git_reset(0, &repo); + git_remove_index(Path::new(B_TXT), &repo); + git_stash(&mut repo); + tree.flush_fs_events(cx).await; + + // Check that more complex repo changes are tracked + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + + dbg!(&repo.statuses); + + + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!( + repo.statuses.get(&Path::new(B_TXT).into()), + Some(&GitStatus::Added) + ); + }); + + std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); + tree.flush_fs_events(cx).await; + + // Check that non-repo behavior is tracked + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 0); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); + }); + } + #[gpui::test] async fn test_write_file(cx: &mut TestAppContext) { let dir = temp_tree(json!({ diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7602ff7db8..845ab333e1 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -13,10 +13,10 @@ use gpui::{ keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton, PromptLevel}, AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + ViewHandle, WeakViewHandle, color::Color, }; use menu::{Confirm, SelectNext, SelectPrev}; -use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; +use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, repository::GitStatus}; use settings::Settings; use std::{ cmp::Ordering, @@ -86,6 +86,7 @@ pub struct EntryDetails { is_editing: bool, is_processing: bool, is_cut: bool, + git_status: Option } actions!( @@ -1008,6 +1009,13 @@ impl ProjectPanel { let entry_range = range.start.saturating_sub(ix)..end_ix - ix; for entry in &visible_worktree_entries[entry_range] { + let path = &entry.path; + let status = snapshot.repo_for(path) + .and_then(|entry| { + entry.status_for(&snapshot, path) + }); + + let mut details = EntryDetails { filename: entry .path @@ -1028,6 +1036,7 @@ impl ProjectPanel { is_cut: self .clipboard_entry .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), + git_status: status }; if let Some(edit_state) = &self.edit_state { @@ -1069,6 +1078,15 @@ impl ProjectPanel { let kind = details.kind; let show_editor = details.is_editing && !details.is_processing; + let git_color = details.git_status.as_ref().and_then(|status| { + match status { + GitStatus::Added => Some(Color::green()), + GitStatus::Modified => Some(Color::blue()), + GitStatus::Conflict => Some(Color::red()), + GitStatus::Untracked => None, + } + }).unwrap_or(Color::transparent_black()); + Flex::row() .with_child( if kind == EntryKind::Dir { @@ -1107,6 +1125,7 @@ impl ProjectPanel { .with_height(style.height) .contained() .with_style(row_container_style) + .with_background_color(git_color) .with_padding_left(padding) .into_any_named("project panel entry visual element") } From 18cec8d64f82aa39fe8c7df059a6e030010c55b7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 14:49:35 -0700 Subject: [PATCH 12/97] Format --- crates/project/src/worktree.rs | 27 +++++--------------- crates/project_panel/src/project_panel.rs | 30 +++++++++++++---------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index cf116d188f..66cef5131b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3787,14 +3787,9 @@ mod tests { let oid = repo.index().unwrap().write_tree().unwrap(); let tree = repo.find_tree(oid).unwrap(); if let Some(head) = repo.head().ok() { - let parent_obj = head - .peel(git2::ObjectType::Commit) - .unwrap(); - - let parent_commit = parent_obj - .as_commit() - .unwrap(); + let parent_obj = head.peel(git2::ObjectType::Commit).unwrap(); + let parent_commit = parent_obj.as_commit().unwrap(); repo.commit( Some("HEAD"), @@ -3806,15 +3801,8 @@ mod tests { ) .expect("Failed to commit with parent"); } else { - repo.commit( - Some("HEAD"), - &signature, - &signature, - msg, - &tree, - &[], - ) - .expect("Failed to commit"); + repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[]) + .expect("Failed to commit"); } } @@ -3842,14 +3830,13 @@ mod tests { .expect("Could not reset"); } + #[allow(dead_code)] #[track_caller] fn git_status(repo: &git2::Repository) -> HashMap { repo.statuses(None) .unwrap() .iter() - .map(|status| { - (status.path().unwrap().to_string(), status.status()) - }) + .map(|status| (status.path().unwrap().to_string(), status.status())) .collect() } @@ -3931,10 +3918,8 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - dbg!(&repo.statuses); - assert_eq!(repo.statuses.iter().count(), 1); assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); assert_eq!( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 845ab333e1..971c4207ba 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -5,6 +5,7 @@ use futures::stream::StreamExt; use gpui::{ actions, anyhow::{anyhow, Result}, + color::Color, elements::{ AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, @@ -13,10 +14,13 @@ use gpui::{ keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton, PromptLevel}, AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext, - ViewHandle, WeakViewHandle, color::Color, + ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; -use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, repository::GitStatus}; +use project::{ + repository::GitStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, + WorktreeId, +}; use settings::Settings; use std::{ cmp::Ordering, @@ -86,7 +90,7 @@ pub struct EntryDetails { is_editing: bool, is_processing: bool, is_cut: bool, - git_status: Option + git_status: Option, } actions!( @@ -1010,11 +1014,9 @@ impl ProjectPanel { let entry_range = range.start.saturating_sub(ix)..end_ix - ix; for entry in &visible_worktree_entries[entry_range] { let path = &entry.path; - let status = snapshot.repo_for(path) - .and_then(|entry| { - entry.status_for(&snapshot, path) - }); - + let status = snapshot + .repo_for(path) + .and_then(|entry| entry.status_for(&snapshot, path)); let mut details = EntryDetails { filename: entry @@ -1036,7 +1038,7 @@ impl ProjectPanel { is_cut: self .clipboard_entry .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), - git_status: status + git_status: status, }; if let Some(edit_state) = &self.edit_state { @@ -1078,14 +1080,16 @@ impl ProjectPanel { let kind = details.kind; let show_editor = details.is_editing && !details.is_processing; - let git_color = details.git_status.as_ref().and_then(|status| { - match status { + let git_color = details + .git_status + .as_ref() + .and_then(|status| match status { GitStatus::Added => Some(Color::green()), GitStatus::Modified => Some(Color::blue()), GitStatus::Conflict => Some(Color::red()), GitStatus::Untracked => None, - } - }).unwrap_or(Color::transparent_black()); + }) + .unwrap_or(Color::transparent_black()); Flex::row() .with_child( From a58a33fc93dd64b607112c1cae8af26903b5a510 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 19:29:45 -0700 Subject: [PATCH 13/97] WIP: integrate status with collab --- crates/collab/src/tests/integration_tests.rs | 113 ++++++++++++++++++- crates/fs/src/fs.rs | 15 ++- crates/fs/src/repository.rs | 13 ++- crates/project/src/worktree.rs | 4 + crates/rpc/proto/zed.proto | 14 +++ 5 files changed, 154 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e3b5b0be7e..764f070f0b 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -10,7 +10,7 @@ use editor::{ ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; -use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions}; +use fs::{repository::GitStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; use futures::StreamExt as _; use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, @@ -2690,6 +2690,117 @@ async fn test_git_branch_name( }); } +#[gpui::test] +async fn test_git_status_sync( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".git": {}, + "a.txt": "a", + "b.txt": "b", + }), + ) + .await; + + const A_TXT: &'static str = "a.txt"; + const B_TXT: &'static str = "b.txt"; + + client_a + .fs + .as_fake() + .set_status_for_repo( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitStatus::Added), + (&Path::new(B_TXT), GitStatus::Added), + ], + ) + .await; + + let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| { + call.share_project(project_local.clone(), cx) + }) + .await + .unwrap(); + + let project_remote = client_b.build_remote_project(project_id, cx_b).await; + + // Wait for it to catch up to the new status + deterministic.run_until_parked(); + + #[track_caller] + fn assert_status(file: &impl AsRef, status: Option, project: &Project, cx: &AppContext) { + let file = file.as_ref(); + let worktrees = project.visible_worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree = worktrees[0].clone(); + let snapshot = worktree.read(cx).snapshot(); + let root_entry = snapshot.root_git_entry().unwrap(); + assert_eq!(root_entry.status_for(&snapshot, file), status); + } + + // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + project_remote.read_with(cx_b, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + + client_a + .fs + .as_fake() + .set_status_for_repo( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitStatus::Modified), + (&Path::new(B_TXT), GitStatus::Modified), + ], + ) + .await; + + // Wait for buffer_local_a to receive it + deterministic.run_until_parked(); + + // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + project_remote.read_with(cx_b, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + + // And synchronization while joining + let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; + project_remote_c.read_with(cx_c, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); +} + #[gpui::test(iterations = 10)] async fn test_fs_operations( deterministic: Arc, diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 945ffaea16..efc24553c4 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,7 @@ use git2::Repository as LibGitRepository; use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; -use repository::GitRepository; +use repository::{GitRepository, GitStatus}; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; @@ -654,6 +654,19 @@ impl FakeFs { }); } + pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitStatus)]) { + self.with_git_state(dot_git, |state| { + state.git_statuses.clear(); + state.git_statuses.extend( + statuses + .iter() + .map(|(path, content)| { + ((**path).into(), content.clone()) + }), + ); + }); + } + pub fn paths(&self) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 626fbf9e12..7fa20bddcb 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -102,6 +102,7 @@ pub struct FakeGitRepository { #[derive(Debug, Clone, Default)] pub struct FakeGitRepositoryState { pub index_contents: HashMap, + pub git_statuses: HashMap, pub branch_name: Option, } @@ -126,11 +127,17 @@ impl GitRepository for FakeGitRepository { } fn statuses(&self) -> Option> { - todo!() + let state = self.state.lock(); + let mut map = TreeMap::default(); + for (repo_path, status) in state.git_statuses.iter() { + map.insert(repo_path.to_owned(), status.to_owned()); + } + Some(map) } - fn file_status(&self, _: &RepoPath) -> Option { - todo!() + fn file_status(&self, path: &RepoPath) -> Option { + let state = self.state.lock(); + state.git_statuses.get(path).cloned() } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 66cef5131b..82c719f31e 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -178,6 +178,9 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), + // TODO: Status + removed_statuses: Default::default(), + updated_statuses: Default::default(), } } } @@ -1855,6 +1858,7 @@ impl LocalSnapshot { let scan_id = self.scan_id; let repo_lock = repo.lock(); + self.repository_entries.insert( work_directory, RepositoryEntry { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 220ef22fb7..abe02f42bb 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -986,8 +986,22 @@ message Entry { message RepositoryEntry { uint64 work_directory_id = 1; optional string branch = 2; + repeated uint64 removed_statuses = 3; + repeated StatusEntry updated_statuses = 4; } +message StatusEntry { + uint64 entry_id = 1; + GitStatus status = 2; +} + +enum GitStatus { + Added = 0; + Modified = 1; + Conflict = 2; +} + + message BufferState { uint64 id = 1; optional File file = 2; From 94a0de4c9fe5417882075aed00f6d337f3bee86d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 19:35:15 -0700 Subject: [PATCH 14/97] Fix compile errors --- crates/collab/src/db.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index bc5b816abf..5867fa7369 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1568,6 +1568,9 @@ impl Database { worktree.updated_repositories.push(proto::RepositoryEntry { work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, + removed_statuses: Default::default(), + updated_statuses: Default::default(), + }); } } @@ -2648,6 +2651,9 @@ impl Database { worktree.repository_entries.push(proto::RepositoryEntry { work_directory_id: db_repository_entry.work_directory_id as u64, branch: db_repository_entry.branch, + removed_statuses: Default::default(), + updated_statuses: Default::default(), + }); } } From f935047ff27bcc9ca2acfb4855df5b5d59b951d7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 9 May 2023 21:06:23 -0700 Subject: [PATCH 15/97] Convert git status calculation to use Entry IDs as the key instead of repo relative paths --- Cargo.lock | 1 - crates/collab/src/tests/integration_tests.rs | 3 +- crates/fs/Cargo.toml | 1 - crates/fs/src/fs.rs | 4 +- crates/fs/src/repository.rs | 15 ++- crates/project/src/worktree.rs | 120 ++++++++++++------- crates/project_panel/src/project_panel.rs | 5 +- 7 files changed, 90 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0190b4d8f5..8fe740267e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2350,7 +2350,6 @@ dependencies = [ "serde_derive", "serde_json", "smol", - "sum_tree", "tempfile", "util", ] diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 764f070f0b..b6046870d1 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2755,7 +2755,8 @@ async fn test_git_status_sync( let worktree = worktrees[0].clone(); let snapshot = worktree.read(cx).snapshot(); let root_entry = snapshot.root_git_entry().unwrap(); - assert_eq!(root_entry.status_for(&snapshot, file), status); + let file_entry_id = snapshot.entry_for_path(file).unwrap().id; + assert_eq!(root_entry.status_for(file_entry_id), status); } // Smoke test status reading diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 54c6ce362a..d080fe3cd1 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -13,7 +13,6 @@ gpui = { path = "../gpui" } lsp = { path = "../lsp" } rope = { path = "../rope" } util = { path = "../util" } -sum_tree = { path = "../sum_tree" } anyhow.workspace = true async-trait.workspace = true futures.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index efc24553c4..9347e7d7b1 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,7 @@ use git2::Repository as LibGitRepository; use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; -use repository::{GitRepository, GitStatus}; +use repository::GitRepository; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; @@ -27,7 +27,7 @@ use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] -use repository::FakeGitRepositoryState; +use repository::{FakeGitRepositoryState, GitStatus}; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 7fa20bddcb..73847dac29 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -8,7 +8,6 @@ use std::{ path::{Component, Path, PathBuf}, sync::Arc, }; -use sum_tree::TreeMap; use util::ResultExt; pub use git2::Repository as LibGitRepository; @@ -21,7 +20,7 @@ pub trait GitRepository: Send { fn branch_name(&self) -> Option; - fn statuses(&self) -> Option>; + fn statuses(&self) -> Option>; fn file_status(&self, path: &RepoPath) -> Option; } @@ -70,10 +69,10 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self) -> Option> { + fn statuses(&self) -> Option> { let statuses = self.statuses(None).log_err()?; - let mut map = TreeMap::default(); + let mut result = HashMap::default(); for status in statuses .iter() @@ -81,10 +80,10 @@ impl GitRepository for LibGitRepository { { let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); - map.insert(path, status.status().into()) + result.insert(path, status.status().into()); } - Some(map) + Some(result) } fn file_status(&self, path: &RepoPath) -> Option { @@ -126,9 +125,9 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> Option> { + fn statuses(&self) -> Option> { let state = self.state.lock(); - let mut map = TreeMap::default(); + let mut map = HashMap::default(); for (repo_path, status) in state.git_statuses.iter() { map.insert(repo_path.to_owned(), status.to_owned()); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 82c719f31e..b9a4b549a1 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -143,7 +143,7 @@ impl Snapshot { pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, - pub(crate) statuses: TreeMap, + pub(crate) statuses: TreeMap, } impl RepositoryEntry { @@ -165,11 +165,8 @@ impl RepositoryEntry { self.work_directory.contains(snapshot, path) } - pub fn status_for(&self, snapshot: &Snapshot, path: &Path) -> Option { - self.work_directory - .relativize(snapshot, path) - .and_then(|repo_path| self.statuses.get(&repo_path)) - .cloned() + pub fn status_for(&self, entry: ProjectEntryId) -> Option { + self.statuses.get(&entry).cloned() } } @@ -1813,10 +1810,6 @@ impl LocalSnapshot { ); } - if parent_path.file_name() == Some(&DOT_GIT) { - self.build_repo(parent_path, fs); - } - let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; let mut entries_by_id_edits = Vec::new(); @@ -1833,6 +1826,10 @@ impl LocalSnapshot { self.entries_by_path.edit(entries_by_path_edits, &()); self.entries_by_id.edit(entries_by_id_edits, &()); + + if parent_path.file_name() == Some(&DOT_GIT) { + self.build_repo(parent_path, fs); + } } fn build_repo(&mut self, parent_path: Arc, fs: &dyn Fs) -> Option<()> { @@ -1858,13 +1855,13 @@ impl LocalSnapshot { let scan_id = self.scan_id; let repo_lock = repo.lock(); - + let statuses = convert_statuses(&work_directory, repo_lock.deref(), self)?; self.repository_entries.insert( work_directory, RepositoryEntry { work_directory: work_dir_id.into(), branch: repo_lock.branch_name().map(Into::into), - statuses: repo_lock.statuses().unwrap_or_default(), + statuses, }, ); drop(repo_lock); @@ -2821,6 +2818,7 @@ impl BackgroundScanner { for (abs_path, metadata) in abs_paths.iter().zip(metadata.iter()) { if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { if matches!(metadata, Ok(None)) || doing_recursive_update { + self.remove_repo_path(&path, &mut snapshot); snapshot.remove_path(path); } event_paths.push(path.into()); @@ -2866,9 +2864,7 @@ impl BackgroundScanner { } } } - Ok(None) => { - self.remove_repo_path(&path, &mut snapshot); - } + Ok(None) => {} Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this log::error!("error reading file on event {:?}", err); @@ -2887,7 +2883,7 @@ impl BackgroundScanner { let scan_id = snapshot.scan_id; let repo = snapshot.repo_for(&path)?; - let repo_path = repo.work_directory.relativize(&snapshot, &path)?; + let repo_path_id = snapshot.entry_for_path(path)?.id; let work_dir = repo.work_directory(snapshot)?; let work_dir_id = repo.work_directory; @@ -2898,7 +2894,7 @@ impl BackgroundScanner { snapshot .repository_entries - .update(&work_dir, |entry| entry.statuses.remove(&repo_path)); + .update(&work_dir, |entry| entry.statuses.remove(&repo_path_id)); } Some(()) @@ -2911,18 +2907,19 @@ impl BackgroundScanner { .components() .any(|component| component.as_os_str() == *DOT_GIT) { - let (entry_id, repo) = snapshot.repo_for_metadata(&path)?; + let (git_dir_id, repo) = snapshot.repo_for_metadata(&path)?; let work_dir = snapshot - .entry_for_id(entry_id) + .entry_for_id(git_dir_id) .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; let repo = repo.lock(); repo.reload_index(); let branch = repo.branch_name(); - let statuses = repo.statuses().unwrap_or_default(); - snapshot.git_repositories.update(&entry_id, |entry| { + let statuses = convert_statuses(&work_dir, repo.deref(), snapshot)?; + + snapshot.git_repositories.update(&git_dir_id, |entry| { entry.scan_id = scan_id; entry.full_scan_id = scan_id; }); @@ -2936,6 +2933,8 @@ impl BackgroundScanner { let repo_path = repo.work_directory.relativize(&snapshot, &path)?; + let path_id = snapshot.entry_for_path(&path)?.id; + let status = { let local_repo = snapshot.get_local_repo(&repo)?; @@ -2958,7 +2957,7 @@ impl BackgroundScanner { snapshot .repository_entries - .update(&work_dir, |entry| entry.statuses.insert(repo_path, status)); + .update(&work_dir, |entry| entry.statuses.insert(path_id, status)); } } @@ -3167,6 +3166,19 @@ impl BackgroundScanner { } } +fn convert_statuses( + work_dir: &RepositoryWorkDirectory, + repo: &dyn GitRepository, + snapshot: &Snapshot, +) -> Option> { + let mut statuses = TreeMap::default(); + for (path, status) in repo.statuses().unwrap_or_default() { + let path_entry = snapshot.entry_for_path(&work_dir.0.join(path.as_path()))?; + statuses.insert(path_entry.id, status) + } + Some(statuses) +} + fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( @@ -3848,6 +3860,11 @@ mod tests { "project": { "a.txt": "a", "b.txt": "bb", + "c": { + "d": { + "e.txt": "eee" + } + } }, })); @@ -3865,18 +3882,39 @@ mod tests { .await .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; + const E_TXT: &'static str = "c/d/e.txt"; let work_dir = root.path().join("project"); + let tree_clone = tree.clone(); + let (a_txt_id, b_txt_id, e_txt_id) = cx.read(|cx| { + let tree = tree_clone.read(cx); + let a_id = tree + .entry_for_path(Path::new("project").join(Path::new(A_TXT))) + .unwrap() + .id; + let b_id = tree + .entry_for_path(Path::new("project").join(Path::new(B_TXT))) + .unwrap() + .id; + let e_id = tree + .entry_for_path(Path::new("project").join(Path::new(E_TXT))) + .unwrap() + .id; + (a_id, b_id, e_id) + }); + let mut repo = git_init(work_dir.as_path()); git_add(Path::new(A_TXT), &repo); + git_add(Path::new(E_TXT), &repo); git_commit("Initial commit", &repo); std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; tree.flush_fs_events(cx).await; // Check that the right git state is observed on startup @@ -3885,16 +3923,9 @@ mod tests { assert_eq!(snapshot.repository_entries.iter().count(), 1); let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(dir.0.as_ref(), Path::new("project")); - assert_eq!(repo.statuses.iter().count(), 2); - assert_eq!( - repo.statuses.get(&Path::new(A_TXT).into()), - Some(&GitStatus::Modified) - ); - assert_eq!( - repo.statuses.get(&Path::new(B_TXT).into()), - Some(&GitStatus::Added) - ); + assert_eq!(repo.statuses.get(&a_txt_id), Some(&GitStatus::Modified)); + assert_eq!(repo.statuses.get(&b_txt_id), Some(&GitStatus::Added)); }); git_add(Path::new(A_TXT), &repo); @@ -3908,15 +3939,18 @@ mod tests { let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(repo.statuses.iter().count(), 0); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!(repo.statuses.get(&a_txt_id), None); + assert_eq!(repo.statuses.get(&b_txt_id), None); }); git_reset(0, &repo); git_remove_index(Path::new(B_TXT), &repo); git_stash(&mut repo); + std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); tree.flush_fs_events(cx).await; + dbg!(git_status(&repo)); + // Check that more complex repo changes are tracked tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); @@ -3924,15 +3958,14 @@ mod tests { dbg!(&repo.statuses); - assert_eq!(repo.statuses.iter().count(), 1); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!( - repo.statuses.get(&Path::new(B_TXT).into()), - Some(&GitStatus::Added) - ); + assert_eq!(repo.statuses.iter().count(), 2); + assert_eq!(repo.statuses.get(&a_txt_id), None); + assert_eq!(repo.statuses.get(&b_txt_id), Some(&GitStatus::Added)); + assert_eq!(repo.statuses.get(&e_txt_id), Some(&GitStatus::Modified)); }); std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); + std::fs::remove_dir_all(work_dir.join("c")).unwrap(); tree.flush_fs_events(cx).await; // Check that non-repo behavior is tracked @@ -3941,8 +3974,9 @@ mod tests { let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(repo.statuses.iter().count(), 0); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!(repo.statuses.get(&a_txt_id), None); + assert_eq!(repo.statuses.get(&b_txt_id), None); + assert_eq!(repo.statuses.get(&e_txt_id), None); }); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 971c4207ba..de440b38a4 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1013,10 +1013,9 @@ impl ProjectPanel { let entry_range = range.start.saturating_sub(ix)..end_ix - ix; for entry in &visible_worktree_entries[entry_range] { - let path = &entry.path; let status = snapshot - .repo_for(path) - .and_then(|entry| entry.status_for(&snapshot, path)); + .repo_for(&entry.path) + .and_then(|repo_entry| repo_entry.status_for(entry.id)); let mut details = EntryDetails { filename: entry From 6b4242cdedbe3c571f9704a28ed103b6d51a211f Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Wed, 10 May 2023 16:21:24 +0300 Subject: [PATCH 16/97] Use theme.editor.diff for the colors --- crates/project_panel/src/project_panel.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index de440b38a4..2baccc0913 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -5,7 +5,6 @@ use futures::stream::StreamExt; use gpui::{ actions, anyhow::{anyhow, Result}, - color::Color, elements::{ AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, @@ -1079,16 +1078,23 @@ impl ProjectPanel { let kind = details.kind; let show_editor = details.is_editing && !details.is_processing; - let git_color = details + // Prepare colors for git statuses + let editor_theme = &cx.global::().theme.editor; + let color_for_added = Some(editor_theme.diff.inserted); + let color_for_modified = Some(editor_theme.diff.modified); + let color_for_conflict = Some(editor_theme.diff.deleted); + let color_for_untracked = None; + let mut filename_text_style = style.text.clone(); + filename_text_style.color = details .git_status .as_ref() .and_then(|status| match status { - GitStatus::Added => Some(Color::green()), - GitStatus::Modified => Some(Color::blue()), - GitStatus::Conflict => Some(Color::red()), - GitStatus::Untracked => None, + GitStatus::Added => color_for_added, + GitStatus::Modified => color_for_modified, + GitStatus::Conflict => color_for_conflict, + GitStatus::Untracked => color_for_untracked, }) - .unwrap_or(Color::transparent_black()); + .unwrap_or(style.text.color); Flex::row() .with_child( @@ -1117,7 +1123,7 @@ impl ProjectPanel { .flex(1.0, true) .into_any() } else { - Label::new(details.filename.clone(), style.text.clone()) + Label::new(details.filename.clone(), filename_text_style) .contained() .with_margin_left(style.icon_spacing) .aligned() @@ -1128,7 +1134,6 @@ impl ProjectPanel { .with_height(style.height) .contained() .with_style(row_container_style) - .with_background_color(git_color) .with_padding_left(padding) .into_any_named("project panel entry visual element") } From 21e1bdc8cd9bc880e01916337ee60a5a034bd30c Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Wed, 10 May 2023 17:05:53 +0300 Subject: [PATCH 17/97] Fix yellow to be yellow --- crates/gpui/src/color.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index cc725776b9..b6c1e3aff9 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -42,7 +42,7 @@ impl Color { } pub fn yellow() -> Self { - Self(ColorU::from_u32(0x00ffffff)) + Self(ColorU::from_u32(0xffff00ff)) } pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { From 0082d68d4af653a9dfd9c1dbeb1bb70373a2de51 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 08:49:30 -0700 Subject: [PATCH 18/97] Revert "Convert git status calculation to use Entry IDs as the key instead of repo relative paths" This reverts commit 728c6892c924ebeabb086e308ec4b5f56c4fd72a. --- Cargo.lock | 1 + crates/collab/src/tests/integration_tests.rs | 3 +- crates/fs/Cargo.toml | 1 + crates/fs/src/fs.rs | 4 +- crates/fs/src/repository.rs | 15 +-- crates/project/src/worktree.rs | 120 +++++++------------ crates/project_panel/src/project_panel.rs | 5 +- 7 files changed, 59 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8fe740267e..0190b4d8f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2350,6 +2350,7 @@ dependencies = [ "serde_derive", "serde_json", "smol", + "sum_tree", "tempfile", "util", ] diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index b6046870d1..764f070f0b 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2755,8 +2755,7 @@ async fn test_git_status_sync( let worktree = worktrees[0].clone(); let snapshot = worktree.read(cx).snapshot(); let root_entry = snapshot.root_git_entry().unwrap(); - let file_entry_id = snapshot.entry_for_path(file).unwrap().id; - assert_eq!(root_entry.status_for(file_entry_id), status); + assert_eq!(root_entry.status_for(&snapshot, file), status); } // Smoke test status reading diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index d080fe3cd1..54c6ce362a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -13,6 +13,7 @@ gpui = { path = "../gpui" } lsp = { path = "../lsp" } rope = { path = "../rope" } util = { path = "../util" } +sum_tree = { path = "../sum_tree" } anyhow.workspace = true async-trait.workspace = true futures.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 9347e7d7b1..efc24553c4 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,7 @@ use git2::Repository as LibGitRepository; use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; -use repository::GitRepository; +use repository::{GitRepository, GitStatus}; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; @@ -27,7 +27,7 @@ use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] -use repository::{FakeGitRepositoryState, GitStatus}; +use repository::FakeGitRepositoryState; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 73847dac29..7fa20bddcb 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -8,6 +8,7 @@ use std::{ path::{Component, Path, PathBuf}, sync::Arc, }; +use sum_tree::TreeMap; use util::ResultExt; pub use git2::Repository as LibGitRepository; @@ -20,7 +21,7 @@ pub trait GitRepository: Send { fn branch_name(&self) -> Option; - fn statuses(&self) -> Option>; + fn statuses(&self) -> Option>; fn file_status(&self, path: &RepoPath) -> Option; } @@ -69,10 +70,10 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self) -> Option> { + fn statuses(&self) -> Option> { let statuses = self.statuses(None).log_err()?; - let mut result = HashMap::default(); + let mut map = TreeMap::default(); for status in statuses .iter() @@ -80,10 +81,10 @@ impl GitRepository for LibGitRepository { { let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); - result.insert(path, status.status().into()); + map.insert(path, status.status().into()) } - Some(result) + Some(map) } fn file_status(&self, path: &RepoPath) -> Option { @@ -125,9 +126,9 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> Option> { + fn statuses(&self) -> Option> { let state = self.state.lock(); - let mut map = HashMap::default(); + let mut map = TreeMap::default(); for (repo_path, status) in state.git_statuses.iter() { map.insert(repo_path.to_owned(), status.to_owned()); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b9a4b549a1..82c719f31e 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -143,7 +143,7 @@ impl Snapshot { pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, - pub(crate) statuses: TreeMap, + pub(crate) statuses: TreeMap, } impl RepositoryEntry { @@ -165,8 +165,11 @@ impl RepositoryEntry { self.work_directory.contains(snapshot, path) } - pub fn status_for(&self, entry: ProjectEntryId) -> Option { - self.statuses.get(&entry).cloned() + pub fn status_for(&self, snapshot: &Snapshot, path: &Path) -> Option { + self.work_directory + .relativize(snapshot, path) + .and_then(|repo_path| self.statuses.get(&repo_path)) + .cloned() } } @@ -1810,6 +1813,10 @@ impl LocalSnapshot { ); } + if parent_path.file_name() == Some(&DOT_GIT) { + self.build_repo(parent_path, fs); + } + let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; let mut entries_by_id_edits = Vec::new(); @@ -1826,10 +1833,6 @@ impl LocalSnapshot { self.entries_by_path.edit(entries_by_path_edits, &()); self.entries_by_id.edit(entries_by_id_edits, &()); - - if parent_path.file_name() == Some(&DOT_GIT) { - self.build_repo(parent_path, fs); - } } fn build_repo(&mut self, parent_path: Arc, fs: &dyn Fs) -> Option<()> { @@ -1855,13 +1858,13 @@ impl LocalSnapshot { let scan_id = self.scan_id; let repo_lock = repo.lock(); - let statuses = convert_statuses(&work_directory, repo_lock.deref(), self)?; + self.repository_entries.insert( work_directory, RepositoryEntry { work_directory: work_dir_id.into(), branch: repo_lock.branch_name().map(Into::into), - statuses, + statuses: repo_lock.statuses().unwrap_or_default(), }, ); drop(repo_lock); @@ -2818,7 +2821,6 @@ impl BackgroundScanner { for (abs_path, metadata) in abs_paths.iter().zip(metadata.iter()) { if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { if matches!(metadata, Ok(None)) || doing_recursive_update { - self.remove_repo_path(&path, &mut snapshot); snapshot.remove_path(path); } event_paths.push(path.into()); @@ -2864,7 +2866,9 @@ impl BackgroundScanner { } } } - Ok(None) => {} + Ok(None) => { + self.remove_repo_path(&path, &mut snapshot); + } Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this log::error!("error reading file on event {:?}", err); @@ -2883,7 +2887,7 @@ impl BackgroundScanner { let scan_id = snapshot.scan_id; let repo = snapshot.repo_for(&path)?; - let repo_path_id = snapshot.entry_for_path(path)?.id; + let repo_path = repo.work_directory.relativize(&snapshot, &path)?; let work_dir = repo.work_directory(snapshot)?; let work_dir_id = repo.work_directory; @@ -2894,7 +2898,7 @@ impl BackgroundScanner { snapshot .repository_entries - .update(&work_dir, |entry| entry.statuses.remove(&repo_path_id)); + .update(&work_dir, |entry| entry.statuses.remove(&repo_path)); } Some(()) @@ -2907,19 +2911,18 @@ impl BackgroundScanner { .components() .any(|component| component.as_os_str() == *DOT_GIT) { - let (git_dir_id, repo) = snapshot.repo_for_metadata(&path)?; + let (entry_id, repo) = snapshot.repo_for_metadata(&path)?; let work_dir = snapshot - .entry_for_id(git_dir_id) + .entry_for_id(entry_id) .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; let repo = repo.lock(); repo.reload_index(); let branch = repo.branch_name(); + let statuses = repo.statuses().unwrap_or_default(); - let statuses = convert_statuses(&work_dir, repo.deref(), snapshot)?; - - snapshot.git_repositories.update(&git_dir_id, |entry| { + snapshot.git_repositories.update(&entry_id, |entry| { entry.scan_id = scan_id; entry.full_scan_id = scan_id; }); @@ -2933,8 +2936,6 @@ impl BackgroundScanner { let repo_path = repo.work_directory.relativize(&snapshot, &path)?; - let path_id = snapshot.entry_for_path(&path)?.id; - let status = { let local_repo = snapshot.get_local_repo(&repo)?; @@ -2957,7 +2958,7 @@ impl BackgroundScanner { snapshot .repository_entries - .update(&work_dir, |entry| entry.statuses.insert(path_id, status)); + .update(&work_dir, |entry| entry.statuses.insert(repo_path, status)); } } @@ -3166,19 +3167,6 @@ impl BackgroundScanner { } } -fn convert_statuses( - work_dir: &RepositoryWorkDirectory, - repo: &dyn GitRepository, - snapshot: &Snapshot, -) -> Option> { - let mut statuses = TreeMap::default(); - for (path, status) in repo.statuses().unwrap_or_default() { - let path_entry = snapshot.entry_for_path(&work_dir.0.join(path.as_path()))?; - statuses.insert(path_entry.id, status) - } - Some(statuses) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( @@ -3860,11 +3848,6 @@ mod tests { "project": { "a.txt": "a", "b.txt": "bb", - "c": { - "d": { - "e.txt": "eee" - } - } }, })); @@ -3882,39 +3865,18 @@ mod tests { .await .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; - const E_TXT: &'static str = "c/d/e.txt"; let work_dir = root.path().join("project"); - let tree_clone = tree.clone(); - let (a_txt_id, b_txt_id, e_txt_id) = cx.read(|cx| { - let tree = tree_clone.read(cx); - let a_id = tree - .entry_for_path(Path::new("project").join(Path::new(A_TXT))) - .unwrap() - .id; - let b_id = tree - .entry_for_path(Path::new("project").join(Path::new(B_TXT))) - .unwrap() - .id; - let e_id = tree - .entry_for_path(Path::new("project").join(Path::new(E_TXT))) - .unwrap() - .id; - (a_id, b_id, e_id) - }); - let mut repo = git_init(work_dir.as_path()); git_add(Path::new(A_TXT), &repo); - git_add(Path::new(E_TXT), &repo); git_commit("Initial commit", &repo); std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; tree.flush_fs_events(cx).await; // Check that the right git state is observed on startup @@ -3923,9 +3885,16 @@ mod tests { assert_eq!(snapshot.repository_entries.iter().count(), 1); let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(dir.0.as_ref(), Path::new("project")); + assert_eq!(repo.statuses.iter().count(), 2); - assert_eq!(repo.statuses.get(&a_txt_id), Some(&GitStatus::Modified)); - assert_eq!(repo.statuses.get(&b_txt_id), Some(&GitStatus::Added)); + assert_eq!( + repo.statuses.get(&Path::new(A_TXT).into()), + Some(&GitStatus::Modified) + ); + assert_eq!( + repo.statuses.get(&Path::new(B_TXT).into()), + Some(&GitStatus::Added) + ); }); git_add(Path::new(A_TXT), &repo); @@ -3939,18 +3908,15 @@ mod tests { let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(repo.statuses.iter().count(), 0); - assert_eq!(repo.statuses.get(&a_txt_id), None); - assert_eq!(repo.statuses.get(&b_txt_id), None); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); }); git_reset(0, &repo); git_remove_index(Path::new(B_TXT), &repo); git_stash(&mut repo); - std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); tree.flush_fs_events(cx).await; - dbg!(git_status(&repo)); - // Check that more complex repo changes are tracked tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); @@ -3958,14 +3924,15 @@ mod tests { dbg!(&repo.statuses); - assert_eq!(repo.statuses.iter().count(), 2); - assert_eq!(repo.statuses.get(&a_txt_id), None); - assert_eq!(repo.statuses.get(&b_txt_id), Some(&GitStatus::Added)); - assert_eq!(repo.statuses.get(&e_txt_id), Some(&GitStatus::Modified)); + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!( + repo.statuses.get(&Path::new(B_TXT).into()), + Some(&GitStatus::Added) + ); }); std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); - std::fs::remove_dir_all(work_dir.join("c")).unwrap(); tree.flush_fs_events(cx).await; // Check that non-repo behavior is tracked @@ -3974,9 +3941,8 @@ mod tests { let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(repo.statuses.iter().count(), 0); - assert_eq!(repo.statuses.get(&a_txt_id), None); - assert_eq!(repo.statuses.get(&b_txt_id), None); - assert_eq!(repo.statuses.get(&e_txt_id), None); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); }); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2baccc0913..16b6232e8b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1012,9 +1012,10 @@ impl ProjectPanel { let entry_range = range.start.saturating_sub(ix)..end_ix - ix; for entry in &visible_worktree_entries[entry_range] { + let path = &entry.path; let status = snapshot - .repo_for(&entry.path) - .and_then(|repo_entry| repo_entry.status_for(entry.id)); + .repo_for(path) + .and_then(|entry| entry.status_for(&snapshot, path)); let mut details = EntryDetails { filename: entry From 23a19d85b817a70474f7ac2f9588191b46b7449a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 09:55:10 -0700 Subject: [PATCH 19/97] Fix bug in status detection when removing a directory --- crates/collab/src/tests/integration_tests.rs | 32 +++---- crates/fs/src/fs.rs | 8 +- crates/fs/src/repository.rs | 62 ++++++------- crates/project/src/worktree.rs | 94 ++++++++++++-------- crates/project_panel/src/project_panel.rs | 17 ++-- 5 files changed, 108 insertions(+), 105 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 764f070f0b..185e6c6354 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -10,7 +10,7 @@ use editor::{ ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; -use fs::{repository::GitStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; +use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; use futures::StreamExt as _; use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, @@ -2728,8 +2728,8 @@ async fn test_git_status_sync( .set_status_for_repo( Path::new("/dir/.git"), &[ - (&Path::new(A_TXT), GitStatus::Added), - (&Path::new(B_TXT), GitStatus::Added), + (&Path::new(A_TXT), GitFileStatus::Added), + (&Path::new(B_TXT), GitFileStatus::Added), ], ) .await; @@ -2748,7 +2748,7 @@ async fn test_git_status_sync( deterministic.run_until_parked(); #[track_caller] - fn assert_status(file: &impl AsRef, status: Option, project: &Project, cx: &AppContext) { + fn assert_status(file: &impl AsRef, status: Option, project: &Project, cx: &AppContext) { let file = file.as_ref(); let worktrees = project.visible_worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -2760,12 +2760,12 @@ async fn test_git_status_sync( // Smoke test status reading project_local.read_with(cx_a, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); project_remote.read_with(cx_b, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); client_a @@ -2774,8 +2774,8 @@ async fn test_git_status_sync( .set_status_for_repo( Path::new("/dir/.git"), &[ - (&Path::new(A_TXT), GitStatus::Modified), - (&Path::new(B_TXT), GitStatus::Modified), + (&Path::new(A_TXT), GitFileStatus::Modified), + (&Path::new(B_TXT), GitFileStatus::Modified), ], ) .await; @@ -2785,19 +2785,19 @@ async fn test_git_status_sync( // Smoke test status reading project_local.read_with(cx_a, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); project_remote.read_with(cx_b, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); // And synchronization while joining let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; project_remote_c.read_with(cx_c, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index efc24553c4..fd094160f5 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,7 @@ use git2::Repository as LibGitRepository; use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; -use repository::{GitRepository, GitStatus}; +use repository::{GitRepository, GitFileStatus}; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; @@ -654,10 +654,10 @@ impl FakeFs { }); } - pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitStatus)]) { + pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) { self.with_git_state(dot_git, |state| { - state.git_statuses.clear(); - state.git_statuses.extend( + state.worktree_statuses.clear(); + state.worktree_statuses.extend( statuses .iter() .map(|(path, content)| { diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 7fa20bddcb..3fb562570e 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,6 +1,5 @@ use anyhow::Result; use collections::HashMap; -use git2::Status; use parking_lot::Mutex; use std::{ ffi::OsStr, @@ -21,9 +20,9 @@ pub trait GitRepository: Send { fn branch_name(&self) -> Option; - fn statuses(&self) -> Option>; + fn worktree_statuses(&self) -> Option>; - fn file_status(&self, path: &RepoPath) -> Option; + fn worktree_status(&self, path: &RepoPath) -> Option; } impl std::fmt::Debug for dyn GitRepository { @@ -70,7 +69,7 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self) -> Option> { + fn worktree_statuses(&self) -> Option> { let statuses = self.statuses(None).log_err()?; let mut map = TreeMap::default(); @@ -80,17 +79,31 @@ impl GitRepository for LibGitRepository { .filter(|status| !status.status().contains(git2::Status::IGNORED)) { let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); + let Some(status) = read_status(status.status()) else { + continue + }; - map.insert(path, status.status().into()) + map.insert(path, status) } Some(map) } - fn file_status(&self, path: &RepoPath) -> Option { + fn worktree_status(&self, path: &RepoPath) -> Option { let status = self.status_file(path).log_err()?; + read_status(status) + } +} - Some(status.into()) +fn read_status(status: git2::Status) -> Option { + if status.contains(git2::Status::CONFLICTED) { + Some(GitFileStatus::Conflict) + } else if status.intersects(git2::Status::WT_MODIFIED | git2::Status::WT_RENAMED) { + Some(GitFileStatus::Modified) + } else if status.intersects(git2::Status::WT_NEW) { + Some(GitFileStatus::Added) + } else { + None } } @@ -102,7 +115,7 @@ pub struct FakeGitRepository { #[derive(Debug, Clone, Default)] pub struct FakeGitRepositoryState { pub index_contents: HashMap, - pub git_statuses: HashMap, + pub worktree_statuses: HashMap, pub branch_name: Option, } @@ -126,18 +139,18 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> Option> { + fn worktree_statuses(&self) -> Option> { let state = self.state.lock(); let mut map = TreeMap::default(); - for (repo_path, status) in state.git_statuses.iter() { + for (repo_path, status) in state.worktree_statuses.iter() { map.insert(repo_path.to_owned(), status.to_owned()); } Some(map) } - fn file_status(&self, path: &RepoPath) -> Option { + fn worktree_status(&self, path: &RepoPath) -> Option { let state = self.state.lock(); - state.git_statuses.get(path).cloned() + state.worktree_statuses.get(path).cloned() } } @@ -170,32 +183,11 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { } } -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub enum GitStatus { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GitFileStatus { Added, Modified, Conflict, - #[default] - Untracked, -} - -impl From for GitStatus { - fn from(value: Status) -> Self { - if value.contains(git2::Status::CONFLICTED) { - GitStatus::Conflict - } else if value.intersects( - git2::Status::INDEX_MODIFIED - | git2::Status::WT_MODIFIED - | git2::Status::INDEX_RENAMED - | git2::Status::WT_RENAMED, - ) { - GitStatus::Modified - } else if value.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) { - GitStatus::Added - } else { - GitStatus::Untracked - } - } } #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 82c719f31e..1fc4fcf5a8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -7,7 +7,7 @@ use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; use fs::{ - repository::{GitRepository, GitStatus, RepoPath}, + repository::{GitRepository, GitFileStatus, RepoPath}, Fs, LineEnding, }; use futures::{ @@ -143,7 +143,7 @@ impl Snapshot { pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, - pub(crate) statuses: TreeMap, + pub(crate) worktree_statuses: TreeMap, } impl RepositoryEntry { @@ -165,10 +165,10 @@ impl RepositoryEntry { self.work_directory.contains(snapshot, path) } - pub fn status_for(&self, snapshot: &Snapshot, path: &Path) -> Option { + pub fn status_for(&self, snapshot: &Snapshot, path: &Path) -> Option { self.work_directory .relativize(snapshot, path) - .and_then(|repo_path| self.statuses.get(&repo_path)) + .and_then(|repo_path| self.worktree_statuses.get(&repo_path)) .cloned() } } @@ -1445,7 +1445,7 @@ impl Snapshot { work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(), branch: repository.branch.map(Into::into), // TODO: status - statuses: Default::default(), + worktree_statuses: Default::default(), }; if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { self.repository_entries @@ -1864,7 +1864,7 @@ impl LocalSnapshot { RepositoryEntry { work_directory: work_dir_id.into(), branch: repo_lock.branch_name().map(Into::into), - statuses: repo_lock.statuses().unwrap_or_default(), + worktree_statuses: repo_lock.worktree_statuses().unwrap_or_default(), }, ); drop(repo_lock); @@ -2896,9 +2896,12 @@ impl BackgroundScanner { .git_repositories .update(&work_dir_id, |entry| entry.scan_id = scan_id); + // TODO: Status Replace linear scan with smarter sum tree traversal snapshot .repository_entries - .update(&work_dir, |entry| entry.statuses.remove(&repo_path)); + .update(&work_dir, |entry| entry.worktree_statuses.retain(|stored_path, _| { + !stored_path.starts_with(&repo_path) + })); } Some(()) @@ -2920,7 +2923,7 @@ impl BackgroundScanner { let repo = repo.lock(); repo.reload_index(); let branch = repo.branch_name(); - let statuses = repo.statuses().unwrap_or_default(); + let statuses = repo.worktree_statuses().unwrap_or_default(); snapshot.git_repositories.update(&entry_id, |entry| { entry.scan_id = scan_id; @@ -2929,7 +2932,7 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { entry.branch = branch.map(Into::into); - entry.statuses = statuses; + entry.worktree_statuses = statuses; }); } else { let repo = snapshot.repo_for(&path)?; @@ -2945,21 +2948,19 @@ impl BackgroundScanner { } let git_ptr = local_repo.repo_ptr.lock(); - git_ptr.file_status(&repo_path)? + git_ptr.worktree_status(&repo_path)? }; - if status != GitStatus::Untracked { - let work_dir = repo.work_directory(snapshot)?; - let work_dir_id = repo.work_directory; + let work_dir = repo.work_directory(snapshot)?; + let work_dir_id = repo.work_directory; - snapshot - .git_repositories - .update(&work_dir_id, |entry| entry.scan_id = scan_id); + snapshot + .git_repositories + .update(&work_dir_id, |entry| entry.scan_id = scan_id); - snapshot - .repository_entries - .update(&work_dir, |entry| entry.statuses.insert(repo_path, status)); - } + snapshot + .repository_entries + .update(&work_dir, |entry| entry.worktree_statuses.insert(repo_path, status)); } Some(()) @@ -3848,6 +3849,11 @@ mod tests { "project": { "a.txt": "a", "b.txt": "bb", + "c": { + "d": { + "e.txt": "eee" + } + } }, })); @@ -3865,18 +3871,22 @@ mod tests { .await .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; + const E_TXT: &'static str = "c/d/e.txt"; + let work_dir = root.path().join("project"); let mut repo = git_init(work_dir.as_path()); git_add(Path::new(A_TXT), &repo); + git_add(Path::new(E_TXT), &repo); git_commit("Initial commit", &repo); std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; tree.flush_fs_events(cx).await; // Check that the right git state is observed on startup @@ -3886,14 +3896,14 @@ mod tests { let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(dir.0.as_ref(), Path::new("project")); - assert_eq!(repo.statuses.iter().count(), 2); + assert_eq!(repo.worktree_statuses.iter().count(), 2); assert_eq!( - repo.statuses.get(&Path::new(A_TXT).into()), - Some(&GitStatus::Modified) + repo.worktree_statuses.get(&Path::new(A_TXT).into()), + Some(&GitFileStatus::Modified) ); assert_eq!( - repo.statuses.get(&Path::new(B_TXT).into()), - Some(&GitStatus::Added) + repo.worktree_statuses.get(&Path::new(B_TXT).into()), + Some(&GitFileStatus::Added) ); }); @@ -3907,14 +3917,15 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.statuses.iter().count(), 0); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!(repo.worktree_statuses.iter().count(), 0); + assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); }); git_reset(0, &repo); git_remove_index(Path::new(B_TXT), &repo); git_stash(&mut repo); + std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); tree.flush_fs_events(cx).await; // Check that more complex repo changes are tracked @@ -3922,17 +3933,21 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - dbg!(&repo.statuses); - - assert_eq!(repo.statuses.iter().count(), 1); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.worktree_statuses.iter().count(), 2); + assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); assert_eq!( - repo.statuses.get(&Path::new(B_TXT).into()), - Some(&GitStatus::Added) + repo.worktree_statuses.get(&Path::new(B_TXT).into()), + Some(&GitFileStatus::Added) + ); + assert_eq!( + repo.worktree_statuses.get(&Path::new(E_TXT).into()), + Some(&GitFileStatus::Modified) ); }); std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); + std::fs::remove_dir_all(work_dir.join("c")).unwrap(); + tree.flush_fs_events(cx).await; // Check that non-repo behavior is tracked @@ -3940,9 +3955,10 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.statuses.iter().count(), 0); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!(repo.worktree_statuses.iter().count(), 0); + assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!(repo.worktree_statuses.get(&Path::new(E_TXT).into()), None); }); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 16b6232e8b..49741ea49f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -17,7 +17,7 @@ use gpui::{ }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ - repository::GitStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, + repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, }; use settings::Settings; @@ -89,7 +89,7 @@ pub struct EntryDetails { is_editing: bool, is_processing: bool, is_cut: bool, - git_status: Option, + git_status: Option, } actions!( @@ -1081,19 +1081,14 @@ impl ProjectPanel { // Prepare colors for git statuses let editor_theme = &cx.global::().theme.editor; - let color_for_added = Some(editor_theme.diff.inserted); - let color_for_modified = Some(editor_theme.diff.modified); - let color_for_conflict = Some(editor_theme.diff.deleted); - let color_for_untracked = None; let mut filename_text_style = style.text.clone(); filename_text_style.color = details .git_status .as_ref() - .and_then(|status| match status { - GitStatus::Added => color_for_added, - GitStatus::Modified => color_for_modified, - GitStatus::Conflict => color_for_conflict, - GitStatus::Untracked => color_for_untracked, + .map(|status| match status { + GitFileStatus::Added => editor_theme.diff.inserted, + GitFileStatus::Modified => editor_theme.diff.modified, + GitFileStatus::Conflict => editor_theme.diff.deleted, }) .unwrap_or(style.text.color); From 00b345fdfe426b00c7da0219585eda03c20a9171 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 11:28:39 -0700 Subject: [PATCH 20/97] Use sum tree traversal to remove paths --- crates/project/src/worktree.rs | 26 ++++++++----- crates/sum_tree/src/tree_map.rs | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1fc4fcf5a8..ff9f7fde9a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -7,7 +7,7 @@ use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; use fs::{ - repository::{GitRepository, GitFileStatus, RepoPath}, + repository::{GitFileStatus, GitRepository, RepoPath}, Fs, LineEnding, }; use futures::{ @@ -46,6 +46,7 @@ use std::{ future::Future, mem, ops::{Deref, DerefMut}, + path::{Path, PathBuf}, pin::Pin, sync::{ @@ -2896,12 +2897,13 @@ impl BackgroundScanner { .git_repositories .update(&work_dir_id, |entry| entry.scan_id = scan_id); - // TODO: Status Replace linear scan with smarter sum tree traversal - snapshot - .repository_entries - .update(&work_dir, |entry| entry.worktree_statuses.retain(|stored_path, _| { - !stored_path.starts_with(&repo_path) - })); + snapshot.repository_entries.update(&work_dir, |entry| { + entry + .worktree_statuses + .remove_from_while(&repo_path, |stored_path, _| { + stored_path.starts_with(&repo_path) + }) + }); } Some(()) @@ -2958,9 +2960,9 @@ impl BackgroundScanner { .git_repositories .update(&work_dir_id, |entry| entry.scan_id = scan_id); - snapshot - .repository_entries - .update(&work_dir, |entry| entry.worktree_statuses.insert(repo_path, status)); + snapshot.repository_entries.update(&work_dir, |entry| { + entry.worktree_statuses.insert(repo_path, status) + }); } Some(()) @@ -3948,6 +3950,8 @@ mod tests { std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); std::fs::remove_dir_all(work_dir.join("c")).unwrap(); + dbg!(git_status(&repo)); + tree.flush_fs_events(cx).await; // Check that non-repo behavior is tracked @@ -3955,6 +3959,8 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + dbg!(&repo.worktree_statuses); + assert_eq!(repo.worktree_statuses.iter().count(), 0); assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index ab37d2577a..359137d439 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -82,6 +82,36 @@ impl TreeMap { cursor.item().map(|item| (&item.key, &item.value)) } + pub fn remove_between(&mut self, from: &K, until: &K) + { + let mut cursor = self.0.cursor::>(); + let from_key = MapKeyRef(Some(from)); + let mut new_tree = cursor.slice(&from_key, Bias::Left, &()); + let until_key = MapKeyRef(Some(until)); + cursor.seek_forward(&until_key, Bias::Left, &()); + new_tree.push_tree(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + } + + pub fn remove_from_while(&mut self, from: &K, mut f: F) + where F: FnMut(&K, &V) -> bool + { + let mut cursor = self.0.cursor::>(); + let from_key = MapKeyRef(Some(from)); + let mut new_tree = cursor.slice(&from_key, Bias::Left, &()); + while let Some(item) = cursor.item() { + if !f(&item.key, &item.value) { + break; + } + cursor.next(&()); + } + new_tree.push_tree(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + } + + pub fn update(&mut self, key: &K, f: F) -> Option where F: FnOnce(&mut V) -> T, @@ -272,4 +302,42 @@ mod tests { map.retain(|key, _| *key % 2 == 0); assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); } + + #[test] + fn test_remove_between() { + let mut map = TreeMap::default(); + + map.insert("a", 1); + map.insert("b", 2); + map.insert("baa", 3); + map.insert("baaab", 4); + map.insert("c", 5); + + map.remove_between(&"ba", &"bb"); + + assert_eq!(map.get(&"a"), Some(&1)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"baaa"), None); + assert_eq!(map.get(&"baaaab"), None); + assert_eq!(map.get(&"c"), Some(&5)); + } + + #[test] + fn test_remove_from_while() { + let mut map = TreeMap::default(); + + map.insert("a", 1); + map.insert("b", 2); + map.insert("baa", 3); + map.insert("baaab", 4); + map.insert("c", 5); + + map.remove_from_while(&"ba", |key, _| key.starts_with(&"ba")); + + assert_eq!(map.get(&"a"), Some(&1)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"baaa"), None); + assert_eq!(map.get(&"baaaab"), None); + assert_eq!(map.get(&"c"), Some(&5)); + } } From 2b80dfa81d2e4ed6f36443b28a18906412ef1419 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 11:59:04 -0700 Subject: [PATCH 21/97] Update protos --- crates/collab/src/db.rs | 8 ++++---- crates/project/src/worktree.rs | 25 +++++++++++++------------ crates/rpc/proto/zed.proto | 6 +++--- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 5867fa7369..b9bd1374eb 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1568,8 +1568,8 @@ impl Database { worktree.updated_repositories.push(proto::RepositoryEntry { work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, - removed_statuses: Default::default(), - updated_statuses: Default::default(), + removed_worktree_repo_paths: Default::default(), + updated_worktree_statuses: Default::default(), }); } @@ -2651,8 +2651,8 @@ impl Database { worktree.repository_entries.push(proto::RepositoryEntry { work_directory_id: db_repository_entry.work_directory_id as u64, branch: db_repository_entry.branch, - removed_statuses: Default::default(), - updated_statuses: Default::default(), + removed_worktree_repo_paths: Default::default(), + updated_worktree_statuses: Default::default(), }); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index ff9f7fde9a..7b760ae354 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -180,8 +180,8 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), // TODO: Status - removed_statuses: Default::default(), - updated_statuses: Default::default(), + removed_worktree_repo_paths: Default::default(), + updated_worktree_statuses: Default::default(), } } } @@ -1597,12 +1597,11 @@ impl LocalSnapshot { pub(crate) fn repo_for_metadata( &self, path: &Path, - ) -> Option<(ProjectEntryId, Arc>)> { - let (entry_id, local_repo) = self + ) -> Option<(&ProjectEntryId, &LocalRepositoryEntry)> { + self .git_repositories .iter() - .find(|(_, repo)| repo.in_dot_git(path))?; - Some((*entry_id, local_repo.repo_ptr.to_owned())) + .find(|(_, repo)| repo.in_dot_git(path)) } #[cfg(test)] @@ -2916,13 +2915,19 @@ impl BackgroundScanner { .components() .any(|component| component.as_os_str() == *DOT_GIT) { - let (entry_id, repo) = snapshot.repo_for_metadata(&path)?; + let (entry_id, repo_ptr) = { + let (entry_id, repo) = snapshot.repo_for_metadata(&path)?; + if repo.full_scan_id == scan_id { + return None; + } + (*entry_id, repo.repo_ptr.to_owned()) + }; let work_dir = snapshot .entry_for_id(entry_id) .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; - let repo = repo.lock(); + let repo = repo_ptr.lock(); repo.reload_index(); let branch = repo.branch_name(); let statuses = repo.worktree_statuses().unwrap_or_default(); @@ -3950,8 +3955,6 @@ mod tests { std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); std::fs::remove_dir_all(work_dir.join("c")).unwrap(); - dbg!(git_status(&repo)); - tree.flush_fs_events(cx).await; // Check that non-repo behavior is tracked @@ -3959,8 +3962,6 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - dbg!(&repo.worktree_statuses); - assert_eq!(repo.worktree_statuses.iter().count(), 0); assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index abe02f42bb..8e45435b89 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -986,12 +986,12 @@ message Entry { message RepositoryEntry { uint64 work_directory_id = 1; optional string branch = 2; - repeated uint64 removed_statuses = 3; - repeated StatusEntry updated_statuses = 4; + repeated string removed_worktree_repo_paths = 3; + repeated StatusEntry updated_worktree_statuses = 4; } message StatusEntry { - uint64 entry_id = 1; + string repo_path = 1; GitStatus status = 2; } From e20eaca59513622b903f201410b89fb2ac21525a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 16:07:41 -0700 Subject: [PATCH 22/97] Got basic replication working :) --- crates/collab/src/db.rs | 2 - crates/collab/src/tests/integration_tests.rs | 49 ++++++- crates/fs/src/fs.rs | 6 +- crates/fs/src/repository.rs | 2 +- crates/project/src/worktree.rs | 129 ++++++++++++++++--- crates/rpc/src/proto.rs | 65 +++++++++- crates/sum_tree/src/tree_map.rs | 44 ++++++- 7 files changed, 256 insertions(+), 41 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b9bd1374eb..217987984a 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1570,7 +1570,6 @@ impl Database { branch: db_repository.branch, removed_worktree_repo_paths: Default::default(), updated_worktree_statuses: Default::default(), - }); } } @@ -2653,7 +2652,6 @@ impl Database { branch: db_repository_entry.branch, removed_worktree_repo_paths: Default::default(), updated_worktree_statuses: Default::default(), - }); } } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 185e6c6354..7dd8f86b8e 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2748,7 +2748,12 @@ async fn test_git_status_sync( deterministic.run_until_parked(); #[track_caller] - fn assert_status(file: &impl AsRef, status: Option, project: &Project, cx: &AppContext) { + fn assert_status( + file: &impl AsRef, + status: Option, + project: &Project, + cx: &AppContext, + ) { let file = file.as_ref(); let worktrees = project.visible_worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -2785,19 +2790,49 @@ async fn test_git_status_sync( // Smoke test status reading project_local.read_with(cx_a, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); + assert_status( + &Path::new(A_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + assert_status( + &Path::new(B_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); }); project_remote.read_with(cx_b, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); + assert_status( + &Path::new(A_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + assert_status( + &Path::new(B_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); }); // And synchronization while joining let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; project_remote_c.read_with(cx_c, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); + assert_status( + &Path::new(A_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + assert_status( + &Path::new(B_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); }); } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index fd094160f5..09ddce2ffa 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,7 @@ use git2::Repository as LibGitRepository; use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; -use repository::{GitRepository, GitFileStatus}; +use repository::{GitFileStatus, GitRepository}; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; @@ -660,9 +660,7 @@ impl FakeFs { state.worktree_statuses.extend( statuses .iter() - .map(|(path, content)| { - ((**path).into(), content.clone()) - }), + .map(|(path, content)| ((**path).into(), content.clone())), ); }); } diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 3fb562570e..90b3761677 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -194,7 +194,7 @@ pub enum GitFileStatus { pub struct RepoPath(PathBuf); impl RepoPath { - fn new(path: PathBuf) -> Self { + pub fn new(path: PathBuf) -> Self { debug_assert!(path.is_relative(), "Repo paths must be relative"); RepoPath(path) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 7b760ae354..0d4a02775d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -46,7 +46,6 @@ use std::{ future::Future, mem, ops::{Deref, DerefMut}, - path::{Path, PathBuf}, pin::Pin, sync::{ @@ -147,6 +146,14 @@ pub struct RepositoryEntry { pub(crate) worktree_statuses: TreeMap, } +fn read_git_status(git_status: i32) -> Option { + proto::GitStatus::from_i32(git_status).map(|status| match status { + proto::GitStatus::Added => GitFileStatus::Added, + proto::GitStatus::Modified => GitFileStatus::Modified, + proto::GitStatus::Conflict => GitFileStatus::Conflict, + }) +} + impl RepositoryEntry { pub fn branch(&self) -> Option> { self.branch.clone() @@ -172,6 +179,70 @@ impl RepositoryEntry { .and_then(|repo_path| self.worktree_statuses.get(&repo_path)) .cloned() } + + pub fn build_update(&self, other: &Self) -> proto::RepositoryEntry { + let mut updated_statuses: Vec = Vec::new(); + let mut removed_statuses: Vec = Vec::new(); + + let mut self_statuses = self.worktree_statuses.iter().peekable(); + let mut other_statuses = other.worktree_statuses.iter().peekable(); + loop { + match (self_statuses.peek(), other_statuses.peek()) { + (Some((self_repo_path, self_status)), Some((other_repo_path, other_status))) => { + match Ord::cmp(self_repo_path, other_repo_path) { + Ordering::Less => { + updated_statuses.push(make_status_entry(self_repo_path, self_status)); + self_statuses.next(); + } + Ordering::Equal => { + if self_status != other_status { + updated_statuses + .push(make_status_entry(self_repo_path, self_status)); + } + + self_statuses.next(); + other_statuses.next(); + } + Ordering::Greater => { + removed_statuses.push(make_repo_path(other_repo_path)); + other_statuses.next(); + } + } + } + (Some((self_repo_path, self_status)), None) => { + updated_statuses.push(make_status_entry(self_repo_path, self_status)); + self_statuses.next(); + } + (None, Some((other_repo_path, _))) => { + removed_statuses.push(make_repo_path(other_repo_path)); + other_statuses.next(); + } + (None, None) => break, + } + } + + proto::RepositoryEntry { + work_directory_id: self.work_directory_id().to_proto(), + branch: self.branch.as_ref().map(|str| str.to_string()), + removed_worktree_repo_paths: removed_statuses, + updated_worktree_statuses: updated_statuses, + } + } +} + +fn make_repo_path(path: &RepoPath) -> String { + path.as_os_str().to_string_lossy().to_string() +} + +fn make_status_entry(path: &RepoPath, status: &GitFileStatus) -> proto::StatusEntry { + proto::StatusEntry { + repo_path: make_repo_path(path), + status: match status { + GitFileStatus::Added => proto::GitStatus::Added.into(), + GitFileStatus::Modified => proto::GitStatus::Modified.into(), + GitFileStatus::Conflict => proto::GitStatus::Conflict.into(), + }, + } } impl From<&RepositoryEntry> for proto::RepositoryEntry { @@ -179,9 +250,12 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), - // TODO: Status + updated_worktree_statuses: value + .worktree_statuses + .iter() + .map(|(repo_path, status)| make_status_entry(repo_path, status)) + .collect(), removed_worktree_repo_paths: Default::default(), - updated_worktree_statuses: Default::default(), } } } @@ -1442,15 +1516,41 @@ impl Snapshot { }); for repository in update.updated_repositories { - let repository = RepositoryEntry { - work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(), - branch: repository.branch.map(Into::into), - // TODO: status - worktree_statuses: Default::default(), - }; - if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { - self.repository_entries - .insert(RepositoryWorkDirectory(entry.path.clone()), repository) + 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 mut statuses = TreeMap::default(); + for status_entry in repository.updated_worktree_statuses { + let Some(git_file_status) = read_git_status(status_entry.status) else { + continue; + }; + + let repo_path = RepoPath::new(status_entry.repo_path.into()); + statuses.insert(repo_path, git_file_status); + } + + 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); + repo.worktree_statuses.insert_tree(statuses); + + for repo_path in repository.removed_worktree_repo_paths { + let repo_path = RepoPath::new(repo_path.into()); + repo.worktree_statuses.remove(&repo_path); + } + }); + } else { + self.repository_entries.insert( + work_directory, + RepositoryEntry { + work_directory: work_directory_entry, + branch: repository.branch.map(Into::into), + worktree_statuses: statuses, + }, + ) + } } else { log::error!("no work directory entry for repository {:?}", repository) } @@ -1598,8 +1698,7 @@ impl LocalSnapshot { &self, path: &Path, ) -> Option<(&ProjectEntryId, &LocalRepositoryEntry)> { - self - .git_repositories + self.git_repositories .iter() .find(|(_, repo)| repo.in_dot_git(path)) } @@ -1691,7 +1790,7 @@ impl LocalSnapshot { } Ordering::Equal => { if self_repo != other_repo { - updated_repositories.push((*self_repo).into()); + updated_repositories.push(self_repo.build_update(other_repo)); } self_repos.next(); diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 20a457cc4b..32f40ad7db 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -484,9 +484,11 @@ pub fn split_worktree_update( mut message: UpdateWorktree, max_chunk_size: usize, ) -> impl Iterator { - let mut done = false; + let mut done_files = false; + let mut done_statuses = false; + let mut repository_index = 0; iter::from_fn(move || { - if done { + if done_files && done_statuses { return None; } @@ -502,22 +504,71 @@ pub fn split_worktree_update( .drain(..removed_entries_chunk_size) .collect(); - done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); + done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty(); // Wait to send repositories until after we've guaranteed that their associated entries // will be read - let updated_repositories = if done { - mem::take(&mut message.updated_repositories) + let updated_repositories = if done_files { + let mut total_statuses = 0; + let mut updated_repositories = Vec::new(); + while total_statuses < max_chunk_size + && repository_index < message.updated_repositories.len() + { + let updated_statuses_chunk_size = cmp::min( + message.updated_repositories[repository_index] + .updated_worktree_statuses + .len(), + max_chunk_size - total_statuses, + ); + + let updated_statuses: Vec<_> = message.updated_repositories[repository_index] + .updated_worktree_statuses + .drain(..updated_statuses_chunk_size) + .collect(); + + total_statuses += updated_statuses.len(); + + let done_this_repo = message.updated_repositories[repository_index] + .updated_worktree_statuses + .is_empty(); + + let removed_repo_paths = if done_this_repo { + mem::take( + &mut message.updated_repositories[repository_index] + .removed_worktree_repo_paths, + ) + } else { + Default::default() + }; + + updated_repositories.push(RepositoryEntry { + work_directory_id: message.updated_repositories[repository_index] + .work_directory_id, + branch: message.updated_repositories[repository_index] + .branch + .clone(), + updated_worktree_statuses: updated_statuses, + removed_worktree_repo_paths: removed_repo_paths, + }); + + if done_this_repo { + repository_index += 1; + } + } + + updated_repositories } else { Default::default() }; - let removed_repositories = if done { + let removed_repositories = if done_files && done_statuses { mem::take(&mut message.removed_repositories) } else { Default::default() }; + done_statuses = repository_index >= message.updated_repositories.len(); + Some(UpdateWorktree { project_id: message.project_id, worktree_id: message.worktree_id, @@ -526,7 +577,7 @@ pub fn split_worktree_update( updated_entries, removed_entries, scan_id: message.scan_id, - is_last_update: done && message.is_last_update, + is_last_update: done_files && message.is_last_update, updated_repositories, removed_repositories, }) diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 359137d439..3942d00b29 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -1,6 +1,6 @@ use std::{cmp::Ordering, fmt::Debug}; -use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary}; +use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeMap(SumTree>) @@ -82,8 +82,7 @@ impl TreeMap { cursor.item().map(|item| (&item.key, &item.value)) } - pub fn remove_between(&mut self, from: &K, until: &K) - { + pub fn remove_between(&mut self, from: &K, until: &K) { let mut cursor = self.0.cursor::>(); let from_key = MapKeyRef(Some(from)); let mut new_tree = cursor.slice(&from_key, Bias::Left, &()); @@ -95,7 +94,8 @@ impl TreeMap { } pub fn remove_from_while(&mut self, from: &K, mut f: F) - where F: FnMut(&K, &V) -> bool + where + F: FnMut(&K, &V) -> bool, { let mut cursor = self.0.cursor::>(); let from_key = MapKeyRef(Some(from)); @@ -111,7 +111,6 @@ impl TreeMap { self.0 = new_tree; } - pub fn update(&mut self, key: &K, f: F) -> Option where F: FnOnce(&mut V) -> T, @@ -155,6 +154,20 @@ impl TreeMap { pub fn values(&self) -> impl Iterator + '_ { self.0.iter().map(|entry| &entry.value) } + + pub fn insert_tree(&mut self, other: TreeMap) { + let edits = other + .iter() + .map(|(key, value)| { + Edit::Insert(MapEntry { + key: key.to_owned(), + value: value.to_owned(), + }) + }) + .collect(); + + self.0.edit(edits, &()); + } } impl Default for TreeMap @@ -340,4 +353,25 @@ mod tests { assert_eq!(map.get(&"baaaab"), None); assert_eq!(map.get(&"c"), Some(&5)); } + + #[test] + fn test_insert_tree() { + let mut map = TreeMap::default(); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 3); + + let mut other = TreeMap::default(); + other.insert("a", 2); + other.insert("b", 2); + other.insert("d", 4); + + map.insert_tree(other); + + assert_eq!(map.iter().count(), 4); + assert_eq!(map.get(&"a"), Some(&2)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"c"), Some(&3)); + assert_eq!(map.get(&"d"), Some(&4)); + } } From 65d4c4f6ed62bd25997c528b3fbd1c0fe8d044ce Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 17:36:16 -0700 Subject: [PATCH 23/97] Add integration test for git status --- .../20221109000000_test_schema.sql | 19 ++- crates/collab/src/db.rs | 134 +++++++++++++++++- crates/collab/src/rpc.rs | 2 +- crates/collab/src/tests/integration_tests.rs | 2 + 4 files changed, 148 insertions(+), 9 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 684b6bffe0..7c6a49f179 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -86,8 +86,8 @@ CREATE TABLE "worktree_repositories" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, "work_directory_id" INTEGER NOT NULL, - "scan_id" INTEGER NOT NULL, "branch" VARCHAR, + "scan_id" INTEGER NOT NULL, "is_deleted" BOOL NOT NULL, PRIMARY KEY(project_id, worktree_id, work_directory_id), FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, @@ -96,6 +96,23 @@ CREATE TABLE "worktree_repositories" ( CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id"); CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); +CREATE TABLE "worktree_repository_statuses" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INTEGER NOT NULL, + "work_directory_id" INTEGER NOT NULL, + "repo_path" VARCHAR NOT NULL, + "status" INTEGER NOT NULL, + "scan_id" INTEGER NOT NULL, + "is_deleted" BOOL NOT NULL, + PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, + FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id"); +CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); +CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); + + CREATE TABLE "worktree_diagnostic_summaries" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 217987984a..cc85d4f369 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -15,6 +15,7 @@ mod worktree; mod worktree_diagnostic_summary; mod worktree_entry; mod worktree_repository; +mod worktree_repository_statuses; use crate::executor::Executor; use crate::{Error, Result}; @@ -2397,6 +2398,74 @@ impl Database { ) .exec(&*tx) .await?; + + for repository in update.updated_repositories.iter() { + if !repository.updated_worktree_statuses.is_empty() { + worktree_repository_statuses::Entity::insert_many( + repository + .updated_worktree_statuses + .iter() + .map(|status_entry| worktree_repository_statuses::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + work_directory_id: ActiveValue::set( + repository.work_directory_id as i64, + ), + repo_path: ActiveValue::set(status_entry.repo_path.clone()), + status: ActiveValue::set(status_entry.status as i64), + scan_id: ActiveValue::set(update.scan_id as i64), + is_deleted: ActiveValue::set(false), + }), + ) + .on_conflict( + OnConflict::columns([ + worktree_repository_statuses::Column::ProjectId, + worktree_repository_statuses::Column::WorktreeId, + worktree_repository_statuses::Column::WorkDirectoryId, + worktree_repository_statuses::Column::RepoPath, + ]) + .update_columns([ + worktree_repository_statuses::Column::ScanId, + worktree_repository_statuses::Column::Status, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + if !repository.removed_worktree_repo_paths.is_empty() { + worktree_repository_statuses::Entity::update_many() + .filter( + worktree_repository_statuses::Column::ProjectId + .eq(project_id) + .and( + worktree_repository_statuses::Column::WorktreeId + .eq(worktree_id), + ) + .and( + worktree_repository_statuses::Column::WorkDirectoryId + .eq(repository.work_directory_id), + ) + .and( + worktree_repository_statuses::Column::RepoPath.is_in( + repository + .removed_worktree_repo_paths + .iter() + .cloned() + .collect::>(), + ), + ), + ) + .set(worktree_repository_statuses::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + } } if !update.removed_repositories.is_empty() { @@ -2417,6 +2486,25 @@ impl Database { }) .exec(&*tx) .await?; + + // Flip all status entries associated with a given repository_entry + worktree_repository_statuses::Entity::update_many() + .filter( + worktree_repository_statuses::Column::ProjectId + .eq(project_id) + .and(worktree_repository_statuses::Column::WorktreeId.eq(worktree_id)) + .and( + worktree_repository_statuses::Column::WorkDirectoryId + .is_in(update.removed_repositories.iter().map(|id| *id as i64)), + ), + ) + .set(worktree_repository_statuses::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; } let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; @@ -2647,12 +2735,44 @@ impl Database { if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) { - worktree.repository_entries.push(proto::RepositoryEntry { - work_directory_id: db_repository_entry.work_directory_id as u64, - branch: db_repository_entry.branch, - removed_worktree_repo_paths: Default::default(), - updated_worktree_statuses: Default::default(), - }); + worktree.repository_entries.insert( + db_repository_entry.work_directory_id as u64, + proto::RepositoryEntry { + work_directory_id: db_repository_entry.work_directory_id as u64, + branch: db_repository_entry.branch, + removed_worktree_repo_paths: Default::default(), + updated_worktree_statuses: Default::default(), + }, + ); + } + } + } + + { + let mut db_status_entries = worktree_repository_statuses::Entity::find() + .filter( + Condition::all() + .add(worktree_repository_statuses::Column::ProjectId.eq(project_id)) + .add(worktree_repository_statuses::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + + while let Some(db_status_entry) = db_status_entries.next().await { + let db_status_entry = db_status_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64)) + { + if let Some(repository_entry) = worktree + .repository_entries + .get_mut(&(db_status_entry.work_directory_id as u64)) + { + repository_entry + .updated_worktree_statuses + .push(proto::StatusEntry { + repo_path: db_status_entry.repo_path, + status: db_status_entry.status as i32, + }); + } } } } @@ -3394,7 +3514,7 @@ pub struct Worktree { pub root_name: String, pub visible: bool, pub entries: Vec, - pub repository_entries: Vec, + pub repository_entries: BTreeMap, pub diagnostic_summaries: Vec, pub scan_id: u64, pub completed_scan_id: u64, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 23935904d3..001f3462d0 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1385,7 +1385,7 @@ async fn join_project( removed_entries: Default::default(), scan_id: worktree.scan_id, is_last_update: worktree.scan_id == worktree.completed_scan_id, - updated_repositories: worktree.repository_entries, + updated_repositories: worktree.repository_entries.into_values().collect(), removed_repositories: Default::default(), }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 7dd8f86b8e..aefc172268 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2820,6 +2820,8 @@ async fn test_git_status_sync( // And synchronization while joining let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; + deterministic.run_until_parked(); + project_remote_c.read_with(cx_c, |project, cx| { assert_status( &Path::new(A_TXT), From c7166fde3bcb88162862411d6d9e9390fe8c17a4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 17:38:29 -0700 Subject: [PATCH 24/97] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index e51ded5969..64fbf19462 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 54; +pub const PROTOCOL_VERSION: u32 = 55; From 18becabfa593ec2da7735c6c7edec75ff18c6445 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 17:50:30 -0700 Subject: [PATCH 25/97] Add postgres migration --- ...20230511004019_add_repository_statuses.sql | 15 ++++++++++++ .../src/db/worktree_repository_statuses.rs | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 crates/collab/migrations/20230511004019_add_repository_statuses.sql create mode 100644 crates/collab/src/db/worktree_repository_statuses.rs diff --git a/crates/collab/migrations/20230511004019_add_repository_statuses.sql b/crates/collab/migrations/20230511004019_add_repository_statuses.sql new file mode 100644 index 0000000000..862561c686 --- /dev/null +++ b/crates/collab/migrations/20230511004019_add_repository_statuses.sql @@ -0,0 +1,15 @@ +CREATE TABLE "worktree_repository_statuses" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INT8 NOT NULL, + "work_directory_id" INT8 NOT NULL, + "repo_path" VARCHAR NOT NULL, + "status" INT8 NOT NULL, + "scan_id" INT8 NOT NULL, + "is_deleted" BOOL NOT NULL, + PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, + FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id"); +CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); +CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); diff --git a/crates/collab/src/db/worktree_repository_statuses.rs b/crates/collab/src/db/worktree_repository_statuses.rs new file mode 100644 index 0000000000..fc15efc816 --- /dev/null +++ b/crates/collab/src/db/worktree_repository_statuses.rs @@ -0,0 +1,23 @@ +use super::ProjectId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "worktree_repository_statuses")] +pub struct Model { + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub worktree_id: i64, + #[sea_orm(primary_key)] + pub work_directory_id: i64, + #[sea_orm(primary_key)] + pub repo_path: String, + pub status: i64, + pub scan_id: i64, + pub is_deleted: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} From f55ca7ae3c65d18581c36ba4515ad6f725e88477 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 17:52:23 -0700 Subject: [PATCH 26/97] Fix incorrect import --- crates/fs/src/fs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 09ddce2ffa..3285eb328a 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,7 @@ use git2::Repository as LibGitRepository; use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; -use repository::{GitFileStatus, GitRepository}; +use repository::GitRepository; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; @@ -27,7 +27,7 @@ use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] -use repository::FakeGitRepositoryState; +use repository::{FakeGitRepositoryState, GitFileStatus}; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; From 9800a149a61480cd3464acc373c3e48a728e465b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 17:59:33 -0700 Subject: [PATCH 27/97] Remove some external context from git status test --- crates/project/src/worktree.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 0d4a02775d..a970067230 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3556,6 +3556,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { mod tests { use super::*; use fs::{FakeFs, RealFs}; + use git2::Signature; use gpui::{executor::Deterministic, TestAppContext}; use pretty_assertions::assert_eq; use rand::prelude::*; @@ -3894,7 +3895,7 @@ mod tests { #[track_caller] fn git_commit(msg: &'static str, repo: &git2::Repository) { - let signature = repo.signature().unwrap(); + let signature = Signature::now("test", "test@zed.dev").unwrap(); let oid = repo.index().unwrap().write_tree().unwrap(); let tree = repo.find_tree(oid).unwrap(); if let Some(head) = repo.head().ok() { From fca3bb3b93a38119771e04272d67c60f3e062783 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 19:21:27 -0700 Subject: [PATCH 28/97] Add randomized test for git statuses --- crates/collab/src/db.rs | 39 +++ .../src/tests/randomized_integration_tests.rs | 259 +++++++++++------- crates/fs/src/repository.rs | 3 +- 3 files changed, 207 insertions(+), 94 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index cc85d4f369..e881121758 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1576,6 +1576,45 @@ impl Database { } } + // Repository Status Entries + for repository in worktree.updated_repositories.iter_mut() { + let repository_status_entry_filter = + if let Some(rejoined_worktree) = rejoined_worktree { + worktree_repository_statuses::Column::ScanId + .gt(rejoined_worktree.scan_id) + } else { + worktree_repository_statuses::Column::IsDeleted.eq(false) + }; + + let mut db_repository_statuses = + worktree_repository_statuses::Entity::find() + .filter( + Condition::all() + .add( + worktree_repository_statuses::Column::WorktreeId + .eq(worktree.id), + ) + .add( + worktree_repository_statuses::Column::WorkDirectoryId + .eq(repository.work_directory_id), + ) + .add(repository_status_entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_status_entry) = db_repository_statuses.next().await { + let db_status_entry = db_status_entry?; + if db_status_entry.is_deleted { + repository.removed_worktree_repo_paths.push(db_status_entry.repo_path); + } else { + repository.updated_worktree_statuses.push(proto::StatusEntry { + repo_path: db_status_entry.repo_path, status: db_status_entry.status as i32 + }); + } + } + } + worktrees.push(worktree); } diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index c4326be101..d5ed47675a 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -8,7 +8,7 @@ use call::ActiveCall; use client::RECEIVE_TIMEOUT; use collections::BTreeMap; use editor::Bias; -use fs::{FakeFs, Fs as _}; +use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt as _; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext}; use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; @@ -32,6 +32,7 @@ use std::{ }, }; use util::ResultExt; +use pretty_assertions::assert_eq; lazy_static::lazy_static! { static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); @@ -763,53 +764,81 @@ async fn apply_client_operation( } } - ClientOperation::WriteGitIndex { - repo_path, - contents, - } => { - if !client.fs.directories().contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - log::info!( - "{}: writing git index for repo {:?}: {:?}", - client.username, + ClientOperation::GitOperation { operation } => match operation { + GitOperation::WriteGitIndex { repo_path, - contents - ); + contents, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } - let dot_git_dir = repo_path.join(".git"); - let contents = contents - .iter() - .map(|(path, contents)| (path.as_path(), contents.clone())) - .collect::>(); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + log::info!( + "{}: writing git index for repo {:?}: {:?}", + client.username, + repo_path, + contents + ); + + let dot_git_dir = repo_path.join(".git"); + let contents = contents + .iter() + .map(|(path, contents)| (path.as_path(), contents.clone())) + .collect::>(); + if client.fs.metadata(&dot_git_dir).await?.is_none() { + client.fs.create_dir(&dot_git_dir).await?; + } + client.fs.set_index_for_repo(&dot_git_dir, &contents).await; } - client.fs.set_index_for_repo(&dot_git_dir, &contents).await; - } - - ClientOperation::WriteGitBranch { - repo_path, - new_branch, - } => { - if !client.fs.directories().contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - log::info!( - "{}: writing git branch for repo {:?}: {:?}", - client.username, + GitOperation::WriteGitBranch { repo_path, - new_branch - ); + new_branch, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } - let dot_git_dir = repo_path.join(".git"); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + log::info!( + "{}: writing git branch for repo {:?}: {:?}", + client.username, + repo_path, + new_branch + ); + + let dot_git_dir = repo_path.join(".git"); + if client.fs.metadata(&dot_git_dir).await?.is_none() { + client.fs.create_dir(&dot_git_dir).await?; + } + client.fs.set_branch_name(&dot_git_dir, new_branch).await; } - client.fs.set_branch_name(&dot_git_dir, new_branch).await; - } + GitOperation::WriteGitStatuses { + repo_path, + statuses, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } + + log::info!( + "{}: writing git statuses for repo {:?}: {:?}", + client.username, + repo_path, + statuses + ); + + let dot_git_dir = repo_path.join(".git"); + + let statuses = statuses.iter() + .map(|(path, val)| (path.as_path(), val.clone())) + .collect::>(); + + if client.fs.metadata(&dot_git_dir).await?.is_none() { + client.fs.create_dir(&dot_git_dir).await?; + } + + client.fs.set_status_for_repo(&dot_git_dir, statuses.as_slice()).await; + }, + }, } Ok(()) } @@ -1178,6 +1207,13 @@ enum ClientOperation { is_dir: bool, content: String, }, + GitOperation { + operation: GitOperation, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum GitOperation { WriteGitIndex { repo_path: PathBuf, contents: Vec<(PathBuf, String)>, @@ -1186,6 +1222,10 @@ enum ClientOperation { repo_path: PathBuf, new_branch: Option, }, + WriteGitStatuses { + repo_path: PathBuf, + statuses: Vec<(PathBuf, GitFileStatus)>, + }, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -1698,57 +1738,10 @@ impl TestPlan { } } - // Update a git index - 91..=93 => { - let repo_path = client - .fs - .directories() - .into_iter() - .choose(&mut self.rng) - .unwrap() - .clone(); - - let mut file_paths = client - .fs - .files() - .into_iter() - .filter(|path| path.starts_with(&repo_path)) - .collect::>(); - let count = self.rng.gen_range(0..=file_paths.len()); - file_paths.shuffle(&mut self.rng); - file_paths.truncate(count); - - let mut contents = Vec::new(); - for abs_child_file_path in &file_paths { - let child_file_path = abs_child_file_path - .strip_prefix(&repo_path) - .unwrap() - .to_path_buf(); - let new_base = Alphanumeric.sample_string(&mut self.rng, 16); - contents.push((child_file_path, new_base)); - } - - break ClientOperation::WriteGitIndex { - repo_path, - contents, - }; - } - - // Update a git branch - 94..=95 => { - let repo_path = client - .fs - .directories() - .choose(&mut self.rng) - .unwrap() - .clone(); - - let new_branch = (self.rng.gen_range(0..10) > 3) - .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); - - break ClientOperation::WriteGitBranch { - repo_path, - new_branch, + // Update a git related action + 91..=95 => { + break ClientOperation::GitOperation { + operation: self.generate_git_operation(client), }; } @@ -1786,6 +1779,86 @@ impl TestPlan { }) } + fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation { + fn generate_file_paths( + repo_path: &Path, + rng: &mut StdRng, + client: &TestClient, + ) -> Vec { + let mut paths = client + .fs + .files() + .into_iter() + .filter(|path| path.starts_with(repo_path)) + .collect::>(); + + let count = rng.gen_range(0..=paths.len()); + paths.shuffle(rng); + paths.truncate(count); + + paths + .iter() + .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf()) + .collect::>() + } + + let repo_path = client + .fs + .directories() + .choose(&mut self.rng) + .unwrap() + .clone(); + + match self.rng.gen_range(0..100_u32) { + 0..=25 => { + let file_paths = generate_file_paths(&repo_path, &mut self.rng, client); + + let contents = file_paths + .into_iter() + .map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16))) + .collect(); + + GitOperation::WriteGitIndex { + repo_path, + contents, + } + } + 26..=63 => { + let new_branch = (self.rng.gen_range(0..10) > 3) + .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); + + GitOperation::WriteGitBranch { + repo_path, + new_branch, + } + } + 64..=100 => { + let file_paths = generate_file_paths(&repo_path, &mut self.rng, client); + + let statuses = file_paths + .into_iter() + .map(|paths| { + ( + paths, + match self.rng.gen_range(0..3_u32) { + 0 => GitFileStatus::Added, + 1 => GitFileStatus::Modified, + 2 => GitFileStatus::Conflict, + _ => unreachable!(), + }, + ) + }) + .collect::>(); + + GitOperation::WriteGitStatuses { + repo_path, + statuses, + } + } + _ => unreachable!(), + } + } + fn next_root_dir_name(&mut self, user_id: UserId) -> String { let user_ix = self .users diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 90b3761677..6bf0a43230 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,6 +1,7 @@ use anyhow::Result; use collections::HashMap; use parking_lot::Mutex; +use serde_derive::{Serialize, Deserialize}; use std::{ ffi::OsStr, os::unix::prelude::OsStrExt, @@ -183,7 +184,7 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum GitFileStatus { Added, Modified, From f5c633e80cc22583b7e72369f29388982170f19d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 19:54:02 -0700 Subject: [PATCH 29/97] Fixed bug in status deletion marking --- crates/collab/src/db.rs | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e881121758..cac84a0ccc 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2472,38 +2472,6 @@ impl Database { .exec(&*tx) .await?; } - - if !repository.removed_worktree_repo_paths.is_empty() { - worktree_repository_statuses::Entity::update_many() - .filter( - worktree_repository_statuses::Column::ProjectId - .eq(project_id) - .and( - worktree_repository_statuses::Column::WorktreeId - .eq(worktree_id), - ) - .and( - worktree_repository_statuses::Column::WorkDirectoryId - .eq(repository.work_directory_id), - ) - .and( - worktree_repository_statuses::Column::RepoPath.is_in( - repository - .removed_worktree_repo_paths - .iter() - .cloned() - .collect::>(), - ), - ), - ) - .set(worktree_repository_statuses::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } } } From adfbbf21b2267eef6e7f6deeb39731e8e7133e27 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 10 May 2023 20:09:37 -0700 Subject: [PATCH 30/97] fmt --- crates/collab/src/db.rs | 13 +++++++++---- .../src/tests/randomized_integration_tests.rs | 12 ++++++++---- crates/fs/src/repository.rs | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index cac84a0ccc..b95bb49b4e 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1606,11 +1606,16 @@ impl Database { while let Some(db_status_entry) = db_repository_statuses.next().await { let db_status_entry = db_status_entry?; if db_status_entry.is_deleted { - repository.removed_worktree_repo_paths.push(db_status_entry.repo_path); + repository + .removed_worktree_repo_paths + .push(db_status_entry.repo_path); } else { - repository.updated_worktree_statuses.push(proto::StatusEntry { - repo_path: db_status_entry.repo_path, status: db_status_entry.status as i32 - }); + repository + .updated_worktree_statuses + .push(proto::StatusEntry { + repo_path: db_status_entry.repo_path, + status: db_status_entry.status as i32, + }); } } } diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index d5ed47675a..fe4b6190ed 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -14,6 +14,7 @@ use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext}; use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; use lsp::FakeLanguageServer; use parking_lot::Mutex; +use pretty_assertions::assert_eq; use project::{search::SearchQuery, Project, ProjectPath}; use rand::{ distributions::{Alphanumeric, DistString}, @@ -32,7 +33,6 @@ use std::{ }, }; use util::ResultExt; -use pretty_assertions::assert_eq; lazy_static::lazy_static! { static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); @@ -828,7 +828,8 @@ async fn apply_client_operation( let dot_git_dir = repo_path.join(".git"); - let statuses = statuses.iter() + let statuses = statuses + .iter() .map(|(path, val)| (path.as_path(), val.clone())) .collect::>(); @@ -836,8 +837,11 @@ async fn apply_client_operation( client.fs.create_dir(&dot_git_dir).await?; } - client.fs.set_status_for_repo(&dot_git_dir, statuses.as_slice()).await; - }, + client + .fs + .set_status_for_repo(&dot_git_dir, statuses.as_slice()) + .await; + } }, } Ok(()) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 6bf0a43230..2fe31f5569 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,7 +1,7 @@ use anyhow::Result; use collections::HashMap; use parking_lot::Mutex; -use serde_derive::{Serialize, Deserialize}; +use serde_derive::{Deserialize, Serialize}; use std::{ ffi::OsStr, os::unix::prelude::OsStrExt, From 0f34af50a82e9c3c77ddc0c3509f6ebdc1ddf032 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 10 May 2023 23:37:02 -0400 Subject: [PATCH 31/97] Use path list generated during entry reload of a refresh request --- crates/project/src/worktree.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9d03169072..895eafac30 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2523,7 +2523,15 @@ impl BackgroundScanner { } async fn process_refresh_request(&self, paths: Vec, barrier: barrier::Sender) -> bool { - self.reload_entries_for_paths(paths, None).await; + if let Some(mut paths) = self.reload_entries_for_paths(paths, None).await { + paths.sort_unstable(); + util::extend_sorted( + &mut self.prev_state.lock().event_paths, + paths, + usize::MAX, + Ord::cmp, + ); + } self.send_status_update(false, Some(barrier)) } From 0ab94551f41869fd613d5de4e32b913ae398456b Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Thu, 11 May 2023 11:37:34 -0400 Subject: [PATCH 32/97] Revert "More keybindings in macOs modals with buttons" This reverts commit 1398a1206299cbaf5e14b9de30d2fbfe83f04334. --- crates/gpui/src/platform/mac/window.rs | 39 ++------------------------ 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index bcff08d005..d96f9bc4ae 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -699,31 +699,6 @@ impl platform::Window for Window { msg: &str, answers: &[&str], ) -> oneshot::Receiver { - // macOs applies overrides to modal window buttons after they are added. - // Two most important for this logic are: - // * Buttons with "Cancel" title will be displayed as the last buttons in the modal - // * Last button added to the modal via `addButtonWithTitle` stays focused - // * Focused buttons react on "space"/" " keypresses - // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus - // - // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion - // ``` - // By default, the first button has a key equivalent of Return, - // any button with a title of “Cancel” has a key equivalent of Escape, - // and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button). - // ``` - // - // To avoid situations when the last element added is "Cancel" and it gets the focus - // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button - // last, so it gets focus and a Space shortcut. - // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key. - let latest_non_cancel_label = answers - .iter() - .enumerate() - .rev() - .find(|(_, &label)| label != "Cancel") - .filter(|&(label_index, _)| label_index > 0); - unsafe { let alert: id = msg_send![class!(NSAlert), alloc]; let alert: id = msg_send![alert, init]; @@ -734,20 +709,10 @@ impl platform::Window for Window { }; let _: () = msg_send![alert, setAlertStyle: alert_style]; let _: () = msg_send![alert, setMessageText: ns_string(msg)]; - - for (ix, answer) in answers - .iter() - .enumerate() - .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix)) - { + for (ix, answer) in answers.iter().enumerate() { let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; let _: () = msg_send![button, setTag: ix as NSInteger]; } - if let Some((ix, answer)) = latest_non_cancel_label { - let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; - let _: () = msg_send![button, setTag: ix as NSInteger]; - } - let (done_tx, done_rx) = oneshot::channel(); let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |answer: NSInteger| { @@ -755,7 +720,7 @@ impl platform::Window for Window { let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap()); } }); - + let block = block.copy(); let native_window = self.0.borrow().native_window; self.0 .borrow() From 191ac86f0926d317ff77498558ecb95e98e4da9f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 09:24:36 -0700 Subject: [PATCH 33/97] Remove the CORRECT, overly agressive deletion codepath --- crates/collab/src/db.rs | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b95bb49b4e..69b561c054 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2477,6 +2477,26 @@ impl Database { .exec(&*tx) .await?; } + if !repository.removed_worktree_repo_paths.is_empty() { + worktree_repository_statuses::Entity::update_many() + .filter( + worktree_repository_statuses::Column::ProjectId + .eq(project_id) + .and(worktree_repository_statuses::Column::WorktreeId.eq(worktree_id)) + .and(worktree_repository_statuses::Column::WorkDirectoryId.eq(repository.work_directory_id as i64)) + .and( + worktree_repository_statuses::Column::RepoPath + .is_in(repository.removed_worktree_repo_paths.iter().map(String::as_str)), + ), + ) + .set(worktree_repository_statuses::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } } } @@ -2498,25 +2518,6 @@ impl Database { }) .exec(&*tx) .await?; - - // Flip all status entries associated with a given repository_entry - worktree_repository_statuses::Entity::update_many() - .filter( - worktree_repository_statuses::Column::ProjectId - .eq(project_id) - .and(worktree_repository_statuses::Column::WorktreeId.eq(worktree_id)) - .and( - worktree_repository_statuses::Column::WorkDirectoryId - .is_in(update.removed_repositories.iter().map(|id| *id as i64)), - ), - ) - .set(worktree_repository_statuses::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; } let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; From 3550110e577c25f7194090618265cec118ff3610 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 11 May 2023 09:43:13 -0700 Subject: [PATCH 34/97] ci: clear the target dir if it gets too big --- .github/workflows/ci.yml | 6 ++++++ script/clear-target-dir-if-larger-than | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100755 script/clear-target-dir-if-larger-than diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b7cb97efa..27af9e1164 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,9 @@ jobs: clean: false submodules: 'recursive' + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 70 + - name: Run check run: cargo check --workspace @@ -110,6 +113,9 @@ jobs: clean: false submodules: 'recursive' + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 70 + - name: Determine version and release channel if: ${{ startsWith(github.ref, 'refs/tags/v') }} run: | diff --git a/script/clear-target-dir-if-larger-than b/script/clear-target-dir-if-larger-than new file mode 100755 index 0000000000..59c07f77f7 --- /dev/null +++ b/script/clear-target-dir-if-larger-than @@ -0,0 +1,20 @@ +#!/bin/bash + +set -eu + +if [[ $# < 1 ]]; then + echo "usage: $0 " + exit 1 +fi + +max_size_gb=$1 + +current_size=$(du -s target | cut -f1) +current_size_gb=$(expr ${current_size} / 1024 / 1024) + +echo "target directory size: ${current_size_gb}gb. max size: ${max_size_gb}gb" + +if [[ ${current_size_gb} -gt ${max_size_gb} ]]; then + echo "clearing target directory" + rm -rf target +fi From 5accf7cf4e5c745ca4cff0bf40cc94a37aecd472 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 10:21:25 -0700 Subject: [PATCH 35/97] Update is_deleted when sending new repositories --- crates/collab/src/db.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 69b561c054..f5175a16a9 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2471,12 +2471,14 @@ impl Database { .update_columns([ worktree_repository_statuses::Column::ScanId, worktree_repository_statuses::Column::Status, + worktree_repository_statuses::Column::IsDeleted, ]) .to_owned(), ) .exec(&*tx) .await?; } + if !repository.removed_worktree_repo_paths.is_empty() { worktree_repository_statuses::Entity::update_many() .filter( From f12dffa60c75404f32870cf555d57e3f7288ed14 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 11 May 2023 20:59:10 +0300 Subject: [PATCH 36/97] Reintroduce more accesible modal keybindings Brings commit 475fc409232775d83797215870256fd5772e299f back --- crates/gpui/src/platform/mac/window.rs | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index d96f9bc4ae..50fcec52ec 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -699,6 +699,31 @@ impl platform::Window for Window { msg: &str, answers: &[&str], ) -> oneshot::Receiver { + // macOs applies overrides to modal window buttons after they are added. + // Two most important for this logic are: + // * Buttons with "Cancel" title will be displayed as the last buttons in the modal + // * Last button added to the modal via `addButtonWithTitle` stays focused + // * Focused buttons react on "space"/" " keypresses + // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus + // + // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion + // ``` + // By default, the first button has a key equivalent of Return, + // any button with a title of “Cancel” has a key equivalent of Escape, + // and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button). + // ``` + // + // To avoid situations when the last element added is "Cancel" and it gets the focus + // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button + // last, so it gets focus and a Space shortcut. + // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key. + let latest_non_cancel_label = answers + .iter() + .enumerate() + .rev() + .find(|(_, &label)| label != "Cancel") + .filter(|&(label_index, _)| label_index > 0); + unsafe { let alert: id = msg_send![class!(NSAlert), alloc]; let alert: id = msg_send![alert, init]; @@ -709,10 +734,20 @@ impl platform::Window for Window { }; let _: () = msg_send![alert, setAlertStyle: alert_style]; let _: () = msg_send![alert, setMessageText: ns_string(msg)]; - for (ix, answer) in answers.iter().enumerate() { + + for (ix, answer) in answers + .iter() + .enumerate() + .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix)) + { let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; let _: () = msg_send![button, setTag: ix as NSInteger]; } + if let Some((ix, answer)) = latest_non_cancel_label { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + } + let (done_tx, done_rx) = oneshot::channel(); let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |answer: NSInteger| { From 5b2ee63f80d8b83583713b65e8bfe2ee5f0589b2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 12:01:42 -0700 Subject: [PATCH 37/97] Added status trickle up --- Cargo.lock | 7 ++++ crates/collab/src/tests/integration_tests.rs | 2 +- crates/fs/src/repository.rs | 2 +- crates/project/src/worktree.rs | 28 ++++++++++++- crates/project_panel/src/project_panel.rs | 10 +++-- crates/sum_tree/src/tree_map.rs | 44 +++++++++++++++++++- crates/util/Cargo.toml | 1 + crates/util/src/util.rs | 2 + 8 files changed, 88 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0190b4d8f5..81e4a4e025 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6537,6 +6537,12 @@ dependencies = [ "winx", ] +[[package]] +name = "take-until" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" + [[package]] name = "target-lexicon" version = "0.12.5" @@ -7596,6 +7602,7 @@ dependencies = [ "serde", "serde_json", "smol", + "take-until", "tempdir", "url", ] diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index aefc172268..47455c0a70 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2760,7 +2760,7 @@ async fn test_git_status_sync( let worktree = worktrees[0].clone(); let snapshot = worktree.read(cx).snapshot(); let root_entry = snapshot.root_git_entry().unwrap(); - assert_eq!(root_entry.status_for(&snapshot, file), status); + assert_eq!(root_entry.status_for_file(&snapshot, file), status); } // Smoke test status reading diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 2fe31f5569..13f55b9c94 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -184,7 +184,7 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum GitFileStatus { Added, Modified, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a970067230..07302b4e2e 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -55,7 +55,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use util::{paths::HOME, ResultExt, TryFutureExt}; +use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -173,13 +173,37 @@ impl RepositoryEntry { self.work_directory.contains(snapshot, path) } - pub fn status_for(&self, snapshot: &Snapshot, path: &Path) -> Option { + pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option { self.work_directory .relativize(snapshot, path) .and_then(|repo_path| self.worktree_statuses.get(&repo_path)) .cloned() } + pub fn status_for_path(&self, snapshot: &Snapshot, path: &Path) -> Option { + self.work_directory + .relativize(snapshot, path) + .and_then(|repo_path| { + self.worktree_statuses + .get_from_while(&repo_path, |repo_path, key, _| key.starts_with(repo_path)) + .map(|(_, status)| status) + // Short circut once we've found the highest level + .take_until(|status| status == &&GitFileStatus::Conflict) + .reduce( + |status_first, status_second| match (status_first, status_second) { + (GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => { + &GitFileStatus::Conflict + } + (GitFileStatus::Added, _) | (_, GitFileStatus::Added) => { + &GitFileStatus::Added + } + _ => &GitFileStatus::Modified, + }, + ) + .copied() + }) + } + pub fn build_update(&self, other: &Self) -> proto::RepositoryEntry { let mut updated_statuses: Vec = Vec::new(); let mut removed_statuses: Vec = Vec::new(); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 49741ea49f..1066875022 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1013,9 +1013,13 @@ impl ProjectPanel { let entry_range = range.start.saturating_sub(ix)..end_ix - ix; for entry in &visible_worktree_entries[entry_range] { let path = &entry.path; - let status = snapshot - .repo_for(path) - .and_then(|entry| entry.status_for(&snapshot, path)); + let status = (entry.path.parent().is_some() && !entry.is_ignored) + .then(|| { + snapshot + .repo_for(path) + .and_then(|entry| entry.status_for_path(&snapshot, path)) + }) + .flatten(); let mut details = EntryDetails { filename: entry diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 3942d00b29..e59b05f00f 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, fmt::Debug}; +use std::{cmp::Ordering, fmt::Debug, iter}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; @@ -111,6 +111,26 @@ impl TreeMap { self.0 = new_tree; } + + pub fn get_from_while<'tree, F>(&'tree self, from: &'tree K, mut f: F) -> impl Iterator + '_ + where + F: FnMut(&K, &K, &V) -> bool + 'tree, + { + let mut cursor = self.0.cursor::>(); + let from_key = MapKeyRef(Some(from)); + cursor.seek(&from_key, Bias::Left, &()); + + iter::from_fn(move || { + let result = cursor.item().and_then(|item| { + (f(from, &item.key, &item.value)) + .then(|| (&item.key, &item.value)) + }); + cursor.next(&()); + result + }) + } + + pub fn update(&mut self, key: &K, f: F) -> Option where F: FnOnce(&mut V) -> T, @@ -354,6 +374,28 @@ mod tests { assert_eq!(map.get(&"c"), Some(&5)); } + #[test] + fn test_get_from_while() { + let mut map = TreeMap::default(); + + map.insert("a", 1); + map.insert("b", 2); + map.insert("baa", 3); + map.insert("baaab", 4); + map.insert("c", 5); + + let result = map.get_from_while(&"ba", |key, _| key.starts_with(&"ba")).collect::>(); + + assert_eq!(result.len(), 2); + assert!(result.iter().find(|(k, _)| k == &&"baa").is_some()); + assert!(result.iter().find(|(k, _)| k == &&"baaab").is_some()); + + let result = map.get_from_while(&"c", |key, _| key.starts_with(&"c")).collect::>(); + + assert_eq!(result.len(), 1); + assert!(result.iter().find(|(k, _)| k == &&"c").is_some()); + } + #[test] fn test_insert_tree() { let mut map = TreeMap::default(); diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 319d815d17..4ec8f7553c 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -26,6 +26,7 @@ serde.workspace = true serde_json.workspace = true git2 = { version = "0.15", default-features = false, optional = true } dirs = "3.0" +take-until = "0.2.0" [dev-dependencies] tempdir.workspace = true diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 903b0eec59..63b2d5f279 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -17,6 +17,8 @@ pub use backtrace::Backtrace; use futures::Future; use rand::{seq::SliceRandom, Rng}; +pub use take_until::*; + #[macro_export] macro_rules! debug_panic { ( $($fmt_arg:tt)* ) => { From dfb6a2f7fca54d22b42ded92dc9fc1fca4cbfcdc Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 12:02:25 -0700 Subject: [PATCH 38/97] fmt --- crates/collab/src/db.rs | 18 ++++++++++--- crates/sum_tree/src/tree_map.rs | 45 ++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f5175a16a9..1047b207b9 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2484,11 +2484,21 @@ impl Database { .filter( worktree_repository_statuses::Column::ProjectId .eq(project_id) - .and(worktree_repository_statuses::Column::WorktreeId.eq(worktree_id)) - .and(worktree_repository_statuses::Column::WorkDirectoryId.eq(repository.work_directory_id as i64)) .and( - worktree_repository_statuses::Column::RepoPath - .is_in(repository.removed_worktree_repo_paths.iter().map(String::as_str)), + worktree_repository_statuses::Column::WorktreeId + .eq(worktree_id), + ) + .and( + worktree_repository_statuses::Column::WorkDirectoryId + .eq(repository.work_directory_id as i64), + ) + .and( + worktree_repository_statuses::Column::RepoPath.is_in( + repository + .removed_worktree_repo_paths + .iter() + .map(String::as_str), + ), ), ) .set(worktree_repository_statuses::ActiveModel { diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index e59b05f00f..b18af3633a 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -111,25 +111,26 @@ impl TreeMap { self.0 = new_tree; } + pub fn get_from_while<'tree, F>( + &'tree self, + from: &'tree K, + mut f: F, + ) -> impl Iterator + '_ + where + F: FnMut(&K, &K, &V) -> bool + 'tree, + { + let mut cursor = self.0.cursor::>(); + let from_key = MapKeyRef(Some(from)); + cursor.seek(&from_key, Bias::Left, &()); - pub fn get_from_while<'tree, F>(&'tree self, from: &'tree K, mut f: F) -> impl Iterator + '_ - where - F: FnMut(&K, &K, &V) -> bool + 'tree, - { - let mut cursor = self.0.cursor::>(); - let from_key = MapKeyRef(Some(from)); - cursor.seek(&from_key, Bias::Left, &()); - - iter::from_fn(move || { - let result = cursor.item().and_then(|item| { - (f(from, &item.key, &item.value)) - .then(|| (&item.key, &item.value)) - }); - cursor.next(&()); - result - }) - } - + iter::from_fn(move || { + let result = cursor.item().and_then(|item| { + (f(from, &item.key, &item.value)).then(|| (&item.key, &item.value)) + }); + cursor.next(&()); + result + }) + } pub fn update(&mut self, key: &K, f: F) -> Option where @@ -384,13 +385,17 @@ mod tests { map.insert("baaab", 4); map.insert("c", 5); - let result = map.get_from_while(&"ba", |key, _| key.starts_with(&"ba")).collect::>(); + let result = map + .get_from_while(&"ba", |key, _| key.starts_with(&"ba")) + .collect::>(); assert_eq!(result.len(), 2); assert!(result.iter().find(|(k, _)| k == &&"baa").is_some()); assert!(result.iter().find(|(k, _)| k == &&"baaab").is_some()); - let result = map.get_from_while(&"c", |key, _| key.starts_with(&"c")).collect::>(); + let result = map + .get_from_while(&"c", |key, _| key.starts_with(&"c")) + .collect::>(); assert_eq!(result.len(), 1); assert!(result.iter().find(|(k, _)| k == &&"c").is_some()); From 1bb34e08bb14a48d7a06efa6490568b74ca30af6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 12:03:39 -0700 Subject: [PATCH 39/97] Fix test --- crates/sum_tree/src/tree_map.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index b18af3633a..bfc8db5bf6 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -386,7 +386,7 @@ mod tests { map.insert("c", 5); let result = map - .get_from_while(&"ba", |key, _| key.starts_with(&"ba")) + .get_from_while(&"ba", |_, key, _| key.starts_with(&"ba")) .collect::>(); assert_eq!(result.len(), 2); @@ -394,7 +394,7 @@ mod tests { assert!(result.iter().find(|(k, _)| k == &&"baaab").is_some()); let result = map - .get_from_while(&"c", |key, _| key.starts_with(&"c")) + .get_from_while(&"c", |_, key, _| key.starts_with(&"c")) .collect::>(); assert_eq!(result.len(), 1); From 6f87f9c51f0d8f3602d6e97113da1192b5712eb2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 13:25:07 -0700 Subject: [PATCH 40/97] Don't scan for statuses in files that are ignored --- crates/project/src/worktree.rs | 59 ++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 07302b4e2e..8ce087e9d9 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3065,6 +3065,15 @@ impl BackgroundScanner { entry.worktree_statuses = statuses; }); } else { + if snapshot + .entry_for_path(&path) + .map(|entry| entry.is_ignored) + .unwrap_or(false) + { + self.remove_repo_path(&path, snapshot); + return None; + } + let repo = snapshot.repo_for(&path)?; let repo_path = repo.work_directory.relativize(&snapshot, &path)?; @@ -3580,7 +3589,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { mod tests { use super::*; use fs::{FakeFs, RealFs}; - use git2::Signature; use gpui::{executor::Deterministic, TestAppContext}; use pretty_assertions::assert_eq; use rand::prelude::*; @@ -3919,6 +3927,8 @@ mod tests { #[track_caller] fn git_commit(msg: &'static str, repo: &git2::Repository) { + use git2::Signature; + let signature = Signature::now("test", "test@zed.dev").unwrap(); let oid = repo.index().unwrap().write_tree().unwrap(); let tree = repo.find_tree(oid).unwrap(); @@ -3944,7 +3954,9 @@ mod tests { #[track_caller] fn git_stash(repo: &mut git2::Repository) { - let signature = repo.signature().unwrap(); + use git2::Signature; + + let signature = Signature::now("test", "test@zed.dev").unwrap(); repo.stash_save(&signature, "N/A", None) .expect("Failed to stash"); } @@ -3976,6 +3988,8 @@ mod tests { .collect() } + const IGNORE_RULE: &'static str = "**/target"; + let root = temp_tree(json!({ "project": { "a.txt": "a", @@ -3984,7 +3998,12 @@ mod tests { "d": { "e.txt": "eee" } - } + }, + "f.txt": "ffff", + "target": { + "build_file": "???" + }, + ".gitignore": IGNORE_RULE }, })); @@ -4008,12 +4027,16 @@ mod tests { const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; const E_TXT: &'static str = "c/d/e.txt"; + const F_TXT: &'static str = "f.txt"; + const DOTGITIGNORE: &'static str = ".gitignore"; + const BUILD_FILE: &'static str = "target/build_file"; let work_dir = root.path().join("project"); - let mut repo = git_init(work_dir.as_path()); + repo.add_ignore_rule(IGNORE_RULE).unwrap(); git_add(Path::new(A_TXT), &repo); git_add(Path::new(E_TXT), &repo); + git_add(Path::new(DOTGITIGNORE), &repo); git_commit("Initial commit", &repo); std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); @@ -4027,7 +4050,7 @@ mod tests { let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(dir.0.as_ref(), Path::new("project")); - assert_eq!(repo.worktree_statuses.iter().count(), 2); + assert_eq!(repo.worktree_statuses.iter().count(), 3); assert_eq!( repo.worktree_statuses.get(&Path::new(A_TXT).into()), Some(&GitFileStatus::Modified) @@ -4036,6 +4059,10 @@ mod tests { repo.worktree_statuses.get(&Path::new(B_TXT).into()), Some(&GitFileStatus::Added) ); + assert_eq!( + repo.worktree_statuses.get(&Path::new(F_TXT).into()), + Some(&GitFileStatus::Added) + ); }); git_add(Path::new(A_TXT), &repo); @@ -4048,15 +4075,20 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.worktree_statuses.iter().count(), 0); + assert_eq!(repo.worktree_statuses.iter().count(), 1); assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!( + repo.worktree_statuses.get(&Path::new(F_TXT).into()), + Some(&GitFileStatus::Added) + ); }); git_reset(0, &repo); git_remove_index(Path::new(B_TXT), &repo); git_stash(&mut repo); std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); + std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap(); tree.flush_fs_events(cx).await; // Check that more complex repo changes are tracked @@ -4064,7 +4096,7 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.worktree_statuses.iter().count(), 2); + assert_eq!(repo.worktree_statuses.iter().count(), 3); assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); assert_eq!( repo.worktree_statuses.get(&Path::new(B_TXT).into()), @@ -4074,22 +4106,35 @@ mod tests { repo.worktree_statuses.get(&Path::new(E_TXT).into()), Some(&GitFileStatus::Modified) ); + assert_eq!( + repo.worktree_statuses.get(&Path::new(F_TXT).into()), + Some(&GitFileStatus::Added) + ); }); std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); std::fs::remove_dir_all(work_dir.join("c")).unwrap(); + std::fs::write(work_dir.join(DOTGITIGNORE), [IGNORE_RULE, "f.txt"].join("\n")).unwrap(); + + git_add(Path::new(DOTGITIGNORE), &repo); + git_commit("Committing modified git ignore", &repo); tree.flush_fs_events(cx).await; + dbg!(git_status(&repo)); + // Check that non-repo behavior is tracked tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + dbg!(&repo.worktree_statuses); + assert_eq!(repo.worktree_statuses.iter().count(), 0); assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); assert_eq!(repo.worktree_statuses.get(&Path::new(E_TXT).into()), None); + assert_eq!(repo.worktree_statuses.get(&Path::new(F_TXT).into()), None); }); } From 72655fc41d154684b674f2d4cba9ff24c32641d6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 13:25:57 -0700 Subject: [PATCH 41/97] fmt --- crates/project/src/worktree.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8ce087e9d9..2112bd92b1 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4114,7 +4114,11 @@ mod tests { std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); std::fs::remove_dir_all(work_dir.join("c")).unwrap(); - std::fs::write(work_dir.join(DOTGITIGNORE), [IGNORE_RULE, "f.txt"].join("\n")).unwrap(); + std::fs::write( + work_dir.join(DOTGITIGNORE), + [IGNORE_RULE, "f.txt"].join("\n"), + ) + .unwrap(); git_add(Path::new(DOTGITIGNORE), &repo); git_commit("Committing modified git ignore", &repo); From d538994c7f03618cfc1df60c54c01a7b166e8a1b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 16:06:25 -0700 Subject: [PATCH 42/97] Use more efficient sum tree traversals for removal and improve ergonomics with iter_from co-authored-by: Nathan --- crates/project/src/worktree.rs | 5 +- crates/sum_tree/src/tree_map.rs | 120 +++++++++++++++++++++----------- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2112bd92b1..305bcbbf16 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -185,7 +185,8 @@ impl RepositoryEntry { .relativize(snapshot, path) .and_then(|repo_path| { self.worktree_statuses - .get_from_while(&repo_path, |repo_path, key, _| key.starts_with(repo_path)) + .iter_from(&repo_path) + .take_while(|(key, _)| key.starts_with(&repo_path)) .map(|(_, status)| status) // Short circut once we've found the highest level .take_until(|status| status == &&GitFileStatus::Conflict) @@ -3022,7 +3023,7 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { entry .worktree_statuses - .remove_from_while(&repo_path, |stored_path, _| { + .remove_by(&repo_path, |stored_path, _| { stored_path.starts_with(&repo_path) }) }); diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index bfc8db5bf6..fdafdaeb3a 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, fmt::Debug, iter}; +use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; @@ -93,43 +93,14 @@ impl TreeMap { self.0 = new_tree; } - pub fn remove_from_while(&mut self, from: &K, mut f: F) - where - F: FnMut(&K, &V) -> bool, - { - let mut cursor = self.0.cursor::>(); - let from_key = MapKeyRef(Some(from)); - let mut new_tree = cursor.slice(&from_key, Bias::Left, &()); - while let Some(item) = cursor.item() { - if !f(&item.key, &item.value) { - break; - } - cursor.next(&()); - } - new_tree.push_tree(cursor.suffix(&()), &()); - drop(cursor); - self.0 = new_tree; - } - - pub fn get_from_while<'tree, F>( - &'tree self, - from: &'tree K, - mut f: F, - ) -> impl Iterator + '_ - where - F: FnMut(&K, &K, &V) -> bool + 'tree, - { + pub fn iter_from<'a>(&'a self, from: &'a K) -> impl Iterator + '_ { let mut cursor = self.0.cursor::>(); let from_key = MapKeyRef(Some(from)); cursor.seek(&from_key, Bias::Left, &()); - iter::from_fn(move || { - let result = cursor.item().and_then(|item| { - (f(from, &item.key, &item.value)).then(|| (&item.key, &item.value)) - }); - cursor.next(&()); - result - }) + cursor + .into_iter() + .map(|map_entry| (&map_entry.key, &map_entry.value)) } pub fn update(&mut self, key: &K, f: F) -> Option @@ -189,6 +160,51 @@ impl TreeMap { self.0.edit(edits, &()); } + + pub fn remove_by(&mut self, key: &K, f: F) + where + F: Fn(&K) -> bool, + { + let mut cursor = self.0.cursor::>(); + let key = MapKeyRef(Some(key)); + let mut new_tree = cursor.slice(&key, Bias::Left, &()); + let until = RemoveByTarget(key, &f); + cursor.seek_forward(&until, Bias::Right, &()); + new_tree.push_tree(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + } +} + +struct RemoveByTarget<'a, K>(MapKeyRef<'a, K>, &'a dyn Fn(&K) -> bool); + +impl<'a, K: Debug> Debug for RemoveByTarget<'a, K> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoveByTarget") + .field("key", &self.0) + .field("F", &"<...>") + .finish() + } +} + +impl<'a, K: Debug + Clone + Default + Ord> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> + for RemoveByTarget<'_, K> +{ + fn cmp( + &self, + cursor_location: &MapKeyRef<'a, K>, + _cx: & as Summary>::Context, + ) -> Ordering { + if let Some(cursor_location) = cursor_location.0 { + if (self.1)(cursor_location) { + Ordering::Equal + } else { + self.0 .0.unwrap().cmp(cursor_location) + } + } else { + Ordering::Greater + } + } } impl Default for TreeMap @@ -357,26 +373,50 @@ mod tests { } #[test] - fn test_remove_from_while() { + fn test_remove_by() { let mut map = TreeMap::default(); map.insert("a", 1); + map.insert("aa", 1); map.insert("b", 2); map.insert("baa", 3); map.insert("baaab", 4); map.insert("c", 5); + map.insert("ca", 6); - map.remove_from_while(&"ba", |key, _| key.starts_with(&"ba")); + map.remove_by(&"ba", |key| key.starts_with("ba")); assert_eq!(map.get(&"a"), Some(&1)); + assert_eq!(map.get(&"aa"), Some(&1)); assert_eq!(map.get(&"b"), Some(&2)); assert_eq!(map.get(&"baaa"), None); assert_eq!(map.get(&"baaaab"), None); assert_eq!(map.get(&"c"), Some(&5)); + assert_eq!(map.get(&"ca"), Some(&6)); + + + map.remove_by(&"c", |key| key.starts_with("c")); + + assert_eq!(map.get(&"a"), Some(&1)); + assert_eq!(map.get(&"aa"), Some(&1)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"c"), None); + assert_eq!(map.get(&"ca"), None); + + map.remove_by(&"a", |key| key.starts_with("a")); + + assert_eq!(map.get(&"a"), None); + assert_eq!(map.get(&"aa"), None); + assert_eq!(map.get(&"b"), Some(&2)); + + map.remove_by(&"b", |key| key.starts_with("b")); + + assert_eq!(map.get(&"b"), None); + } #[test] - fn test_get_from_while() { + fn test_iter_from() { let mut map = TreeMap::default(); map.insert("a", 1); @@ -386,7 +426,8 @@ mod tests { map.insert("c", 5); let result = map - .get_from_while(&"ba", |_, key, _| key.starts_with(&"ba")) + .iter_from(&"ba") + .take_while(|(key, _)| key.starts_with(&"ba")) .collect::>(); assert_eq!(result.len(), 2); @@ -394,7 +435,8 @@ mod tests { assert!(result.iter().find(|(k, _)| k == &&"baaab").is_some()); let result = map - .get_from_while(&"c", |_, key, _| key.starts_with(&"c")) + .iter_from(&"c") + .take_while(|(key, _)| key.starts_with(&"c")) .collect::>(); assert_eq!(result.len(), 1); From d526fa6f1fb8d84958ee345178880fe649150113 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 16:06:56 -0700 Subject: [PATCH 43/97] fmt --- crates/sum_tree/src/tree_map.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index fdafdaeb3a..509a79ec47 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -394,7 +394,6 @@ mod tests { assert_eq!(map.get(&"c"), Some(&5)); assert_eq!(map.get(&"ca"), Some(&6)); - map.remove_by(&"c", |key| key.starts_with("c")); assert_eq!(map.get(&"a"), Some(&1)); @@ -412,7 +411,6 @@ mod tests { map.remove_by(&"b", |key| key.starts_with("b")); assert_eq!(map.get(&"b"), None); - } #[test] From 5fe8b73f0491ec4f62a2b3204484918a10f5aa53 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 11 May 2023 16:07:41 -0700 Subject: [PATCH 44/97] =?UTF-8?q?compile=20error=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/project/src/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 305bcbbf16..fcda45fd6f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3023,7 +3023,7 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { entry .worktree_statuses - .remove_by(&repo_path, |stored_path, _| { + .remove_by(&repo_path, |stored_path| { stored_path.starts_with(&repo_path) }) }); From ee3637216eb44bd146d7426d22e5e0d8ad149a51 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 11 May 2023 23:20:20 -0600 Subject: [PATCH 45/97] Add TreeMap::remove_between that can take abstract start and end points This commit introduces a new adaptor trait for SeekTarget that works around frustrating issues with lifetimes. It wraps the arguments in a newtype wrapper that lives on the stack to avoid the lifetime getting extended to the caller of the method. This allows us to introduce a PathSuccessor object that can be passed as the end argument of remove_between to remove a whole subtree. --- crates/project/src/worktree.rs | 6 +- crates/sum_tree/src/sum_tree.rs | 4 +- crates/sum_tree/src/tree_map.rs | 198 +++++++++++++++----------------- 3 files changed, 94 insertions(+), 114 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index fcda45fd6f..249559de88 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -54,7 +54,7 @@ use std::{ }, time::{Duration, SystemTime}, }; -use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; +use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet, PathDescendants}; use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] @@ -3023,9 +3023,7 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { entry .worktree_statuses - .remove_by(&repo_path, |stored_path| { - stored_path.starts_with(&repo_path) - }) + .remove_range(&repo_path, &PathDescendants(&repo_path)) }); } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 3e916ccd1b..6b6eacda59 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -5,7 +5,7 @@ use arrayvec::ArrayVec; pub use cursor::{Cursor, FilterCursor, Iter}; use std::marker::PhantomData; use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc}; -pub use tree_map::{TreeMap, TreeSet}; +pub use tree_map::{TreeMap, TreeSet, PathDescendants}; #[cfg(test)] const TREE_BASE: usize = 2; @@ -47,7 +47,7 @@ impl<'a, T: Summary> Dimension<'a, T> for T { } pub trait SeekTarget<'a, S: Summary, D: Dimension<'a, S>>: fmt::Debug { - fn cmp(&self, cursor_location: &D, cx: &S::Context) -> Ordering; + fn cmp(&self, cursor_location: &D, cx: &S::Context) -> Ordering; } impl<'a, S: Summary, D: Dimension<'a, S> + Ord> SeekTarget<'a, S, D> for D { diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 509a79ec47..3d49c48998 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -1,4 +1,8 @@ -use std::{cmp::Ordering, fmt::Debug}; +use std::{ + cmp::Ordering, + fmt::Debug, + path::{Path, PathBuf}, +}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; @@ -73,6 +77,17 @@ impl TreeMap { removed } + pub fn remove_range(&mut self, start: &impl MapSeekTarget, end: &impl MapSeekTarget) { + let start = MapSeekTargetAdaptor(start); + let end = MapSeekTargetAdaptor(end); + let mut cursor = self.0.cursor::>(); + let mut new_tree = cursor.slice(&start, Bias::Left, &()); + cursor.seek(&end, Bias::Left, &()); + new_tree.push_tree(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + } + /// Returns the key-value pair with the greatest key less than or equal to the given key. pub fn closest(&self, key: &K) -> Option<(&K, &V)> { let mut cursor = self.0.cursor::>(); @@ -82,17 +97,6 @@ impl TreeMap { cursor.item().map(|item| (&item.key, &item.value)) } - pub fn remove_between(&mut self, from: &K, until: &K) { - let mut cursor = self.0.cursor::>(); - let from_key = MapKeyRef(Some(from)); - let mut new_tree = cursor.slice(&from_key, Bias::Left, &()); - let until_key = MapKeyRef(Some(until)); - cursor.seek_forward(&until_key, Bias::Left, &()); - new_tree.push_tree(cursor.suffix(&()), &()); - drop(cursor); - self.0 = new_tree; - } - pub fn iter_from<'a>(&'a self, from: &'a K) -> impl Iterator + '_ { let mut cursor = self.0.cursor::>(); let from_key = MapKeyRef(Some(from)); @@ -160,46 +164,43 @@ impl TreeMap { self.0.edit(edits, &()); } - - pub fn remove_by(&mut self, key: &K, f: F) - where - F: Fn(&K) -> bool, - { - let mut cursor = self.0.cursor::>(); - let key = MapKeyRef(Some(key)); - let mut new_tree = cursor.slice(&key, Bias::Left, &()); - let until = RemoveByTarget(key, &f); - cursor.seek_forward(&until, Bias::Right, &()); - new_tree.push_tree(cursor.suffix(&()), &()); - drop(cursor); - self.0 = new_tree; - } } -struct RemoveByTarget<'a, K>(MapKeyRef<'a, K>, &'a dyn Fn(&K) -> bool); +#[derive(Debug)] +struct MapSeekTargetAdaptor<'a, T>(&'a T); -impl<'a, K: Debug> Debug for RemoveByTarget<'a, K> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RemoveByTarget") - .field("key", &self.0) - .field("F", &"<...>") - .finish() - } -} - -impl<'a, K: Debug + Clone + Default + Ord> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> - for RemoveByTarget<'_, K> +impl<'a, K: Debug + Clone + Default + Ord, T: MapSeekTarget> + SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapSeekTargetAdaptor<'_, T> { - fn cmp( - &self, - cursor_location: &MapKeyRef<'a, K>, - _cx: & as Summary>::Context, - ) -> Ordering { - if let Some(cursor_location) = cursor_location.0 { - if (self.1)(cursor_location) { - Ordering::Equal + fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { + MapSeekTarget::cmp(self.0, cursor_location) + } +} + +pub trait MapSeekTarget: Debug { + fn cmp(&self, cursor_location: &MapKeyRef) -> Ordering; +} + +impl MapSeekTarget for K { + fn cmp(&self, cursor_location: &MapKeyRef) -> Ordering { + if let Some(key) = &cursor_location.0 { + self.cmp(key) + } else { + Ordering::Greater + } + } +} + +#[derive(Debug)] +pub struct PathDescendants<'a>(&'a Path); + +impl MapSeekTarget for PathDescendants<'_> { + fn cmp(&self, cursor_location: &MapKeyRef) -> Ordering { + if let Some(key) = &cursor_location.0 { + if key.starts_with(&self.0) { + Ordering::Greater } else { - self.0 .0.unwrap().cmp(cursor_location) + self.0.cmp(key) } } else { Ordering::Greater @@ -266,7 +267,7 @@ where K: Clone + Debug + Default + Ord, { fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { - self.0.cmp(&cursor_location.0) + Ord::cmp(&self.0, &cursor_location.0) } } @@ -353,66 +354,6 @@ mod tests { assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); } - #[test] - fn test_remove_between() { - let mut map = TreeMap::default(); - - map.insert("a", 1); - map.insert("b", 2); - map.insert("baa", 3); - map.insert("baaab", 4); - map.insert("c", 5); - - map.remove_between(&"ba", &"bb"); - - assert_eq!(map.get(&"a"), Some(&1)); - assert_eq!(map.get(&"b"), Some(&2)); - assert_eq!(map.get(&"baaa"), None); - assert_eq!(map.get(&"baaaab"), None); - assert_eq!(map.get(&"c"), Some(&5)); - } - - #[test] - fn test_remove_by() { - let mut map = TreeMap::default(); - - map.insert("a", 1); - map.insert("aa", 1); - map.insert("b", 2); - map.insert("baa", 3); - map.insert("baaab", 4); - map.insert("c", 5); - map.insert("ca", 6); - - map.remove_by(&"ba", |key| key.starts_with("ba")); - - assert_eq!(map.get(&"a"), Some(&1)); - assert_eq!(map.get(&"aa"), Some(&1)); - assert_eq!(map.get(&"b"), Some(&2)); - assert_eq!(map.get(&"baaa"), None); - assert_eq!(map.get(&"baaaab"), None); - assert_eq!(map.get(&"c"), Some(&5)); - assert_eq!(map.get(&"ca"), Some(&6)); - - map.remove_by(&"c", |key| key.starts_with("c")); - - assert_eq!(map.get(&"a"), Some(&1)); - assert_eq!(map.get(&"aa"), Some(&1)); - assert_eq!(map.get(&"b"), Some(&2)); - assert_eq!(map.get(&"c"), None); - assert_eq!(map.get(&"ca"), None); - - map.remove_by(&"a", |key| key.starts_with("a")); - - assert_eq!(map.get(&"a"), None); - assert_eq!(map.get(&"aa"), None); - assert_eq!(map.get(&"b"), Some(&2)); - - map.remove_by(&"b", |key| key.starts_with("b")); - - assert_eq!(map.get(&"b"), None); - } - #[test] fn test_iter_from() { let mut map = TreeMap::default(); @@ -461,4 +402,45 @@ mod tests { assert_eq!(map.get(&"c"), Some(&3)); assert_eq!(map.get(&"d"), Some(&4)); } + + #[test] + fn test_remove_between_and_path_successor() { + let mut map = TreeMap::default(); + + map.insert(PathBuf::from("a"), 1); + map.insert(PathBuf::from("a/a"), 1); + map.insert(PathBuf::from("b"), 2); + map.insert(PathBuf::from("b/a/a"), 3); + map.insert(PathBuf::from("b/a/a/a/b"), 4); + map.insert(PathBuf::from("c"), 5); + map.insert(PathBuf::from("c/a"), 6); + + map.remove_range(&PathBuf::from("b/a"), &PathDescendants(&PathBuf::from("b/a"))); + + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("b/a/a")), None); + assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None); + assert_eq!(map.get(&PathBuf::from("c")), Some(&5)); + assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6)); + + map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c"))); + + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("c")), None); + assert_eq!(map.get(&PathBuf::from("c/a")), None); + + map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a"))); + + assert_eq!(map.get(&PathBuf::from("a")), None); + assert_eq!(map.get(&PathBuf::from("a/a")), None); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + + map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b"))); + + assert_eq!(map.get(&PathBuf::from("b")), None); + } } From 6ef0f70528959d0a04b8baff78b74c22752241c1 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 12 May 2023 08:37:07 -0700 Subject: [PATCH 46/97] Made the map seek target a publicly implementable interface Integrated remove_range with the existing git code co-authored-by: Nathan --- crates/fs/src/repository.rs | 16 ++++++++- crates/project/src/worktree.rs | 6 ++-- crates/sum_tree/src/sum_tree.rs | 4 +-- crates/sum_tree/src/tree_map.rs | 59 ++++++++++++++++----------------- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 13f55b9c94..51b69b8bc7 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -3,12 +3,13 @@ use collections::HashMap; use parking_lot::Mutex; use serde_derive::{Deserialize, Serialize}; use std::{ + cmp::Ordering, ffi::OsStr, os::unix::prelude::OsStrExt, path::{Component, Path, PathBuf}, sync::Arc, }; -use sum_tree::TreeMap; +use sum_tree::{MapSeekTarget, TreeMap}; use util::ResultExt; pub use git2::Repository as LibGitRepository; @@ -233,3 +234,16 @@ impl std::ops::Deref for RepoPath { &self.0 } } + +#[derive(Debug)] +pub struct RepoPathDescendants<'a>(pub &'a Path); + +impl<'a> MapSeekTarget for RepoPathDescendants<'a> { + fn cmp_cursor(&self, key: &RepoPath) -> Ordering { + if key.starts_with(&self.0) { + Ordering::Greater + } else { + self.0.cmp(key) + } + } +} diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 249559de88..e713eed58d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -7,7 +7,7 @@ use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; use fs::{ - repository::{GitFileStatus, GitRepository, RepoPath}, + repository::{GitFileStatus, GitRepository, RepoPath, RepoPathDescendants}, Fs, LineEnding, }; use futures::{ @@ -54,7 +54,7 @@ use std::{ }, time::{Duration, SystemTime}, }; -use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet, PathDescendants}; +use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] @@ -3023,7 +3023,7 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { entry .worktree_statuses - .remove_range(&repo_path, &PathDescendants(&repo_path)) + .remove_range(&repo_path, &RepoPathDescendants(&repo_path)) }); } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 6b6eacda59..36f0f926cd 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -5,7 +5,7 @@ use arrayvec::ArrayVec; pub use cursor::{Cursor, FilterCursor, Iter}; use std::marker::PhantomData; use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc}; -pub use tree_map::{TreeMap, TreeSet, PathDescendants}; +pub use tree_map::{MapSeekTarget, TreeMap, TreeSet}; #[cfg(test)] const TREE_BASE: usize = 2; @@ -47,7 +47,7 @@ impl<'a, T: Summary> Dimension<'a, T> for T { } pub trait SeekTarget<'a, S: Summary, D: Dimension<'a, S>>: fmt::Debug { - fn cmp(&self, cursor_location: &D, cx: &S::Context) -> Ordering; + fn cmp(&self, cursor_location: &D, cx: &S::Context) -> Ordering; } impl<'a, S: Summary, D: Dimension<'a, S> + Ord> SeekTarget<'a, S, D> for D { diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 3d49c48998..ea69fb0dca 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -1,8 +1,4 @@ -use std::{ - cmp::Ordering, - fmt::Debug, - path::{Path, PathBuf}, -}; +use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; @@ -173,38 +169,21 @@ impl<'a, K: Debug + Clone + Default + Ord, T: MapSeekTarget> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapSeekTargetAdaptor<'_, T> { fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { - MapSeekTarget::cmp(self.0, cursor_location) + if let Some(key) = &cursor_location.0 { + MapSeekTarget::cmp_cursor(self.0, key) + } else { + Ordering::Greater + } } } pub trait MapSeekTarget: Debug { - fn cmp(&self, cursor_location: &MapKeyRef) -> Ordering; + fn cmp_cursor(&self, cursor_location: &K) -> Ordering; } impl MapSeekTarget for K { - fn cmp(&self, cursor_location: &MapKeyRef) -> Ordering { - if let Some(key) = &cursor_location.0 { - self.cmp(key) - } else { - Ordering::Greater - } - } -} - -#[derive(Debug)] -pub struct PathDescendants<'a>(&'a Path); - -impl MapSeekTarget for PathDescendants<'_> { - fn cmp(&self, cursor_location: &MapKeyRef) -> Ordering { - if let Some(key) = &cursor_location.0 { - if key.starts_with(&self.0) { - Ordering::Greater - } else { - self.0.cmp(key) - } - } else { - Ordering::Greater - } + fn cmp_cursor(&self, cursor_location: &K) -> Ordering { + self.cmp(cursor_location) } } @@ -405,6 +384,21 @@ mod tests { #[test] fn test_remove_between_and_path_successor() { + use std::path::{Path, PathBuf}; + + #[derive(Debug)] + pub struct PathDescendants<'a>(&'a Path); + + impl MapSeekTarget for PathDescendants<'_> { + fn cmp_cursor(&self, key: &PathBuf) -> Ordering { + if key.starts_with(&self.0) { + Ordering::Greater + } else { + self.0.cmp(key) + } + } + } + let mut map = TreeMap::default(); map.insert(PathBuf::from("a"), 1); @@ -415,7 +409,10 @@ mod tests { map.insert(PathBuf::from("c"), 5); map.insert(PathBuf::from("c/a"), 6); - map.remove_range(&PathBuf::from("b/a"), &PathDescendants(&PathBuf::from("b/a"))); + map.remove_range( + &PathBuf::from("b/a"), + &PathDescendants(&PathBuf::from("b/a")), + ); assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); From 60320c6b095f9de717773c15d581e16495c816ea Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 12 May 2023 09:37:02 -0700 Subject: [PATCH 47/97] Send the root branch along with it's entry --- crates/rpc/src/proto.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 32f40ad7db..d74ed5e46c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -487,17 +487,37 @@ pub fn split_worktree_update( let mut done_files = false; let mut done_statuses = false; let mut repository_index = 0; + let mut root_repo_found = false; iter::from_fn(move || { if done_files && done_statuses { return None; } let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); - let updated_entries = message + let updated_entries: Vec<_> = message .updated_entries .drain(..updated_entries_chunk_size) .collect(); + let mut updated_repositories: Vec<_> = Default::default(); + + if !root_repo_found { + for entry in updated_entries.iter() { + if let Some(repo) = message.updated_repositories.get(0) { + if repo.work_directory_id == entry.id { + root_repo_found = true; + updated_repositories.push(RepositoryEntry { + work_directory_id: repo.work_directory_id, + branch: repo.branch.clone(), + removed_worktree_repo_paths: Default::default(), + updated_worktree_statuses: Default::default(), + }); + break; + } + } + } + } + let removed_entries_chunk_size = cmp::min(message.removed_entries.len(), max_chunk_size); let removed_entries = message .removed_entries @@ -508,9 +528,8 @@ pub fn split_worktree_update( // Wait to send repositories until after we've guaranteed that their associated entries // will be read - let updated_repositories = if done_files { + if done_files { let mut total_statuses = 0; - let mut updated_repositories = Vec::new(); while total_statuses < max_chunk_size && repository_index < message.updated_repositories.len() { @@ -555,8 +574,6 @@ pub fn split_worktree_update( repository_index += 1; } } - - updated_repositories } else { Default::default() }; From e71846c653ea797f03aea6db6d93052625fefa81 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 12 May 2023 10:12:47 -0700 Subject: [PATCH 48/97] Create pull_request_template.md --- .github/workflows/pull_request_template.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/workflows/pull_request_template.md diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md new file mode 100644 index 0000000000..cd15ab5114 --- /dev/null +++ b/.github/workflows/pull_request_template.md @@ -0,0 +1,4 @@ +[[PR Description here]] + +Release Notes: +* [[Added support for foo / Fixed bar / No notes]] From 4663ac8abff7a8e7ab7d89b305b164797bc4550a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 12 May 2023 10:14:54 -0700 Subject: [PATCH 49/97] Create pull_request_template.md --- .github/pull_request_template.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..8d16a59bc1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ +[[PR Description]] + +Release Notes: + +* [[Added foo / Fixed bar / No notes]] From ad7ed56e6bb99cd8bcce9ee7c4ff5ab3092aafa0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 12 May 2023 10:15:13 -0700 Subject: [PATCH 50/97] Delete pull_request_template.md --- .github/workflows/pull_request_template.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/workflows/pull_request_template.md diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md deleted file mode 100644 index cd15ab5114..0000000000 --- a/.github/workflows/pull_request_template.md +++ /dev/null @@ -1,4 +0,0 @@ -[[PR Description here]] - -Release Notes: -* [[Added support for foo / Fixed bar / No notes]] From b70c874a0e695923b81fb176ce7e49b9130d5ebc Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Fri, 12 May 2023 14:04:36 -0400 Subject: [PATCH 51/97] Update release links --- .github/workflows/release_actions.yml | 2 +- crates/auto_update/src/auto_update.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 4a9d777769..5feb29e469 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -14,7 +14,7 @@ jobs: content: | 📣 Zed ${{ github.event.release.tag_name }} was just released! - Restart your Zed or head to https://zed.dev/releases/latest to grab it. + Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it. ```md # Changelog diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 68d3776e1c..89b70acec7 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -102,7 +102,7 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { { format!("{server_url}/releases/preview/latest") } else { - format!("{server_url}/releases/latest") + format!("{server_url}/releases/stable/latest") }; cx.platform().open_url(&latest_release_url); } From 41bef2e444b96b012b5ebf7c156562921caf3702 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 13 May 2023 02:26:45 -0700 Subject: [PATCH 52/97] Refactor out git status into FileName component Integrate file name component into the editor's tab content --- Cargo.lock | 1 + crates/editor/src/items.rs | 23 ++++++++- crates/gpui/src/elements.rs | 9 ++++ crates/project_panel/src/project_panel.rs | 33 +++++-------- crates/theme/Cargo.toml | 1 + crates/theme/src/ui.rs | 57 +++++++++++++++++++++-- 6 files changed, 98 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81e4a4e025..e009cfd342 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6688,6 +6688,7 @@ name = "theme" version = "0.1.0" dependencies = [ "anyhow", + "fs", "gpui", "indexmap", "parking_lot 0.11.2", diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index e971af943a..80c1009aa4 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -14,7 +14,7 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, SelectionGoal, }; -use project::{FormatTrigger, Item as _, Project, ProjectPath}; +use project::{repository::GitFileStatus, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view}; use settings::Settings; use smallvec::SmallVec; @@ -27,6 +27,7 @@ use std::{ path::{Path, PathBuf}, }; use text::Selection; +use theme::ui::FileName; use util::{ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ @@ -565,8 +566,25 @@ impl Item for Editor { style: &theme::Tab, cx: &AppContext, ) -> AnyElement { + fn git_file_status(this: &Editor, cx: &AppContext) -> Option { + let project_entry_id = this + .buffer() + .read(cx) + .as_singleton()? + .read(cx) + .entry_id(cx)?; + let project = this.project.as_ref()?.read(cx); + let path = project.path_for_entry(project_entry_id, cx)?.path; + let worktree = project.worktree_for_entry(project_entry_id, cx)?.read(cx); + worktree.repo_for(&path)?.status_for_path(&worktree, &path) + } + Flex::row() - .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).aligned()) + .with_child(ComponentHost::new(FileName::new( + self.title(cx).to_string(), + git_file_status(self, cx), + FileName::style(style.label.clone(), &cx.global::().theme), + ))) .with_children(detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); @@ -580,6 +598,7 @@ impl Item for Editor { .aligned(), ) })) + .align_children_center() .into_any() } diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index e2c4af143c..27b01a8db2 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -578,6 +578,15 @@ pub struct ComponentHost> { view_type: PhantomData, } +impl> ComponentHost { + pub fn new(c: C) -> Self { + Self { + component: c, + view_type: PhantomData, + } + } +} + impl> Deref for ComponentHost { type Target = C; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 1066875022..bb7f97fbf8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -6,7 +6,7 @@ use gpui::{ actions, anyhow::{anyhow, Result}, elements::{ - AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler, + AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, }, geometry::vector::Vector2F, @@ -29,7 +29,7 @@ use std::{ path::Path, sync::Arc, }; -use theme::ProjectPanelEntry; +use theme::{ui::FileName, ProjectPanelEntry}; use unicase::UniCase; use workspace::Workspace; @@ -1083,19 +1083,6 @@ impl ProjectPanel { let kind = details.kind; let show_editor = details.is_editing && !details.is_processing; - // Prepare colors for git statuses - let editor_theme = &cx.global::().theme.editor; - let mut filename_text_style = style.text.clone(); - filename_text_style.color = details - .git_status - .as_ref() - .map(|status| match status { - GitFileStatus::Added => editor_theme.diff.inserted, - GitFileStatus::Modified => editor_theme.diff.modified, - GitFileStatus::Conflict => editor_theme.diff.deleted, - }) - .unwrap_or(style.text.color); - Flex::row() .with_child( if kind == EntryKind::Dir { @@ -1123,12 +1110,16 @@ impl ProjectPanel { .flex(1.0, true) .into_any() } else { - Label::new(details.filename.clone(), filename_text_style) - .contained() - .with_margin_left(style.icon_spacing) - .aligned() - .left() - .into_any() + ComponentHost::new(FileName::new( + details.filename.clone(), + details.git_status, + FileName::style(style.text.clone(), &cx.global::().theme), + )) + .contained() + .with_margin_left(style.icon_spacing) + .aligned() + .left() + .into_any() }) .constrained() .with_height(style.height) diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 67a28397e2..c7dc2938ed 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] gpui = { path = "../gpui" } +fs = { path = "../fs" } anyhow.workspace = true indexmap = "1.6.2" parking_lot.workspace = true diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index b86bfca8c4..e4df24c89f 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -1,9 +1,10 @@ use std::borrow::Cow; +use fs::repository::GitFileStatus; use gpui::{ color::Color, elements::{ - ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, + ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, LabelStyle, MouseEventHandler, ParentElement, Stack, Svg, }, fonts::TextStyle, @@ -11,11 +12,11 @@ use gpui::{ platform, platform::MouseButton, scene::MouseClick, - Action, Element, EventContext, MouseState, View, ViewContext, + Action, AnyElement, Element, EventContext, MouseState, View, ViewContext, }; use serde::Deserialize; -use crate::{ContainedText, Interactive}; +use crate::{ContainedText, Interactive, Theme}; #[derive(Clone, Deserialize, Default)] pub struct CheckboxStyle { @@ -252,3 +253,53 @@ where .constrained() .with_height(style.dimensions().y()) } + +pub struct FileName { + filename: String, + git_status: Option, + style: FileNameStyle, +} + +pub struct FileNameStyle { + template_style: LabelStyle, + git_inserted: Color, + git_modified: Color, + git_deleted: Color, +} + +impl FileName { + pub fn new(filename: String, git_status: Option, style: FileNameStyle) -> Self { + FileName { + filename, + git_status, + style, + } + } + + pub fn style>(style: I, theme: &Theme) -> FileNameStyle { + FileNameStyle { + template_style: style.into(), + git_inserted: theme.editor.diff.inserted, + git_modified: theme.editor.diff.modified, + git_deleted: theme.editor.diff.deleted, + } + } +} + +impl gpui::elements::Component for FileName { + fn render(&self, _: &mut V, _: &mut ViewContext) -> AnyElement { + // Prepare colors for git statuses + let mut filename_text_style = self.style.template_style.text.clone(); + filename_text_style.color = self + .git_status + .as_ref() + .map(|status| match status { + GitFileStatus::Added => self.style.git_inserted, + GitFileStatus::Modified => self.style.git_modified, + GitFileStatus::Conflict => self.style.git_deleted, + }) + .unwrap_or(self.style.template_style.text.color); + + Label::new(self.filename.clone(), filename_text_style).into_any() + } +} From 62c445da570e9a65a791c3535c4e371a3e395822 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 13 May 2023 02:30:59 -0700 Subject: [PATCH 53/97] Match priority of folder highlights to vscode --- crates/project/src/worktree.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e713eed58d..b391ff829c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -195,10 +195,10 @@ impl RepositoryEntry { (GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => { &GitFileStatus::Conflict } - (GitFileStatus::Added, _) | (_, GitFileStatus::Added) => { - &GitFileStatus::Added + (GitFileStatus::Modified, _) | (_, GitFileStatus::Modified) => { + &GitFileStatus::Modified } - _ => &GitFileStatus::Modified, + _ => &GitFileStatus::Added, }, ) .copied() From 04041af78b4831761b02ff91b4c714154fde60cc Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 13 May 2023 02:40:22 -0700 Subject: [PATCH 54/97] Fixed bug with failing to clear git file status --- crates/project/src/worktree.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b391ff829c..c1f5178399 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3086,7 +3086,7 @@ impl BackgroundScanner { } let git_ptr = local_repo.repo_ptr.lock(); - git_ptr.worktree_status(&repo_path)? + git_ptr.worktree_status(&repo_path) }; let work_dir = repo.work_directory(snapshot)?; @@ -3097,7 +3097,11 @@ impl BackgroundScanner { .update(&work_dir_id, |entry| entry.scan_id = scan_id); snapshot.repository_entries.update(&work_dir, |entry| { - entry.worktree_statuses.insert(repo_path, status) + if let Some(status) = status { + entry.worktree_statuses.insert(repo_path, status); + } else { + entry.worktree_statuses.remove(&repo_path); + } }); } From 5e2aaf45a072f47cac3a7dd193f7a6b798fea422 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 13 May 2023 10:38:24 -0700 Subject: [PATCH 55/97] Fix repository initialization bug --- crates/editor/src/editor.rs | 15 +++++++++++++++ crates/project/src/worktree.rs | 13 ++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2bb9869e6d..b6d44397a9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1248,6 +1248,16 @@ impl Editor { let soft_wrap_mode_override = (mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None); + + let mut project_subscription = None; + if mode == EditorMode::Full && buffer.read(cx).is_singleton() { + if let Some(project) = project.as_ref() { + project_subscription = Some(cx.observe(project, |_, _, cx| { + cx.emit(Event::TitleChanged); + })) + } + } + let mut this = Self { handle: cx.weak_handle(), buffer: buffer.clone(), @@ -1304,6 +1314,11 @@ impl Editor { cx.observe_global::(Self::settings_changed), ], }; + + if let Some(project_subscription) = project_subscription { + this._subscriptions.push(project_subscription); + } + this.end_selection(cx); this.scroll_manager.show_scrollbar(cx); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c1f5178399..e5a1f9c93f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2973,7 +2973,7 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry, self.fs.as_ref()); - self.reload_repo_for_path(&path, &mut snapshot); + self.reload_repo_for_path(&path, &mut snapshot, self.fs.as_ref()); if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); @@ -3030,7 +3030,7 @@ impl BackgroundScanner { Some(()) } - fn reload_repo_for_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { + fn reload_repo_for_path(&self, path: &Path, snapshot: &mut LocalSnapshot, fs: &dyn Fs) -> Option<()> { let scan_id = snapshot.scan_id; if path @@ -3038,7 +3038,14 @@ impl BackgroundScanner { .any(|component| component.as_os_str() == *DOT_GIT) { let (entry_id, repo_ptr) = { - let (entry_id, repo) = snapshot.repo_for_metadata(&path)?; + let Some((entry_id, repo)) = snapshot.repo_for_metadata(&path) else { + let dot_git_dir = path.ancestors() + .skip_while(|ancestor| ancestor.file_name() != Some(&*DOT_GIT)) + .next()?; + + snapshot.build_repo(dot_git_dir.into(), fs); + return None; + }; if repo.full_scan_id == scan_id { return None; } From a6a4b846bc75554f3dda0984b930185395f1a2e3 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 13 May 2023 10:43:16 -0700 Subject: [PATCH 56/97] fmt --- crates/project/src/worktree.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e5a1f9c93f..9b965eeea4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3030,7 +3030,12 @@ impl BackgroundScanner { Some(()) } - fn reload_repo_for_path(&self, path: &Path, snapshot: &mut LocalSnapshot, fs: &dyn Fs) -> Option<()> { + fn reload_repo_for_path( + &self, + path: &Path, + snapshot: &mut LocalSnapshot, + fs: &dyn Fs, + ) -> Option<()> { let scan_id = snapshot.scan_id; if path From fa32adecd5e8970d2b876ad64646a44c2c4bf49b Mon Sep 17 00:00:00 2001 From: Julia Date: Sun, 14 May 2023 12:05:50 -0400 Subject: [PATCH 57/97] Fixup more, tests finally pass --- crates/project/src/project.rs | 168 ++++++++++++++++++--------------- crates/project/src/worktree.rs | 29 +++--- 2 files changed, 110 insertions(+), 87 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f4b5e728fb..b34cd0e1ce 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -123,6 +123,7 @@ pub struct Project { HashMap, Shared, Arc>>>>, opened_buffers: HashMap, local_buffer_ids_by_path: HashMap, + local_buffer_ids_by_entry_id: HashMap, /// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it. /// Used for re-issuing buffer requests when peers temporarily disconnect incomplete_remote_buffers: HashMap>>, @@ -451,6 +452,7 @@ impl Project { loading_buffers_by_path: Default::default(), loading_local_worktrees: Default::default(), local_buffer_ids_by_path: Default::default(), + local_buffer_ids_by_entry_id: Default::default(), buffer_snapshots: Default::default(), join_project_response_message_id: 0, client_state: None, @@ -520,6 +522,7 @@ impl Project { incomplete_remote_buffers: Default::default(), loading_local_worktrees: Default::default(), local_buffer_ids_by_path: Default::default(), + local_buffer_ids_by_entry_id: Default::default(), active_entry: None, collaborators: Default::default(), join_project_response_message_id: response.message_id, @@ -1640,6 +1643,9 @@ impl Project { }, remote_id, ); + + self.local_buffer_ids_by_entry_id + .insert(file.entry_id, remote_id); } } @@ -4574,96 +4580,106 @@ impl Project { fn update_local_worktree_buffers( &mut self, worktree_handle: &ModelHandle, - changes: &HashMap, PathChange>, + changes: &HashMap<(Arc, ProjectEntryId), PathChange>, cx: &mut ModelContext, ) { let snapshot = worktree_handle.read(cx).snapshot(); let mut renamed_buffers = Vec::new(); - for path in changes.keys() { + for (path, entry_id) in changes.keys() { let worktree_id = worktree_handle.read(cx).id(); let project_path = ProjectPath { worktree_id, path: path.clone(), }; - if let Some(&buffer_id) = self.local_buffer_ids_by_path.get(&project_path) { - if let Some(buffer) = self - .opened_buffers - .get(&buffer_id) - .and_then(|buffer| buffer.upgrade(cx)) - { - buffer.update(cx, |buffer, cx| { - if let Some(old_file) = File::from_dyn(buffer.file()) { - if old_file.worktree != *worktree_handle { - return; - } + let buffer_id = match self.local_buffer_ids_by_entry_id.get(entry_id) { + Some(&buffer_id) => buffer_id, + None => match self.local_buffer_ids_by_path.get(&project_path) { + Some(&buffer_id) => buffer_id, + None => continue, + }, + }; - let new_file = - if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { - File { - is_local: true, - entry_id: entry.id, - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree_handle.clone(), - is_deleted: false, - } - } else if let Some(entry) = - snapshot.entry_for_path(old_file.path().as_ref()) - { - File { - is_local: true, - entry_id: entry.id, - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree_handle.clone(), - is_deleted: false, - } - } else { - File { - is_local: true, - entry_id: old_file.entry_id, - path: old_file.path().clone(), - mtime: old_file.mtime(), - worktree: worktree_handle.clone(), - is_deleted: true, - } - }; + let open_buffer = self.opened_buffers.get(&buffer_id); + let buffer = if let Some(buffer) = open_buffer.and_then(|buffer| buffer.upgrade(cx)) { + buffer + } else { + self.opened_buffers.remove(&buffer_id); + self.local_buffer_ids_by_path.remove(&project_path); + self.local_buffer_ids_by_entry_id.remove(entry_id); + continue; + }; - let old_path = old_file.abs_path(cx); - if new_file.abs_path(cx) != old_path { - renamed_buffers.push((cx.handle(), old_file.clone())); - self.local_buffer_ids_by_path.remove(&project_path); - self.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id, - path: path.clone(), - }, - buffer_id, - ); - } + buffer.update(cx, |buffer, cx| { + if let Some(old_file) = File::from_dyn(buffer.file()) { + if old_file.worktree != *worktree_handle { + return; + } - if new_file != *old_file { - if let Some(project_id) = self.remote_id() { - self.client - .send(proto::UpdateBufferFile { - project_id, - buffer_id: buffer_id as u64, - file: Some(new_file.to_proto()), - }) - .log_err(); - } - - buffer.file_updated(Arc::new(new_file), cx).detach(); - } + let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, } - }); - } else { - self.opened_buffers.remove(&buffer_id); - self.local_buffer_ids_by_path.remove(&project_path); + } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, + } + } else { + File { + is_local: true, + entry_id: old_file.entry_id, + path: old_file.path().clone(), + mtime: old_file.mtime(), + worktree: worktree_handle.clone(), + is_deleted: true, + } + }; + + let old_path = old_file.abs_path(cx); + if new_file.abs_path(cx) != old_path { + renamed_buffers.push((cx.handle(), old_file.clone())); + self.local_buffer_ids_by_path.remove(&project_path); + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id, + path: path.clone(), + }, + buffer_id, + ); + } + + if new_file.entry_id != *entry_id { + self.local_buffer_ids_by_entry_id.remove(entry_id); + self.local_buffer_ids_by_entry_id + .insert(new_file.entry_id, buffer_id); + } + + if new_file != *old_file { + if let Some(project_id) = self.remote_id() { + self.client + .send(proto::UpdateBufferFile { + project_id, + buffer_id: buffer_id as u64, + file: Some(new_file.to_proto()), + }) + .log_err(); + } + + buffer.file_updated(Arc::new(new_file), cx).detach(); + } } - } + }); } for (buffer, old_file) in renamed_buffers { @@ -4676,7 +4692,7 @@ impl Project { fn update_local_worktree_language_servers( &mut self, worktree_handle: &ModelHandle, - changes: &HashMap, PathChange>, + changes: &HashMap<(Arc, ProjectEntryId), PathChange>, cx: &mut ModelContext, ) { let worktree_id = worktree_handle.read(cx).id(); @@ -4693,7 +4709,7 @@ impl Project { let params = lsp::DidChangeWatchedFilesParams { changes: changes .iter() - .filter_map(|(path, change)| { + .filter_map(|((path, _), change)| { let path = abs_path.join(path); if watched_paths.matches(&path) { Some(lsp::FileEvent { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 895eafac30..a07673e95c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -265,7 +265,7 @@ enum ScanState { Started, Updated { snapshot: LocalSnapshot, - changes: HashMap, PathChange>, + changes: HashMap<(Arc, ProjectEntryId), PathChange>, barrier: Option, scanning: bool, }, @@ -279,7 +279,7 @@ struct ShareState { } pub enum Event { - UpdatedEntries(HashMap, PathChange>), + UpdatedEntries(HashMap<(Arc, ProjectEntryId), PathChange>), UpdatedGitRepositories(HashMap, LocalRepositoryEntry>), } @@ -3039,7 +3039,7 @@ impl BackgroundScanner { old_snapshot: &Snapshot, new_snapshot: &Snapshot, event_paths: &[Arc], - ) -> HashMap, PathChange> { + ) -> HashMap<(Arc, ProjectEntryId), PathChange> { use PathChange::{Added, AddedOrUpdated, Removed, Updated}; let mut changes = HashMap::default(); @@ -3065,7 +3065,7 @@ impl BackgroundScanner { match Ord::cmp(&old_entry.path, &new_entry.path) { Ordering::Less => { - changes.insert(old_entry.path.clone(), Removed); + changes.insert((old_entry.path.clone(), old_entry.id), Removed); old_paths.next(&()); } Ordering::Equal => { @@ -3073,31 +3073,35 @@ impl BackgroundScanner { // If the worktree was not fully initialized when this event was generated, // we can't know whether this entry was added during the scan or whether // it was merely updated. - changes.insert(new_entry.path.clone(), AddedOrUpdated); + changes.insert( + (new_entry.path.clone(), new_entry.id), + AddedOrUpdated, + ); } else if old_entry.mtime != new_entry.mtime { - changes.insert(new_entry.path.clone(), Updated); + changes.insert((new_entry.path.clone(), new_entry.id), Updated); } old_paths.next(&()); new_paths.next(&()); } Ordering::Greater => { - changes.insert(new_entry.path.clone(), Added); + changes.insert((new_entry.path.clone(), new_entry.id), Added); new_paths.next(&()); } } } (Some(old_entry), None) => { - changes.insert(old_entry.path.clone(), Removed); + changes.insert((old_entry.path.clone(), old_entry.id), Removed); old_paths.next(&()); } (None, Some(new_entry)) => { - changes.insert(new_entry.path.clone(), Added); + changes.insert((new_entry.path.clone(), new_entry.id), Added); new_paths.next(&()); } (None, None) => break, } } } + changes } @@ -3937,7 +3941,7 @@ mod tests { cx.subscribe(&worktree, move |tree, _, event, _| { if let Event::UpdatedEntries(changes) = event { - for (path, change_type) in changes.iter() { + for ((path, _), change_type) in changes.iter() { let path = path.clone(); let ix = match paths.binary_search(&path) { Ok(ix) | Err(ix) => ix, @@ -3947,13 +3951,16 @@ mod tests { assert_ne!(paths.get(ix), Some(&path)); paths.insert(ix, path); } + PathChange::Removed => { assert_eq!(paths.get(ix), Some(&path)); paths.remove(ix); } + PathChange::Updated => { assert_eq!(paths.get(ix), Some(&path)); } + PathChange::AddedOrUpdated => { if paths[ix] != path { paths.insert(ix, path); @@ -3961,6 +3968,7 @@ mod tests { } } } + let new_paths = tree.paths().cloned().collect::>(); assert_eq!(paths, new_paths, "incorrect changes: {:?}", changes); } @@ -3970,7 +3978,6 @@ mod tests { let mut snapshots = Vec::new(); let mut mutations_len = operations; - fs.as_fake().pause_events().await; while mutations_len > 1 { if rng.gen_bool(0.2) { worktree From 4f36ba3b1ed22fcf7dcc449a57593571a255caf0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 14 May 2023 22:06:33 +0300 Subject: [PATCH 58/97] Add a job to build Zed images from current main The job triggers on every commit to `main` or every PR with `run-build-dmg` label and produces an install-ready *.dmg artifact attached to the corresponding CI run. --- .github/workflows/build_dmg.yml | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/build_dmg.yml diff --git a/.github/workflows/build_dmg.yml b/.github/workflows/build_dmg.yml new file mode 100644 index 0000000000..6558790fae --- /dev/null +++ b/.github/workflows/build_dmg.yml @@ -0,0 +1,49 @@ +on: + push: + branches: + - main + pull_request: + +defaults: + run: + shell: bash -euxo pipefail {0} + +concurrency: + # Allow only one workflow per any non-`main` branch. + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} + cancel-in-progress: true + +env: + RUST_BACKTRACE: 1 + COPT: '-Werror' + +jobs: + build-dmg: + if: github.ref_name == 'main' || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') + env: + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + runs-on: + - self-hosted + - test + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + clean: false + submodules: 'recursive' + + - name: Install Rust + run: | + rustup set profile minimal + rustup update stable + + + - name: Build dmg bundle + run: ./script/bundle + + - name: Upload the build stats + uses: actions/upload-artifact@v3 + with: + name: zed-main-$SHA.dmg + path: ./target/release/Zed.dmg From 5465948f200b55dd4dca0a2afeb1045ef9be359b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 14 May 2023 22:09:28 +0300 Subject: [PATCH 59/97] Build Zed dmg --- .github/workflows/build_dmg.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_dmg.yml b/.github/workflows/build_dmg.yml index 6558790fae..989914e5e8 100644 --- a/.github/workflows/build_dmg.yml +++ b/.github/workflows/build_dmg.yml @@ -1,7 +1,10 @@ +name: Build Zed.dmg + on: push: branches: - main + - "v[0-9]+.[0-9]+.x" pull_request: defaults: @@ -20,8 +23,6 @@ env: jobs: build-dmg: if: github.ref_name == 'main' || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') - env: - SHA: ${{ github.event.pull_request.head.sha || github.sha }} runs-on: - self-hosted - test @@ -38,12 +39,16 @@ jobs: rustup set profile minimal rustup update stable + - name: Install node + uses: actions/setup-node@v3 + with: + node-version: 18 - name: Build dmg bundle run: ./script/bundle - - name: Upload the build stats + - name: Upload the build artifact uses: actions/upload-artifact@v3 with: - name: zed-main-$SHA.dmg + name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg path: ./target/release/Zed.dmg From 18e0ee44a68454e8e2abd94225da422f7aa6d1ec Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 14 May 2023 13:37:03 +0300 Subject: [PATCH 60/97] Remove redundant scopes and actions to fix the focus toggle on ESC co-authored-by: Antonio --- assets/keymaps/default.json | 6 +++--- crates/gpui/src/keymap_matcher/binding.rs | 13 +++++++++++++ crates/workspace/src/workspace.rs | 6 ------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index d2fd4107e4..01a09e0cba 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -191,7 +191,7 @@ } }, { - "context": "BufferSearchBar > Editor", + "context": "BufferSearchBar", "bindings": { "escape": "buffer_search::Dismiss", "tab": "buffer_search::FocusEditor", @@ -200,13 +200,13 @@ } }, { - "context": "ProjectSearchBar > Editor", + "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus" } }, { - "context": "ProjectSearchView > Editor", + "context": "ProjectSearchView", "bindings": { "escape": "project_search::ToggleFocus" } diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index aa40e8c6af..4d8334128b 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -11,6 +11,19 @@ pub struct Binding { context_predicate: Option, } +impl std::fmt::Debug for Binding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}", + self.keystrokes, + self.action.namespace(), + self.action.name(), + self.context_predicate + ) + } +} + impl Clone for Binding { fn clone(&self) -> Self { Self { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8a62a28c11..6350b43415 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -234,7 +234,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { }, ); cx.add_action(Workspace::toggle_sidebar_item); - cx.add_action(Workspace::focus_center); cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { workspace.activate_previous_pane(cx) }); @@ -1415,11 +1414,6 @@ impl Workspace { cx.notify(); } - pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.focus_self(); - cx.notify(); - } - fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { let pane = cx.add_view(|cx| { Pane::new( From 6a7d7183814a33b188bd60094d2046fc2dfcc3f4 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Mon, 15 May 2023 14:12:02 -0400 Subject: [PATCH 61/97] Update jetbrains keymap --- assets/keymaps/jetbrains.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index 59e069e7f7..383de07904 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -11,6 +11,7 @@ "ctrl->": "zed::IncreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize", "cmd-d": "editor::DuplicateLine", + "cmd-backspace": "editor::DeleteLine", "cmd-pagedown": "editor::MovePageDown", "cmd-pageup": "editor::MovePageUp", "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart", @@ -33,6 +34,7 @@ ], "shift-alt-up": "editor::MoveLineUp", "shift-alt-down": "editor::MoveLineDown", + "cmd-alt-l": "editor::Format", "cmd-[": "pane::GoBack", "cmd-]": "pane::GoForward", "alt-f7": "editor::FindAllReferences", @@ -63,6 +65,7 @@ { "context": "Workspace", "bindings": { + "cmd-shift-o": "file_finder::Toggle", "cmd-shift-a": "command_palette::Toggle", "cmd-alt-o": "project_symbols::Toggle", "cmd-1": "workspace::ToggleLeftSidebar", From 2b18975cdc077e77880867f84d764328351d6335 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 09:41:56 -0700 Subject: [PATCH 62/97] Change folder styling from a reduce over all child files to a simple 'always modified' Remove git status from tab titles --- crates/editor/src/items.rs | 22 ++-------------------- crates/project/src/worktree.rs | 26 ++++++++------------------ 2 files changed, 10 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 80c1009aa4..d2b9c20803 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -14,7 +14,7 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, SelectionGoal, }; -use project::{repository::GitFileStatus, FormatTrigger, Item as _, Project, ProjectPath}; +use project::{FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view}; use settings::Settings; use smallvec::SmallVec; @@ -27,7 +27,6 @@ use std::{ path::{Path, PathBuf}, }; use text::Selection; -use theme::ui::FileName; use util::{ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ @@ -566,25 +565,8 @@ impl Item for Editor { style: &theme::Tab, cx: &AppContext, ) -> AnyElement { - fn git_file_status(this: &Editor, cx: &AppContext) -> Option { - let project_entry_id = this - .buffer() - .read(cx) - .as_singleton()? - .read(cx) - .entry_id(cx)?; - let project = this.project.as_ref()?.read(cx); - let path = project.path_for_entry(project_entry_id, cx)?.path; - let worktree = project.worktree_for_entry(project_entry_id, cx)?.read(cx); - worktree.repo_for(&path)?.status_for_path(&worktree, &path) - } - Flex::row() - .with_child(ComponentHost::new(FileName::new( - self.title(cx).to_string(), - git_file_status(self, cx), - FileName::style(style.label.clone(), &cx.global::().theme), - ))) + .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any()) .with_children(detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index bfd4eaa43f..cb00fc5c41 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -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, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -187,20 +187,12 @@ impl RepositoryEntry { self.worktree_statuses .iter_from(&repo_path) .take_while(|(key, _)| key.starts_with(&repo_path)) - .map(|(_, status)| status) - // Short circut once we've found the highest level - .take_until(|status| status == &&GitFileStatus::Conflict) - .reduce( - |status_first, status_second| match (status_first, status_second) { - (GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => { - &GitFileStatus::Conflict - } - (GitFileStatus::Modified, _) | (_, GitFileStatus::Modified) => { - &GitFileStatus::Modified - } - _ => &GitFileStatus::Added, - }, - ) + .map(|(path, status)| if path == &repo_path { + status + } else { + &GitFileStatus::Modified + }) + .next() .copied() }) } @@ -4170,15 +4162,13 @@ mod tests { tree.flush_fs_events(cx).await; - dbg!(git_status(&repo)); + git_status(&repo); // Check that non-repo behavior is tracked tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - dbg!(&repo.worktree_statuses); - assert_eq!(repo.worktree_statuses.iter().count(), 0); assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); From 6c26f3d0e4d0135e49f7073f8a0412f175df5abf Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 09:48:27 -0700 Subject: [PATCH 63/97] Fixed formatting --- crates/project/src/worktree.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index cb00fc5c41..5216db76f6 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -187,10 +187,12 @@ impl RepositoryEntry { self.worktree_statuses .iter_from(&repo_path) .take_while(|(key, _)| key.starts_with(&repo_path)) - .map(|(path, status)| if path == &repo_path { - status - } else { - &GitFileStatus::Modified + .map(|(path, status)| { + if path == &repo_path { + status + } else { + &GitFileStatus::Modified + } }) .next() .copied() @@ -4162,8 +4164,6 @@ mod tests { tree.flush_fs_events(cx).await; - git_status(&repo); - // Check that non-repo behavior is tracked tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); From 1e4ab6cd75f83dfe7d920da4b23d7bd663a842ea Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 12:00:12 -0700 Subject: [PATCH 64/97] Add index tracking to status --- crates/fs/src/repository.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 51b69b8bc7..4163dbab90 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -100,9 +100,9 @@ impl GitRepository for LibGitRepository { fn read_status(status: git2::Status) -> Option { if status.contains(git2::Status::CONFLICTED) { Some(GitFileStatus::Conflict) - } else if status.intersects(git2::Status::WT_MODIFIED | git2::Status::WT_RENAMED) { + } else if status.intersects(git2::Status::WT_MODIFIED | git2::Status::WT_RENAMED | git2::Status::INDEX_MODIFIED | git2::Status::INDEX_RENAMED) { Some(GitFileStatus::Modified) - } else if status.intersects(git2::Status::WT_NEW) { + } else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) { Some(GitFileStatus::Added) } else { None From 307dd2b83e240ab23594735a6638fe3bd1bdd5fc Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 13:40:55 -0700 Subject: [PATCH 65/97] Update proto names to reflect new status info --- crates/collab/src/db.rs | 54 +++++++++------------ crates/fs/src/repository.rs | 19 +++++--- crates/project/src/worktree.rs | 86 +++++++++++++++++----------------- crates/rpc/proto/zed.proto | 4 +- crates/rpc/src/proto.rs | 17 ++++--- 5 files changed, 88 insertions(+), 92 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 1047b207b9..453aa82b53 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1569,8 +1569,8 @@ impl Database { worktree.updated_repositories.push(proto::RepositoryEntry { work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, - removed_worktree_repo_paths: Default::default(), - updated_worktree_statuses: Default::default(), + removed_repo_paths: Default::default(), + updated_statuses: Default::default(), }); } } @@ -1607,15 +1607,13 @@ impl Database { let db_status_entry = db_status_entry?; if db_status_entry.is_deleted { repository - .removed_worktree_repo_paths + .removed_repo_paths .push(db_status_entry.repo_path); } else { - repository - .updated_worktree_statuses - .push(proto::StatusEntry { - repo_path: db_status_entry.repo_path, - status: db_status_entry.status as i32, - }); + repository.updated_statuses.push(proto::StatusEntry { + repo_path: db_status_entry.repo_path, + status: db_status_entry.status as i32, + }); } } } @@ -2444,12 +2442,10 @@ impl Database { .await?; for repository in update.updated_repositories.iter() { - if !repository.updated_worktree_statuses.is_empty() { + if !repository.updated_statuses.is_empty() { worktree_repository_statuses::Entity::insert_many( - repository - .updated_worktree_statuses - .iter() - .map(|status_entry| worktree_repository_statuses::ActiveModel { + repository.updated_statuses.iter().map(|status_entry| { + worktree_repository_statuses::ActiveModel { project_id: ActiveValue::set(project_id), worktree_id: ActiveValue::set(worktree_id), work_directory_id: ActiveValue::set( @@ -2459,7 +2455,8 @@ impl Database { status: ActiveValue::set(status_entry.status as i64), scan_id: ActiveValue::set(update.scan_id as i64), is_deleted: ActiveValue::set(false), - }), + } + }), ) .on_conflict( OnConflict::columns([ @@ -2479,7 +2476,7 @@ impl Database { .await?; } - if !repository.removed_worktree_repo_paths.is_empty() { + if !repository.removed_repo_paths.is_empty() { worktree_repository_statuses::Entity::update_many() .filter( worktree_repository_statuses::Column::ProjectId @@ -2492,14 +2489,9 @@ impl Database { worktree_repository_statuses::Column::WorkDirectoryId .eq(repository.work_directory_id as i64), ) - .and( - worktree_repository_statuses::Column::RepoPath.is_in( - repository - .removed_worktree_repo_paths - .iter() - .map(String::as_str), - ), - ), + .and(worktree_repository_statuses::Column::RepoPath.is_in( + repository.removed_repo_paths.iter().map(String::as_str), + )), ) .set(worktree_repository_statuses::ActiveModel { is_deleted: ActiveValue::Set(true), @@ -2765,8 +2757,8 @@ impl Database { proto::RepositoryEntry { work_directory_id: db_repository_entry.work_directory_id as u64, branch: db_repository_entry.branch, - removed_worktree_repo_paths: Default::default(), - updated_worktree_statuses: Default::default(), + removed_repo_paths: Default::default(), + updated_statuses: Default::default(), }, ); } @@ -2791,12 +2783,10 @@ impl Database { .repository_entries .get_mut(&(db_status_entry.work_directory_id as u64)) { - repository_entry - .updated_worktree_statuses - .push(proto::StatusEntry { - repo_path: db_status_entry.repo_path, - status: db_status_entry.status as i32, - }); + repository_entry.updated_statuses.push(proto::StatusEntry { + repo_path: db_status_entry.repo_path, + status: db_status_entry.status as i32, + }); } } } diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 4163dbab90..2c309351fc 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -22,9 +22,9 @@ pub trait GitRepository: Send { fn branch_name(&self) -> Option; - fn worktree_statuses(&self) -> Option>; + fn statuses(&self) -> Option>; - fn worktree_status(&self, path: &RepoPath) -> Option; + fn status(&self, path: &RepoPath) -> Option; } impl std::fmt::Debug for dyn GitRepository { @@ -71,7 +71,7 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn worktree_statuses(&self) -> Option> { + fn statuses(&self) -> Option> { let statuses = self.statuses(None).log_err()?; let mut map = TreeMap::default(); @@ -91,7 +91,7 @@ impl GitRepository for LibGitRepository { Some(map) } - fn worktree_status(&self, path: &RepoPath) -> Option { + fn status(&self, path: &RepoPath) -> Option { let status = self.status_file(path).log_err()?; read_status(status) } @@ -100,7 +100,12 @@ impl GitRepository for LibGitRepository { fn read_status(status: git2::Status) -> Option { if status.contains(git2::Status::CONFLICTED) { Some(GitFileStatus::Conflict) - } else if status.intersects(git2::Status::WT_MODIFIED | git2::Status::WT_RENAMED | git2::Status::INDEX_MODIFIED | git2::Status::INDEX_RENAMED) { + } else if status.intersects( + git2::Status::WT_MODIFIED + | git2::Status::WT_RENAMED + | git2::Status::INDEX_MODIFIED + | git2::Status::INDEX_RENAMED, + ) { Some(GitFileStatus::Modified) } else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) { Some(GitFileStatus::Added) @@ -141,7 +146,7 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn worktree_statuses(&self) -> Option> { + fn statuses(&self) -> Option> { let state = self.state.lock(); let mut map = TreeMap::default(); for (repo_path, status) in state.worktree_statuses.iter() { @@ -150,7 +155,7 @@ impl GitRepository for FakeGitRepository { Some(map) } - fn worktree_status(&self, path: &RepoPath) -> Option { + fn status(&self, path: &RepoPath) -> Option { let state = self.state.lock(); state.worktree_statuses.get(path).cloned() } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5216db76f6..9c214b7ecf 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -143,7 +143,7 @@ impl Snapshot { pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, - pub(crate) worktree_statuses: TreeMap, + pub(crate) statuses: TreeMap, } fn read_git_status(git_status: i32) -> Option { @@ -176,7 +176,7 @@ impl RepositoryEntry { pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option { self.work_directory .relativize(snapshot, path) - .and_then(|repo_path| self.worktree_statuses.get(&repo_path)) + .and_then(|repo_path| self.statuses.get(&repo_path)) .cloned() } @@ -184,7 +184,7 @@ impl RepositoryEntry { self.work_directory .relativize(snapshot, path) .and_then(|repo_path| { - self.worktree_statuses + self.statuses .iter_from(&repo_path) .take_while(|(key, _)| key.starts_with(&repo_path)) .map(|(path, status)| { @@ -203,8 +203,8 @@ impl RepositoryEntry { let mut updated_statuses: Vec = Vec::new(); let mut removed_statuses: Vec = Vec::new(); - let mut self_statuses = self.worktree_statuses.iter().peekable(); - let mut other_statuses = other.worktree_statuses.iter().peekable(); + let mut self_statuses = self.statuses.iter().peekable(); + let mut other_statuses = other.statuses.iter().peekable(); loop { match (self_statuses.peek(), other_statuses.peek()) { (Some((self_repo_path, self_status)), Some((other_repo_path, other_status))) => { @@ -243,8 +243,8 @@ impl RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id().to_proto(), branch: self.branch.as_ref().map(|str| str.to_string()), - removed_worktree_repo_paths: removed_statuses, - updated_worktree_statuses: updated_statuses, + removed_repo_paths: removed_statuses, + updated_statuses: updated_statuses, } } } @@ -269,12 +269,12 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), - updated_worktree_statuses: value - .worktree_statuses + updated_statuses: value + .statuses .iter() .map(|(repo_path, status)| make_status_entry(repo_path, status)) .collect(), - removed_worktree_repo_paths: Default::default(), + removed_repo_paths: Default::default(), } } } @@ -1540,7 +1540,7 @@ impl Snapshot { if let Some(entry) = self.entry_for_id(*work_directory_entry) { let mut statuses = TreeMap::default(); - for status_entry in repository.updated_worktree_statuses { + for status_entry in repository.updated_statuses { let Some(git_file_status) = read_git_status(status_entry.status) else { continue; }; @@ -1553,11 +1553,11 @@ impl Snapshot { if self.repository_entries.get(&work_directory).is_some() { self.repository_entries.update(&work_directory, |repo| { repo.branch = repository.branch.map(Into::into); - repo.worktree_statuses.insert_tree(statuses); + repo.statuses.insert_tree(statuses); - for repo_path in repository.removed_worktree_repo_paths { + for repo_path in repository.removed_repo_paths { let repo_path = RepoPath::new(repo_path.into()); - repo.worktree_statuses.remove(&repo_path); + repo.statuses.remove(&repo_path); } }); } else { @@ -1566,7 +1566,7 @@ impl Snapshot { RepositoryEntry { work_directory: work_directory_entry, branch: repository.branch.map(Into::into), - worktree_statuses: statuses, + statuses, }, ) } @@ -1982,7 +1982,7 @@ impl LocalSnapshot { RepositoryEntry { work_directory: work_dir_id.into(), branch: repo_lock.branch_name().map(Into::into), - worktree_statuses: repo_lock.worktree_statuses().unwrap_or_default(), + statuses: repo_lock.statuses().unwrap_or_default(), }, ); drop(repo_lock); @@ -2681,6 +2681,8 @@ impl BackgroundScanner { self.update_ignore_statuses().await; + // + let mut snapshot = self.snapshot.lock(); let mut git_repositories = mem::take(&mut snapshot.git_repositories); @@ -2993,7 +2995,7 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry, self.fs.as_ref()); - self.reload_repo_for_path(&path, &mut snapshot, self.fs.as_ref()); + self.reload_repo_for_file_path(&path, &mut snapshot, self.fs.as_ref()); if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); @@ -3042,7 +3044,7 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { entry - .worktree_statuses + .statuses .remove_range(&repo_path, &RepoPathDescendants(&repo_path)) }); } @@ -3050,7 +3052,7 @@ impl BackgroundScanner { Some(()) } - fn reload_repo_for_path( + fn reload_repo_for_file_path( &self, path: &Path, snapshot: &mut LocalSnapshot, @@ -3084,7 +3086,7 @@ impl BackgroundScanner { let repo = repo_ptr.lock(); repo.reload_index(); let branch = repo.branch_name(); - let statuses = repo.worktree_statuses().unwrap_or_default(); + let statuses = repo.statuses().unwrap_or_default(); snapshot.git_repositories.update(&entry_id, |entry| { entry.scan_id = scan_id; @@ -3093,7 +3095,7 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { entry.branch = branch.map(Into::into); - entry.worktree_statuses = statuses; + entry.statuses = statuses; }); } else { if snapshot @@ -3118,7 +3120,7 @@ impl BackgroundScanner { } let git_ptr = local_repo.repo_ptr.lock(); - git_ptr.worktree_status(&repo_path) + git_ptr.status(&repo_path) }; let work_dir = repo.work_directory(snapshot)?; @@ -3130,9 +3132,9 @@ impl BackgroundScanner { snapshot.repository_entries.update(&work_dir, |entry| { if let Some(status) = status { - entry.worktree_statuses.insert(repo_path, status); + entry.statuses.insert(repo_path, status); } else { - entry.worktree_statuses.remove(&repo_path); + entry.statuses.remove(&repo_path); } }); } @@ -4089,17 +4091,17 @@ mod tests { let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(dir.0.as_ref(), Path::new("project")); - assert_eq!(repo.worktree_statuses.iter().count(), 3); + assert_eq!(repo.statuses.iter().count(), 3); assert_eq!( - repo.worktree_statuses.get(&Path::new(A_TXT).into()), + repo.statuses.get(&Path::new(A_TXT).into()), Some(&GitFileStatus::Modified) ); assert_eq!( - repo.worktree_statuses.get(&Path::new(B_TXT).into()), + repo.statuses.get(&Path::new(B_TXT).into()), Some(&GitFileStatus::Added) ); assert_eq!( - repo.worktree_statuses.get(&Path::new(F_TXT).into()), + repo.statuses.get(&Path::new(F_TXT).into()), Some(&GitFileStatus::Added) ); }); @@ -4114,11 +4116,11 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.worktree_statuses.iter().count(), 1); - assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); assert_eq!( - repo.worktree_statuses.get(&Path::new(F_TXT).into()), + repo.statuses.get(&Path::new(F_TXT).into()), Some(&GitFileStatus::Added) ); }); @@ -4135,18 +4137,18 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.worktree_statuses.iter().count(), 3); - assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.statuses.iter().count(), 3); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); assert_eq!( - repo.worktree_statuses.get(&Path::new(B_TXT).into()), + repo.statuses.get(&Path::new(B_TXT).into()), Some(&GitFileStatus::Added) ); assert_eq!( - repo.worktree_statuses.get(&Path::new(E_TXT).into()), + repo.statuses.get(&Path::new(E_TXT).into()), Some(&GitFileStatus::Modified) ); assert_eq!( - repo.worktree_statuses.get(&Path::new(F_TXT).into()), + repo.statuses.get(&Path::new(F_TXT).into()), Some(&GitFileStatus::Added) ); }); @@ -4169,11 +4171,11 @@ mod tests { let snapshot = tree.snapshot(); let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); - assert_eq!(repo.worktree_statuses.iter().count(), 0); - assert_eq!(repo.worktree_statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.worktree_statuses.get(&Path::new(B_TXT).into()), None); - assert_eq!(repo.worktree_statuses.get(&Path::new(E_TXT).into()), None); - assert_eq!(repo.worktree_statuses.get(&Path::new(F_TXT).into()), None); + assert_eq!(repo.statuses.iter().count(), 0); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(E_TXT).into()), None); + assert_eq!(repo.statuses.get(&Path::new(F_TXT).into()), None); }); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8e45435b89..eca5fda306 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -986,8 +986,8 @@ message Entry { message RepositoryEntry { uint64 work_directory_id = 1; optional string branch = 2; - repeated string removed_worktree_repo_paths = 3; - repeated StatusEntry updated_worktree_statuses = 4; + repeated string removed_repo_paths = 3; + repeated StatusEntry updated_statuses = 4; } message StatusEntry { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index d74ed5e46c..efaaaea52e 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -509,8 +509,8 @@ pub fn split_worktree_update( updated_repositories.push(RepositoryEntry { work_directory_id: repo.work_directory_id, branch: repo.branch.clone(), - removed_worktree_repo_paths: Default::default(), - updated_worktree_statuses: Default::default(), + removed_repo_paths: Default::default(), + updated_statuses: Default::default(), }); break; } @@ -535,26 +535,25 @@ pub fn split_worktree_update( { let updated_statuses_chunk_size = cmp::min( message.updated_repositories[repository_index] - .updated_worktree_statuses + .updated_statuses .len(), max_chunk_size - total_statuses, ); let updated_statuses: Vec<_> = message.updated_repositories[repository_index] - .updated_worktree_statuses + .updated_statuses .drain(..updated_statuses_chunk_size) .collect(); total_statuses += updated_statuses.len(); let done_this_repo = message.updated_repositories[repository_index] - .updated_worktree_statuses + .updated_statuses .is_empty(); let removed_repo_paths = if done_this_repo { mem::take( - &mut message.updated_repositories[repository_index] - .removed_worktree_repo_paths, + &mut message.updated_repositories[repository_index].removed_repo_paths, ) } else { Default::default() @@ -566,8 +565,8 @@ pub fn split_worktree_update( branch: message.updated_repositories[repository_index] .branch .clone(), - updated_worktree_statuses: updated_statuses, - removed_worktree_repo_paths: removed_repo_paths, + updated_statuses, + removed_repo_paths, }); if done_this_repo { From 68078853b7f44325743710cc86f8540bcda01cd3 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 15:50:24 -0700 Subject: [PATCH 66/97] Made status tracking resilient to folder renames co-authored-by: max --- crates/project/src/worktree.rs | 269 ++++++++++++++++++++++++++++----- 1 file changed, 228 insertions(+), 41 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9c214b7ecf..cea308d7c1 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1663,6 +1663,30 @@ impl Snapshot { } } + fn descendent_entries<'a>( + &'a self, + include_dirs: bool, + include_ignored: bool, + parent_path: &'a Path, + ) -> DescendentEntriesIter<'a> { + let mut cursor = self.entries_by_path.cursor(); + cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &()); + let mut traversal = Traversal { + cursor, + include_dirs, + include_ignored, + }; + + if traversal.end_offset() == traversal.start_offset() { + traversal.advance(); + } + + DescendentEntriesIter { + traversal, + parent_path, + } + } + pub fn root_entry(&self) -> Option<&Entry> { self.entry_for_path("") } @@ -2664,14 +2688,13 @@ impl BackgroundScanner { async fn process_events(&mut self, paths: Vec) { let (scan_job_tx, scan_job_rx) = channel::unbounded(); - if let Some(mut paths) = self + let paths = self .reload_entries_for_paths(paths, Some(scan_job_tx.clone())) - .await - { - paths.sort_unstable(); + .await; + if let Some(paths) = &paths { util::extend_sorted( &mut self.prev_state.lock().event_paths, - paths, + paths.iter().cloned(), usize::MAX, Ord::cmp, ); @@ -2681,10 +2704,14 @@ impl BackgroundScanner { self.update_ignore_statuses().await; - // - let mut snapshot = self.snapshot.lock(); + if let Some(paths) = paths { + for path in paths { + self.reload_repo_for_file_path(&path, &mut *snapshot, self.fs.as_ref()); + } + } + let mut git_repositories = mem::take(&mut snapshot.git_repositories); git_repositories.retain(|work_directory_id, _| { snapshot @@ -2995,8 +3022,6 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry, self.fs.as_ref()); - self.reload_repo_for_file_path(&path, &mut snapshot, self.fs.as_ref()); - if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) { @@ -3109,34 +3134,36 @@ impl BackgroundScanner { let repo = snapshot.repo_for(&path)?; - let repo_path = repo.work_directory.relativize(&snapshot, &path)?; - - let status = { - let local_repo = snapshot.get_local_repo(&repo)?; - - // Short circuit if we've already scanned everything - if local_repo.full_scan_id == scan_id { - return None; - } - - let git_ptr = local_repo.repo_ptr.lock(); - git_ptr.status(&repo_path) - }; - let work_dir = repo.work_directory(snapshot)?; - let work_dir_id = repo.work_directory; + let work_dir_id = repo.work_directory.clone(); snapshot .git_repositories .update(&work_dir_id, |entry| entry.scan_id = scan_id); - snapshot.repository_entries.update(&work_dir, |entry| { + let local_repo = snapshot.get_local_repo(&repo)?.to_owned(); + + // Short circuit if we've already scanned everything + if local_repo.full_scan_id == scan_id { + return None; + } + + let mut repository = snapshot.repository_entries.remove(&work_dir)?; + + for entry in snapshot.descendent_entries(false, false, path) { + let Some(repo_path) = repo.work_directory.relativize(snapshot, &entry.path) else { + continue; + }; + + let status = local_repo.repo_ptr.lock().status(&repo_path); if let Some(status) = status { - entry.statuses.insert(repo_path, status); + repository.statuses.insert(repo_path.clone(), status); } else { - entry.statuses.remove(&repo_path); + repository.statuses.remove(&repo_path); } - }); + } + + snapshot.repository_entries.insert(work_dir, repository) } Some(()) @@ -3471,17 +3498,13 @@ pub struct Traversal<'a> { impl<'a> Traversal<'a> { pub fn advance(&mut self) -> bool { - self.advance_to_offset(self.offset() + 1) - } - - pub fn advance_to_offset(&mut self, offset: usize) -> bool { self.cursor.seek_forward( &TraversalTarget::Count { - count: offset, + count: self.end_offset() + 1, include_dirs: self.include_dirs, include_ignored: self.include_ignored, }, - Bias::Right, + Bias::Left, &(), ) } @@ -3508,11 +3531,17 @@ impl<'a> Traversal<'a> { self.cursor.item() } - pub fn offset(&self) -> usize { + pub fn start_offset(&self) -> usize { self.cursor .start() .count(self.include_dirs, self.include_ignored) } + + pub fn end_offset(&self) -> usize { + self.cursor + .end(&()) + .count(self.include_dirs, self.include_ignored) + } } impl<'a> Iterator for Traversal<'a> { @@ -3581,6 +3610,25 @@ impl<'a> Iterator for ChildEntriesIter<'a> { } } +struct DescendentEntriesIter<'a> { + parent_path: &'a Path, + traversal: Traversal<'a>, +} + +impl<'a> Iterator for DescendentEntriesIter<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if let Some(item) = self.traversal.entry() { + if item.path.starts_with(&self.parent_path) { + self.traversal.advance(); + return Some(item); + } + } + None + } +} + impl<'a> From<&'a Entry> for proto::Entry { fn from(entry: &'a Entry) -> Self { Self { @@ -3695,6 +3743,105 @@ mod tests { }) } + #[gpui::test] + async fn test_descendent_entries(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "a": "", + "b": { + "c": { + "d": "" + }, + "e": {} + }, + "f": "", + "g": { + "h": {} + }, + "i": { + "j": { + "k": "" + }, + "l": { + + } + }, + ".gitignore": "i/j\n", + }), + ) + .await; + + let http_client = FakeHttpClient::with_404_response(); + let client = cx.read(|cx| Client::new(http_client, cx)); + + let tree = Worktree::local( + client, + Path::new("/root"), + true, + fs, + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.descendent_entries(false, false, Path::new("b")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("b/c/d"),] + ); + assert_eq!( + tree.descendent_entries(true, false, Path::new("b")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![ + Path::new("b"), + Path::new("b/c"), + Path::new("b/c/d"), + Path::new("b/e"), + ] + ); + + assert_eq!( + tree.descendent_entries(false, false, Path::new("g")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + Vec::::new() + ); + assert_eq!( + tree.descendent_entries(true, false, Path::new("g")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("g"), Path::new("g/h"),] + ); + + assert_eq!( + tree.descendent_entries(false, false, Path::new("i")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + Vec::::new() + ); + assert_eq!( + tree.descendent_entries(false, true, Path::new("i")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("i/j/k")] + ); + assert_eq!( + tree.descendent_entries(true, false, Path::new("i")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("i"), Path::new("i/l"),] + ); + }) + } + #[gpui::test(iterations = 10)] async fn test_circular_symlinks(executor: Arc, cx: &mut TestAppContext) { let fs = FakeFs::new(cx.background()); @@ -4117,8 +4264,6 @@ mod tests { let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(repo.statuses.iter().count(), 1); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); assert_eq!( repo.statuses.get(&Path::new(F_TXT).into()), Some(&GitFileStatus::Added) @@ -4172,10 +4317,52 @@ mod tests { let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); assert_eq!(repo.statuses.iter().count(), 0); - assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(E_TXT).into()), None); - assert_eq!(repo.statuses.get(&Path::new(F_TXT).into()), None); + }); + + let mut renamed_dir_name = "first_directory/second_directory"; + const RENAMED_FILE: &'static str = "rf.txt"; + + std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap(); + std::fs::write( + work_dir.join(renamed_dir_name).join(RENAMED_FILE), + "new-contents", + ) + .unwrap(); + + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!( + repo.statuses + .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()), + Some(&GitFileStatus::Added) + ); + }); + + renamed_dir_name = "new_first_directory/second_directory"; + + std::fs::rename( + work_dir.join("first_directory"), + work_dir.join("new_first_directory"), + ) + .unwrap(); + + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!( + repo.statuses + .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()), + Some(&GitFileStatus::Added) + ); }); } From f59256f761bbb9915c565d6de92338aa116c940e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 16:15:41 -0700 Subject: [PATCH 67/97] Update git repositories to be streamed with their entries co-authored-by: max --- crates/rpc/src/proto.rs | 91 +++++++++-------------------------------- 1 file changed, 19 insertions(+), 72 deletions(-) diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index efaaaea52e..cef4e6867c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -1,6 +1,7 @@ use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope}; use anyhow::{anyhow, Result}; use async_tungstenite::tungstenite::Message as WebSocketMessage; +use collections::HashMap; use futures::{SinkExt as _, StreamExt as _}; use prost::Message as _; use serde::Serialize; @@ -485,11 +486,15 @@ pub fn split_worktree_update( max_chunk_size: usize, ) -> impl Iterator { let mut done_files = false; - let mut done_statuses = false; - let mut repository_index = 0; - let mut root_repo_found = false; + + let mut repository_map = message + .updated_repositories + .into_iter() + .map(|repo| (repo.work_directory_id, repo)) + .collect::>(); + iter::from_fn(move || { - if done_files && done_statuses { + if done_files { return None; } @@ -499,25 +504,6 @@ pub fn split_worktree_update( .drain(..updated_entries_chunk_size) .collect(); - let mut updated_repositories: Vec<_> = Default::default(); - - if !root_repo_found { - for entry in updated_entries.iter() { - if let Some(repo) = message.updated_repositories.get(0) { - if repo.work_directory_id == entry.id { - root_repo_found = true; - updated_repositories.push(RepositoryEntry { - work_directory_id: repo.work_directory_id, - branch: repo.branch.clone(), - removed_repo_paths: Default::default(), - updated_statuses: Default::default(), - }); - break; - } - } - } - } - let removed_entries_chunk_size = cmp::min(message.removed_entries.len(), max_chunk_size); let removed_entries = message .removed_entries @@ -526,64 +512,25 @@ pub fn split_worktree_update( done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty(); - // Wait to send repositories until after we've guaranteed that their associated entries - // will be read - if done_files { - let mut total_statuses = 0; - while total_statuses < max_chunk_size - && repository_index < message.updated_repositories.len() - { - let updated_statuses_chunk_size = cmp::min( - message.updated_repositories[repository_index] - .updated_statuses - .len(), - max_chunk_size - total_statuses, - ); + let mut updated_repositories = Vec::new(); - let updated_statuses: Vec<_> = message.updated_repositories[repository_index] - .updated_statuses - .drain(..updated_statuses_chunk_size) - .collect(); - - total_statuses += updated_statuses.len(); - - let done_this_repo = message.updated_repositories[repository_index] - .updated_statuses - .is_empty(); - - let removed_repo_paths = if done_this_repo { - mem::take( - &mut message.updated_repositories[repository_index].removed_repo_paths, - ) - } else { - Default::default() - }; - - updated_repositories.push(RepositoryEntry { - work_directory_id: message.updated_repositories[repository_index] - .work_directory_id, - branch: message.updated_repositories[repository_index] - .branch - .clone(), - updated_statuses, - removed_repo_paths, - }); - - if done_this_repo { - repository_index += 1; + if !repository_map.is_empty() { + for entry in &updated_entries { + if let Some(repo) = repository_map.remove(&entry.id) { + updated_repositories.push(repo) } } - } else { - Default::default() - }; + } - let removed_repositories = if done_files && done_statuses { + let removed_repositories = if done_files { mem::take(&mut message.removed_repositories) } else { Default::default() }; - done_statuses = repository_index >= message.updated_repositories.len(); + if done_files { + updated_repositories.extend(mem::take(&mut repository_map).into_values()); + } Some(UpdateWorktree { project_id: message.project_id, From 4d40aa5d6fe5505cdf9cb3108e8eeecd20105f10 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 16:17:18 -0700 Subject: [PATCH 68/97] Restore trickle up git status to folder co-authored-by: max --- crates/project/src/worktree.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index cea308d7c1..92c3c20c75 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -55,7 +55,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use util::{paths::HOME, ResultExt, TryFutureExt}; +use util::{paths::HOME, ResultExt, TryFutureExt, TakeUntilExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -187,14 +187,20 @@ impl RepositoryEntry { self.statuses .iter_from(&repo_path) .take_while(|(key, _)| key.starts_with(&repo_path)) - .map(|(path, status)| { - if path == &repo_path { - status - } else { - &GitFileStatus::Modified - } - }) - .next() + // Short circut once we've found the highest level + .take_until(|(_, status)| status == &&GitFileStatus::Conflict) + .map(|(_, status)| status) + .reduce( + |status_first, status_second| match (status_first, status_second) { + (GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => { + &GitFileStatus::Conflict + } + (GitFileStatus::Modified, _) | (_, GitFileStatus::Modified) => { + &GitFileStatus::Modified + } + _ => &GitFileStatus::Added, + }, + ) .copied() }) } From e4d509adf47758231b0947dcc5daa85457c8bd45 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 16:22:52 -0700 Subject: [PATCH 69/97] fmt --- crates/project/src/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 92c3c20c75..cc16ed91b8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -55,7 +55,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use util::{paths::HOME, ResultExt, TryFutureExt, TakeUntilExt}; +use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); From 606d5e36e1016d473f15869757a1efb39c7f45cf Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 16:44:09 -0700 Subject: [PATCH 70/97] Add events for copilot suggestion accepting and discarding --- crates/copilot/src/copilot.rs | 36 ++++++++++++++++++++++++++++++++++- crates/editor/src/editor.rs | 16 ++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 9ccd9c445d..55c4c70f65 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -47,6 +47,10 @@ pub fn init(http: Arc, node_runtime: Arc, cx: &mut }); cx.set_global(copilot.clone()); + ////////////////////////////////////// + // SUBSCRIBE TO COPILOT EVENTS HERE // + ////////////////////////////////////// + cx.observe(&copilot, |handle, cx| { let status = handle.read(cx).status(); cx.update_default_global::(move |filter, _cx| { @@ -270,8 +274,19 @@ pub struct Copilot { buffers: HashMap>, } +pub enum Event { + CompletionAccepted { + uuid: String, + file_type: Option>, + }, + CompletionDiscarded { + uuids: Vec, + file_type: Option>, + }, +} + impl Entity for Copilot { - type Event = (); + type Event = Event; fn app_will_quit( &mut self, @@ -737,18 +752,26 @@ impl Copilot { pub fn accept_completion( &mut self, completion: &Completion, + file_type: Option>, cx: &mut ModelContext, ) -> Task> { let server = match self.server.as_authenticated() { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; + + cx.emit(Event::CompletionAccepted { + uuid: completion.uuid.clone(), + file_type, + }); + let request = server .lsp .request::(request::NotifyAcceptedParams { uuid: completion.uuid.clone(), }); + cx.background().spawn(async move { request.await?; Ok(()) @@ -758,12 +781,22 @@ impl Copilot { pub fn discard_completions( &mut self, completions: &[Completion], + file_type: Option>, cx: &mut ModelContext, ) -> Task> { let server = match self.server.as_authenticated() { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; + + cx.emit(Event::CompletionDiscarded { + uuids: completions + .iter() + .map(|completion| completion.uuid.clone()) + .collect(), + file_type: file_type.clone(), + }); + let request = server .lsp @@ -773,6 +806,7 @@ impl Copilot { .map(|completion| completion.uuid.clone()) .collect(), }); + cx.background().spawn(async move { request.await?; Ok(()) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b6d44397a9..221e94370e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3094,8 +3094,14 @@ impl Editor { if let Some((copilot, completion)) = Copilot::global(cx).zip(self.copilot_state.active_completion()) { + let language = self + .language_at(completion.range.start.offset, cx) + .map(|language| language.name()); + copilot - .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) + .update(cx, |copilot, cx| { + copilot.accept_completion(completion, language, cx) + }) .detach_and_log_err(cx); } self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx); @@ -3109,9 +3115,15 @@ impl Editor { fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { if self.has_active_copilot_suggestion(cx) { if let Some(copilot) = Copilot::global(cx) { + let file_type = self.copilot_state + .completions + .get(0) + .and_then(|completion| self.language_at(completion.range.start.offset, cx)) + .map(|language| language.name()); + copilot .update(cx, |copilot, cx| { - copilot.discard_completions(&self.copilot_state.completions, cx) + copilot.discard_completions(&self.copilot_state.completions, file_type, cx) }) .detach_and_log_err(cx); } From ead9ac6f236423ee4f04fe5451e35bbdb9bdd0b8 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 15 May 2023 16:47:39 -0700 Subject: [PATCH 71/97] Fix typo --- crates/copilot/src/copilot.rs | 4 ++-- crates/editor/src/editor.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 55c4c70f65..b9e850b371 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -279,7 +279,7 @@ pub enum Event { uuid: String, file_type: Option>, }, - CompletionDiscarded { + CompletionsDiscarded { uuids: Vec, file_type: Option>, }, @@ -789,7 +789,7 @@ impl Copilot { Err(error) => return Task::ready(Err(error)), }; - cx.emit(Event::CompletionDiscarded { + cx.emit(Event::CompletionsDiscarded { uuids: completions .iter() .map(|completion| completion.uuid.clone()) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 221e94370e..9c5fe7e940 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3115,7 +3115,8 @@ impl Editor { fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { if self.has_active_copilot_suggestion(cx) { if let Some(copilot) = Copilot::global(cx) { - let file_type = self.copilot_state + let file_type = self + .copilot_state .completions .get(0) .and_then(|completion| self.language_at(completion.range.start.offset, cx)) From a6a2f9360743a9fa12b199c9b0df8eb26caf15cb Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 00:34:58 -0400 Subject: [PATCH 72/97] Update telemetry client to accept copilot events --- crates/client/src/telemetry.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 7151dcd7bb..d11a78bb62 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -86,6 +86,11 @@ pub enum ClickhouseEvent { copilot_enabled: bool, copilot_enabled_for_language: bool, }, + Copilot { + suggestion_id: String, + suggestion_accepted: bool, + file_extension: Option, + }, } #[derive(Serialize, Debug)] From f50afefed337ca13dc44bb175e90b73dfc2c997e Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 00:35:21 -0400 Subject: [PATCH 73/97] Subscribe to copilot events (WIP) --- crates/copilot/src/copilot.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index b9e850b371..07c54c7785 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -47,9 +47,21 @@ pub fn init(http: Arc, node_runtime: Arc, cx: &mut }); cx.set_global(copilot.clone()); - ////////////////////////////////////// - // SUBSCRIBE TO COPILOT EVENTS HERE // - ////////////////////////////////////// + cx.subscribe(&copilot, |_, event, _| { + match event { + Event::CompletionAccepted { uuid, file_type } => { + // Build event object and pass it in + // telemetry.report_clickhouse_event(event, settings.telemetry()) + } + Event::CompletionsDiscarded { uuids, file_type } => { + for uuid in uuids { + // Build event object and pass it in + // telemetry.report_clickhouse_event(event, settings.telemetry()) + } + } + }; + }) + .detach(); cx.observe(&copilot, |handle, cx| { let status = handle.read(cx).status(); From a7fc07a8cd62570f9f44bbad1fa4bcaeb97548cf Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 03:12:39 -0400 Subject: [PATCH 74/97] Init copilot with client instead of http client --- Cargo.lock | 1 + crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 38 ++++++++++++++++++++++------------- crates/zed/src/main.rs | 3 ++- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e009cfd342..2af1a4aa36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,6 +1338,7 @@ dependencies = [ "anyhow", "async-compression", "async-tar", + "client", "clock", "collections", "context_menu", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index bac335f7b7..1a6ec7968d 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -22,6 +22,7 @@ test-support = [ collections = { path = "../collections" } context_menu = { path = "../context_menu" } gpui = { path = "../gpui" } +client = { path = "../client" } language = { path = "../language" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 07c54c7785..f1b547a182 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,6 +4,7 @@ mod sign_in; use anyhow::{anyhow, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; +use client::{ClickhouseEvent, Client}; use collections::HashMap; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ @@ -40,26 +41,35 @@ actions!( [Suggest, NextSuggestion, PreviousSuggestion, Reinstall] ); -pub fn init(http: Arc, node_runtime: Arc, cx: &mut AppContext) { +pub fn init(client: &Client, node_runtime: Arc, cx: &mut AppContext) { let copilot = cx.add_model({ let node_runtime = node_runtime.clone(); - move |cx| Copilot::start(http, node_runtime, cx) + move |cx| Copilot::start(client.http_client(), node_runtime, cx) }); cx.set_global(copilot.clone()); - cx.subscribe(&copilot, |_, event, _| { - match event { - Event::CompletionAccepted { uuid, file_type } => { - // Build event object and pass it in - // telemetry.report_clickhouse_event(event, settings.telemetry()) + let telemetry_settings = cx.global::().telemetry(); + let telemetry = client.telemetry(); + + cx.subscribe(&copilot, move |_, event, _| match event { + Event::CompletionAccepted { uuid, file_type } => { + let event = ClickhouseEvent::Copilot { + suggestion_id: uuid.clone(), + suggestion_accepted: true, + file_extension: file_type.clone().map(|a| a.to_string()), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); + } + Event::CompletionsDiscarded { uuids, file_type } => { + for uuid in uuids { + let event = ClickhouseEvent::Copilot { + suggestion_id: uuid.clone(), + suggestion_accepted: false, + file_extension: file_type.clone().map(|a| a.to_string()), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); } - Event::CompletionsDiscarded { uuids, file_type } => { - for uuid in uuids { - // Build event object and pass it in - // telemetry.report_clickhouse_event(event, settings.telemetry()) - } - } - }; + } }) .detach(); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f498078b52..434234f7f6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -178,7 +178,6 @@ fn main() { vim::init(cx); terminal_view::init(cx); theme_testbench::init(cx); - copilot::init(http.clone(), node_runtime, cx); cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) .detach(); @@ -197,6 +196,8 @@ fn main() { cx.global::().telemetry(), ); + copilot::init(&client, node_runtime, cx); + let app_state = Arc::new(AppState { languages, themes, From 903eed964ab3941b359a3c299a1d34106ead70cd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 16 May 2023 14:45:50 +0300 Subject: [PATCH 75/97] Allow CLI to start Zed from local sources Zed now is able to behave as if it's being started from CLI (`ZED_FORCE_CLI_MODE` env var) Zed CLI accepts regular binary file path into `-b` parameter (only *.app before), and tries to start it as Zed editor with `ZED_FORCE_CLI_MODE` env var and other params needed. --- crates/cli/src/cli.rs | 4 + crates/cli/src/main.rs | 197 ++++++++++++++++++++++++++++++----------- crates/zed/src/main.rs | 85 ++++++++++++------ 3 files changed, 211 insertions(+), 75 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 7cad42b534..de7b14e142 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -20,3 +20,7 @@ pub enum CliResponse { Stderr { message: String }, Exit { status: i32 }, } + +/// When Zed started not as an *.app but as a binary (e.g. local development), +/// there's a possibility to tell it to behave "regularly". +pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE"; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a31e59587f..0ae4d2477e 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,6 +1,6 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use clap::Parser; -use cli::{CliRequest, CliResponse, IpcHandshake}; +use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME}; use core_foundation::{ array::{CFArray, CFIndex}, string::kCFStringEncodingUTF8, @@ -43,20 +43,10 @@ struct InfoPlist { fn main() -> Result<()> { let args = Args::parse(); - let bundle_path = if let Some(bundle_path) = args.bundle_path { - bundle_path.canonicalize()? - } else { - locate_bundle()? - }; + let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?; if args.version { - let plist_path = bundle_path.join("Contents/Info.plist"); - let plist = plist::from_file::<_, InfoPlist>(plist_path)?; - println!( - "Zed {} – {}", - plist.bundle_short_version_string, - bundle_path.to_string_lossy() - ); + println!("{}", bundle.zed_version_string()); return Ok(()); } @@ -66,7 +56,7 @@ fn main() -> Result<()> { } } - let (tx, rx) = launch_app(bundle_path)?; + let (tx, rx) = bundle.launch()?; tx.send(CliRequest::Open { paths: args @@ -89,6 +79,148 @@ fn main() -> Result<()> { Ok(()) } +enum Bundle { + App { + app_bundle: PathBuf, + plist: InfoPlist, + }, + LocalPath { + executable: PathBuf, + plist: InfoPlist, + }, +} + +impl Bundle { + fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result { + let bundle_path = if let Some(bundle_path) = args_bundle_path { + bundle_path + .canonicalize() + .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))? + } else { + locate_bundle().context("bundle autodiscovery")? + }; + + match bundle_path.extension().and_then(|ext| ext.to_str()) { + Some("app") => { + let plist_path = bundle_path.join("Contents/Info.plist"); + let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| { + format!("Reading *.app bundle plist file at {plist_path:?}") + })?; + Ok(Self::App { + app_bundle: bundle_path, + plist, + }) + } + _ => { + println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build"); + let plist_path = bundle_path + .parent() + .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))? + .join("WebRTC.framework/Resources/Info.plist"); + let plist = plist::from_file::<_, InfoPlist>(&plist_path) + .with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?; + Ok(Self::LocalPath { + executable: bundle_path, + plist, + }) + } + } + } + + fn plist(&self) -> &InfoPlist { + match self { + Self::App { plist, .. } => plist, + Self::LocalPath { plist, .. } => plist, + } + } + + fn path(&self) -> &Path { + match self { + Self::App { app_bundle, .. } => app_bundle, + Self::LocalPath { + executable: excutable, + .. + } => excutable, + } + } + + fn launch(&self) -> anyhow::Result<(IpcSender, IpcReceiver)> { + let (server, server_name) = + IpcOneShotServer::::new().context("Handshake before Zed spawn")?; + let url = format!("zed-cli://{server_name}"); + + match self { + Self::App { app_bundle, .. } => { + let app_path = app_bundle; + + let status = unsafe { + let app_url = CFURL::from_path(app_path, true) + .with_context(|| format!("invalid app path {app_path:?}"))?; + let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes( + ptr::null(), + url.as_ptr(), + url.len() as CFIndex, + kCFStringEncodingUTF8, + ptr::null(), + )); + let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); + LSOpenFromURLSpec( + &LSLaunchURLSpec { + appURL: app_url.as_concrete_TypeRef(), + itemURLs: urls_to_open.as_concrete_TypeRef(), + passThruParams: ptr::null(), + launchFlags: kLSLaunchDefaults, + asyncRefCon: ptr::null_mut(), + }, + ptr::null_mut(), + ) + }; + + anyhow::ensure!( + status == 0, + "cannot start app bundle {}", + self.zed_version_string() + ); + } + Self::LocalPath { executable, .. } => { + let executable_parent = executable + .parent() + .with_context(|| format!("Executable {executable:?} path has no parent"))?; + let subprocess_stdout_file = + fs::File::create(executable_parent.join("zed_dev.log")) + .with_context(|| format!("Log file creation in {executable_parent:?}"))?; + let subprocess_stdin_file = + subprocess_stdout_file.try_clone().with_context(|| { + format!("Cloning descriptor for file {subprocess_stdout_file:?}") + })?; + let mut command = std::process::Command::new(executable); + let command = command + .env(FORCE_CLI_MODE_ENV_VAR_NAME, "") + .stderr(subprocess_stdout_file) + .stdout(subprocess_stdin_file) + .arg(url); + + command + .spawn() + .with_context(|| format!("Spawning {command:?}"))?; + } + } + + let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; + Ok((handshake.requests, handshake.responses)) + } + + fn zed_version_string(&self) -> String { + let is_dev = matches!(self, Self::LocalPath { .. }); + format!( + "Zed {}{} – {}", + self.plist().bundle_short_version_string, + if is_dev { " (dev)" } else { "" }, + self.path().display(), + ) + } +} + fn touch(path: &Path) -> io::Result<()> { match OpenOptions::new().create(true).write(true).open(path) { Ok(_) => Ok(()), @@ -106,38 +238,3 @@ fn locate_bundle() -> Result { } Ok(app_path) } - -fn launch_app(app_path: PathBuf) -> Result<(IpcSender, IpcReceiver)> { - let (server, server_name) = IpcOneShotServer::::new()?; - let url = format!("zed-cli://{server_name}"); - - let status = unsafe { - let app_url = - CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?; - let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes( - ptr::null(), - url.as_ptr(), - url.len() as CFIndex, - kCFStringEncodingUTF8, - ptr::null(), - )); - let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); - LSOpenFromURLSpec( - &LSLaunchURLSpec { - appURL: app_url.as_concrete_TypeRef(), - itemURLs: urls_to_open.as_concrete_TypeRef(), - passThruParams: ptr::null(), - launchFlags: kLSLaunchDefaults, - asyncRefCon: ptr::null_mut(), - }, - ptr::null_mut(), - ) - }; - - if status == 0 { - let (_, handshake) = server.accept()?; - Ok((handshake.requests, handshake.responses)) - } else { - Err(anyhow!("cannot start {:?}", app_path)) - } -} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f498078b52..60a2fc66be 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -6,7 +6,7 @@ use assets::Assets; use backtrace::Backtrace; use cli::{ ipc::{self, IpcSender}, - CliRequest, CliResponse, IpcHandshake, + CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; @@ -37,7 +37,10 @@ use std::{ os::unix::prelude::OsStrExt, panic, path::PathBuf, - sync::{Arc, Weak}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Weak, + }, thread, time::Duration, }; @@ -89,29 +92,17 @@ fn main() { }; let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded(); + let cli_connections_tx = Arc::new(cli_connections_tx); let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded(); + let open_paths_tx = Arc::new(open_paths_tx); + let urls_callback_triggered = Arc::new(AtomicBool::new(false)); + + let callback_cli_connections_tx = Arc::clone(&cli_connections_tx); + let callback_open_paths_tx = Arc::clone(&open_paths_tx); + let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered); app.on_open_urls(move |urls, _| { - if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { - if let Some(cli_connection) = connect_to_cli(server_name).log_err() { - cli_connections_tx - .unbounded_send(cli_connection) - .map_err(|_| anyhow!("no listener for cli connections")) - .log_err(); - }; - } else { - let paths: Vec<_> = urls - .iter() - .flat_map(|url| url.strip_prefix("file://")) - .map(|url| { - let decoded = urlencoding::decode_binary(url.as_bytes()); - PathBuf::from(OsStr::from_bytes(decoded.as_ref())) - }) - .collect(); - open_paths_tx - .unbounded_send(paths) - .map_err(|_| anyhow!("no listener for open urls requests")) - .log_err(); - } + callback_urls_callback_triggered.store(true, Ordering::Release); + open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx); }) .on_reopen(move |cx| { if cx.has_global::>() { @@ -234,6 +225,14 @@ fn main() { workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx); } } else { + // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead + // of an *app, hence gets no specific callbacks run. Emulate them here, if needed. + if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() + && !urls_callback_triggered.load(Ordering::Acquire) + { + open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx) + } + if let Ok(Some(connection)) = cli_connections_rx.try_next() { cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) .detach(); @@ -284,6 +283,37 @@ fn main() { }); } +fn open_urls( + urls: Vec, + cli_connections_tx: &mpsc::UnboundedSender<( + mpsc::Receiver, + IpcSender, + )>, + open_paths_tx: &mpsc::UnboundedSender>, +) { + if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { + if let Some(cli_connection) = connect_to_cli(server_name).log_err() { + cli_connections_tx + .unbounded_send(cli_connection) + .map_err(|_| anyhow!("no listener for cli connections")) + .log_err(); + }; + } else { + let paths: Vec<_> = urls + .iter() + .flat_map(|url| url.strip_prefix("file://")) + .map(|url| { + let decoded = urlencoding::decode_binary(url.as_bytes()); + PathBuf::from(OsStr::from_bytes(decoded.as_ref())) + }) + .collect(); + open_paths_tx + .unbounded_send(paths) + .map_err(|_| anyhow!("no listener for open urls requests")) + .log_err(); + } +} + async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { if let Some(location) = workspace::last_opened_workspace_paths().await { cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) @@ -514,7 +544,8 @@ async fn load_login_shell_environment() -> Result<()> { } fn stdout_is_a_pty() -> bool { - unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 } + std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() + && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 } } fn collect_path_args() -> Vec { @@ -527,7 +558,11 @@ fn collect_path_args() -> Vec { None } }) - .collect::>() + .collect() +} + +fn collect_url_args() -> Vec { + env::args().skip(1).collect() } fn load_embedded_fonts(app: &App) { From 2d4b2e0844ee6415375b3f768d8e7df73e16f333 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 11:51:20 -0400 Subject: [PATCH 76/97] Fix compile error --- crates/copilot/src/copilot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index f1b547a182..c7671cee7a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -49,7 +49,7 @@ pub fn init(client: &Client, node_runtime: Arc, cx: &mut AppContext cx.set_global(copilot.clone()); let telemetry_settings = cx.global::().telemetry(); - let telemetry = client.telemetry(); + let telemetry = client.telemetry().clone(); cx.subscribe(&copilot, move |_, event, _| match event { Event::CompletionAccepted { uuid, file_type } => { From f50240181a669f2359c0f35722b3e6737c40881a Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 May 2023 13:00:39 -0400 Subject: [PATCH 77/97] Avoid removing fake fs entry when rename fails later in the process Co-Authored-By: Antonio Scandurra --- crates/fs/src/fs.rs | 21 +++++++++++++++++---- crates/project/src/worktree.rs | 11 +++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 3285eb328a..99562405b5 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -572,15 +572,15 @@ impl FakeFs { Ok(()) } - pub async fn pause_events(&self) { + pub fn pause_events(&self) { self.state.lock().events_paused = true; } - pub async fn buffered_event_count(&self) -> usize { + pub fn buffered_event_count(&self) -> usize { self.state.lock().buffered_events.len() } - pub async fn flush_events(&self, count: usize) { + pub fn flush_events(&self, count: usize) { self.state.lock().flush_events(count); } @@ -832,14 +832,16 @@ impl Fs for FakeFs { let old_path = normalize_path(old_path); let new_path = normalize_path(new_path); + let mut state = self.state.lock(); let moved_entry = state.write_path(&old_path, |e| { if let btree_map::Entry::Occupied(e) = e { - Ok(e.remove()) + Ok(e.get().clone()) } else { Err(anyhow!("path does not exist: {}", &old_path.display())) } })?; + state.write_path(&new_path, |e| { match e { btree_map::Entry::Occupied(mut e) => { @@ -855,6 +857,17 @@ impl Fs for FakeFs { } Ok(()) })?; + + state + .write_path(&old_path, |e| { + if let btree_map::Entry::Occupied(e) = e { + Ok(e.remove()) + } else { + unreachable!() + } + }) + .unwrap(); + state.emit_event(&[old_path, new_path]); Ok(()) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index cc16ed91b8..958e72fa18 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4632,6 +4632,7 @@ mod tests { .detach(); }); + fs.as_fake().pause_events(); let mut snapshots = Vec::new(); let mut mutations_len = operations; while mutations_len > 1 { @@ -4641,16 +4642,16 @@ mod tests { randomly_mutate_worktree(worktree, &mut rng, cx) }) .await - .unwrap(); + .log_err(); } else { randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; } - let buffered_event_count = fs.as_fake().buffered_event_count().await; + let buffered_event_count = fs.as_fake().buffered_event_count(); if buffered_event_count > 0 && rng.gen_bool(0.3) { let len = rng.gen_range(0..=buffered_event_count); log::info!("flushing {} events", len); - fs.as_fake().flush_events(len).await; + fs.as_fake().flush_events(len); } else { randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await; mutations_len -= 1; @@ -4666,7 +4667,7 @@ mod tests { } log::info!("quiescing"); - fs.as_fake().flush_events(usize::MAX).await; + fs.as_fake().flush_events(usize::MAX); cx.foreground().run_until_parked(); let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); snapshot.check_invariants(); @@ -4726,6 +4727,7 @@ mod tests { rng: &mut impl Rng, cx: &mut ModelContext, ) -> Task> { + log::info!("mutating worktree"); let worktree = worktree.as_local_mut().unwrap(); let snapshot = worktree.snapshot(); let entry = snapshot.entries(false).choose(rng).unwrap(); @@ -4787,6 +4789,7 @@ mod tests { insertion_probability: f64, rng: &mut impl Rng, ) { + log::info!("mutating fs"); let mut files = Vec::new(); let mut dirs = Vec::new(); for path in fs.as_fake().paths() { From 8b63caa0bd6bc57d837bf9a52f5c0d70c0c92513 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 May 2023 13:01:29 -0400 Subject: [PATCH 78/97] Fix worktree refresh request causing gitignore to not update Co-Authored-By: Antonio Scandurra --- crates/project/src/worktree.rs | 40 ++++++++++++++++------------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 958e72fa18..403d893425 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -338,7 +338,7 @@ impl<'a> From for WorkDirectoryEntry { #[derive(Debug, Clone)] pub struct LocalSnapshot { - ignores_by_parent_abs_path: HashMap, (Arc, usize)>, + ignores_by_parent_abs_path: HashMap, (Arc, bool)>, // (gitignore, needs_update) // The ProjectEntryId corresponds to the entry for the .git dir // work_directory_id git_repositories: TreeMap, @@ -1882,10 +1882,8 @@ impl LocalSnapshot { let abs_path = self.abs_path.join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { Ok(ignore) => { - self.ignores_by_parent_abs_path.insert( - abs_path.parent().unwrap().into(), - (Arc::new(ignore), self.scan_id), - ); + self.ignores_by_parent_abs_path + .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true)); } Err(error) => { log::error!( @@ -1955,10 +1953,8 @@ impl LocalSnapshot { } if let Some(ignore) = ignore { - self.ignores_by_parent_abs_path.insert( - self.abs_path.join(&parent_path).into(), - (ignore, self.scan_id), - ); + self.ignores_by_parent_abs_path + .insert(self.abs_path.join(&parent_path).into(), (ignore, false)); } if parent_path.file_name() == Some(&DOT_GIT) { @@ -2062,11 +2058,11 @@ impl LocalSnapshot { if path.file_name() == Some(&GITIGNORE) { let abs_parent_path = self.abs_path.join(path.parent().unwrap()); - if let Some((_, scan_id)) = self + if let Some((_, needs_update)) = self .ignores_by_parent_abs_path .get_mut(abs_parent_path.as_path()) { - *scan_id = self.snapshot.scan_id; + *needs_update = true; } } } @@ -2609,7 +2605,7 @@ impl BackgroundScanner { self.snapshot .lock() .ignores_by_parent_abs_path - .insert(ancestor.into(), (ignore.into(), 0)); + .insert(ancestor.into(), (ignore.into(), false)); } } { @@ -2662,7 +2658,7 @@ impl BackgroundScanner { // these before handling changes reported by the filesystem. request = self.refresh_requests_rx.recv().fuse() => { let Ok((paths, barrier)) = request else { break }; - if !self.process_refresh_request(paths, barrier).await { + if !self.process_refresh_request(paths.clone(), barrier).await { return; } } @@ -2673,7 +2669,7 @@ impl BackgroundScanner { while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) { paths.extend(more_events.into_iter().map(|e| e.path)); } - self.process_events(paths).await; + self.process_events(paths.clone()).await; } } } @@ -3181,16 +3177,18 @@ impl BackgroundScanner { let mut snapshot = self.snapshot.lock().clone(); let mut ignores_to_update = Vec::new(); let mut ignores_to_delete = Vec::new(); - for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_parent_abs_path { - if let Ok(parent_path) = parent_abs_path.strip_prefix(&snapshot.abs_path) { - if *scan_id > snapshot.completed_scan_id - && snapshot.entry_for_path(parent_path).is_some() - { - ignores_to_update.push(parent_abs_path.clone()); + let abs_path = snapshot.abs_path.clone(); + for (parent_abs_path, (_, needs_update)) in &mut snapshot.ignores_by_parent_abs_path { + if let Ok(parent_path) = parent_abs_path.strip_prefix(&abs_path) { + if *needs_update { + *needs_update = false; + if snapshot.snapshot.entry_for_path(parent_path).is_some() { + ignores_to_update.push(parent_abs_path.clone()); + } } let ignore_path = parent_path.join(&*GITIGNORE); - if snapshot.entry_for_path(ignore_path).is_none() { + if snapshot.snapshot.entry_for_path(ignore_path).is_none() { ignores_to_delete.push(parent_abs_path.clone()); } } From 6976d60bfed7af33a5147c910e45c756711a2204 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 13:24:25 -0400 Subject: [PATCH 79/97] Rework code to contain submitting of copilot events within editor --- Cargo.lock | 1 - crates/copilot/Cargo.toml | 1 - crates/copilot/src/copilot.rs | 64 +++-------------------------------- crates/editor/src/editor.rs | 50 +++++++++++++++++++-------- crates/zed/src/main.rs | 3 +- 5 files changed, 40 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2af1a4aa36..e009cfd342 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,7 +1338,6 @@ dependencies = [ "anyhow", "async-compression", "async-tar", - "client", "clock", "collections", "context_menu", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 1a6ec7968d..bac335f7b7 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -22,7 +22,6 @@ test-support = [ collections = { path = "../collections" } context_menu = { path = "../context_menu" } gpui = { path = "../gpui" } -client = { path = "../client" } language = { path = "../language" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index c7671cee7a..65d0a19bed 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,7 +4,6 @@ mod sign_in; use anyhow::{anyhow, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use client::{ClickhouseEvent, Client}; use collections::HashMap; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ @@ -41,38 +40,13 @@ actions!( [Suggest, NextSuggestion, PreviousSuggestion, Reinstall] ); -pub fn init(client: &Client, node_runtime: Arc, cx: &mut AppContext) { +pub fn init(http: Arc, node_runtime: Arc, cx: &mut AppContext) { let copilot = cx.add_model({ let node_runtime = node_runtime.clone(); - move |cx| Copilot::start(client.http_client(), node_runtime, cx) + move |cx| Copilot::start(http, node_runtime, cx) }); cx.set_global(copilot.clone()); - let telemetry_settings = cx.global::().telemetry(); - let telemetry = client.telemetry().clone(); - - cx.subscribe(&copilot, move |_, event, _| match event { - Event::CompletionAccepted { uuid, file_type } => { - let event = ClickhouseEvent::Copilot { - suggestion_id: uuid.clone(), - suggestion_accepted: true, - file_extension: file_type.clone().map(|a| a.to_string()), - }; - telemetry.report_clickhouse_event(event, telemetry_settings); - } - Event::CompletionsDiscarded { uuids, file_type } => { - for uuid in uuids { - let event = ClickhouseEvent::Copilot { - suggestion_id: uuid.clone(), - suggestion_accepted: false, - file_extension: file_type.clone().map(|a| a.to_string()), - }; - telemetry.report_clickhouse_event(event, telemetry_settings); - } - } - }) - .detach(); - cx.observe(&copilot, |handle, cx| { let status = handle.read(cx).status(); cx.update_default_global::(move |filter, _cx| { @@ -284,7 +258,7 @@ impl RegisteredBuffer { #[derive(Debug)] pub struct Completion { - uuid: String, + pub uuid: String, pub range: Range, pub text: String, } @@ -296,19 +270,8 @@ pub struct Copilot { buffers: HashMap>, } -pub enum Event { - CompletionAccepted { - uuid: String, - file_type: Option>, - }, - CompletionsDiscarded { - uuids: Vec, - file_type: Option>, - }, -} - impl Entity for Copilot { - type Event = Event; + type Event = (); fn app_will_quit( &mut self, @@ -774,26 +737,18 @@ impl Copilot { pub fn accept_completion( &mut self, completion: &Completion, - file_type: Option>, cx: &mut ModelContext, ) -> Task> { let server = match self.server.as_authenticated() { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; - - cx.emit(Event::CompletionAccepted { - uuid: completion.uuid.clone(), - file_type, - }); - let request = server .lsp .request::(request::NotifyAcceptedParams { uuid: completion.uuid.clone(), }); - cx.background().spawn(async move { request.await?; Ok(()) @@ -803,22 +758,12 @@ impl Copilot { pub fn discard_completions( &mut self, completions: &[Completion], - file_type: Option>, cx: &mut ModelContext, ) -> Task> { let server = match self.server.as_authenticated() { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; - - cx.emit(Event::CompletionsDiscarded { - uuids: completions - .iter() - .map(|completion| completion.uuid.clone()) - .collect(), - file_type: file_type.clone(), - }); - let request = server .lsp @@ -828,7 +773,6 @@ impl Copilot { .map(|completion| completion.uuid.clone()) .collect(), }); - cx.background().spawn(async move { request.await?; Ok(()) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9c5fe7e940..b4af9abb85 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3094,15 +3094,11 @@ impl Editor { if let Some((copilot, completion)) = Copilot::global(cx).zip(self.copilot_state.active_completion()) { - let language = self - .language_at(completion.range.start.offset, cx) - .map(|language| language.name()); - copilot - .update(cx, |copilot, cx| { - copilot.accept_completion(completion, language, cx) - }) + .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .detach_and_log_err(cx); + + self.report_copilot_event(completion.uuid.clone(), true, cx) } self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx); cx.notify(); @@ -3115,18 +3111,15 @@ impl Editor { fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { if self.has_active_copilot_suggestion(cx) { if let Some(copilot) = Copilot::global(cx) { - let file_type = self - .copilot_state - .completions - .get(0) - .and_then(|completion| self.language_at(completion.range.start.offset, cx)) - .map(|language| language.name()); - copilot .update(cx, |copilot, cx| { - copilot.discard_completions(&self.copilot_state.completions, file_type, cx) + copilot.discard_completions(&self.copilot_state.completions, cx) }) .detach_and_log_err(cx); + + for completion in &self.copilot_state.completions { + self.report_copilot_event(completion.uuid.clone(), false, cx) + } } self.display_map @@ -6889,6 +6882,33 @@ impl Editor { .collect() } + fn report_copilot_event( + &self, + suggestion_id: String, + suggestion_accepted: bool, + cx: &AppContext, + ) { + if let Some((project, file)) = self.project.as_ref().zip( + self.buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()), + ) { + let telemetry_settings = cx.global::().telemetry(); + let extension = Path::new(file.file_name(cx)) + .extension() + .and_then(|e| e.to_str()); + let telemetry = project.read(cx).client().telemetry().clone(); + + let event = ClickhouseEvent::Copilot { + suggestion_id, + suggestion_accepted, + file_extension: extension.map(ToString::to_string), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); + } + } + fn report_editor_event(&self, name: &'static str, cx: &AppContext) { if let Some((project, file)) = self.project.as_ref().zip( self.buffer diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 434234f7f6..f498078b52 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -178,6 +178,7 @@ fn main() { vim::init(cx); terminal_view::init(cx); theme_testbench::init(cx); + copilot::init(http.clone(), node_runtime, cx); cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) .detach(); @@ -196,8 +197,6 @@ fn main() { cx.global::().telemetry(), ); - copilot::init(&client, node_runtime, cx); - let app_state = Arc::new(AppState { languages, themes, From afe75e8cbd750a7b18dbf6d5dfacefdaa9be7adf Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 14:02:36 -0400 Subject: [PATCH 80/97] Send copilot events even if file_extension is not known at the time --- crates/editor/src/editor.rs | 39 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b4af9abb85..c51ed1a14a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6888,25 +6888,28 @@ impl Editor { suggestion_accepted: bool, cx: &AppContext, ) { - if let Some((project, file)) = self.project.as_ref().zip( - self.buffer - .read(cx) - .as_singleton() - .and_then(|b| b.read(cx).file()), - ) { - let telemetry_settings = cx.global::().telemetry(); - let extension = Path::new(file.file_name(cx)) - .extension() - .and_then(|e| e.to_str()); - let telemetry = project.read(cx).client().telemetry().clone(); + let Some(project) = &self.project else { + return + }; - let event = ClickhouseEvent::Copilot { - suggestion_id, - suggestion_accepted, - file_extension: extension.map(ToString::to_string), - }; - telemetry.report_clickhouse_event(event, telemetry_settings); - } + // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension + let file_extension = self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()) + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()); + + let telemetry = project.read(cx).client().telemetry().clone(); + let telemetry_settings = cx.global::().telemetry(); + + let event = ClickhouseEvent::Copilot { + suggestion_id, + suggestion_accepted, + file_extension: file_extension.map(ToString::to_string), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); } fn report_editor_event(&self, name: &'static str, cx: &AppContext) { From 3eea2fb5f8605c0a86c3b5559890ce38b87d96ad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 10 May 2023 13:35:47 +0300 Subject: [PATCH 81/97] Parse file find queries with extra data --- crates/file_finder/src/file_finder.rs | 112 ++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 17 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f00430feb7..62eae72cc5 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -23,7 +23,7 @@ pub struct FileFinderDelegate { search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, - latest_search_query: String, + latest_search_query: Option, relative_to: Option>, matches: Vec, selected: Option<(usize, Arc)>, @@ -60,6 +60,62 @@ pub enum Event { Dismissed, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct FileSearchQuery { + raw_query: String, + file_path_end: Option, + file_row: Option, + file_column: Option, +} + +impl FileSearchQuery { + fn new(raw_query: String) -> Self { + let fallback_query = Self { + raw_query: raw_query.clone(), + file_path_end: None, + file_row: None, + file_column: None, + }; + + let mut possible_path_and_coordinates = raw_query.as_str().rsplitn(3, ':').fuse(); + match ( + possible_path_and_coordinates.next(), + possible_path_and_coordinates.next(), + possible_path_and_coordinates.next(), + ) { + (Some(column_number_str), Some(row_number_str), Some(file_path_part)) => Self { + file_path_end: Some(file_path_part.len()), + file_row: match row_number_str.parse().ok() { + None => return fallback_query, + row => row, + }, + file_column: match column_number_str.parse().ok() { + None => return fallback_query, + column => column, + }, + raw_query, + }, + (Some(row_number_str), Some(file_path_part), None) => Self { + file_path_end: Some(file_path_part.len()), + file_row: match row_number_str.parse().ok() { + None => return fallback_query, + row => row, + }, + file_column: None, + raw_query, + }, + _no_colons_query => fallback_query, + } + } + + fn path_query(&self) -> &str { + match self.file_path_end { + Some(file_path_end) => &self.raw_query[..file_path_end], + None => &self.raw_query, + } + } +} + impl FileFinderDelegate { fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec, String, Vec) { let path = &path_match.path; @@ -103,7 +159,7 @@ impl FileFinderDelegate { search_count: 0, latest_search_id: 0, latest_search_did_cancel: false, - latest_search_query: String::new(), + latest_search_query: None, relative_to, matches: Vec::new(), selected: None, @@ -111,7 +167,11 @@ impl FileFinderDelegate { } } - fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + fn spawn_search( + &mut self, + query: FileSearchQuery, + cx: &mut ViewContext, + ) -> Task<()> { let relative_to = self.relative_to.clone(); let worktrees = self .project @@ -140,7 +200,7 @@ impl FileFinderDelegate { cx.spawn(|picker, mut cx| async move { let matches = fuzzy::match_path_sets( candidate_sets.as_slice(), - &query, + query.path_query(), relative_to, false, 100, @@ -163,18 +223,18 @@ impl FileFinderDelegate { &mut self, search_id: usize, did_cancel: bool, - query: String, + query: FileSearchQuery, matches: Vec, cx: &mut ViewContext, ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; - if self.latest_search_did_cancel && query == self.latest_search_query { + if self.latest_search_did_cancel && Some(&query) == self.latest_search_query.as_ref() { util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); } else { self.matches = matches; } - self.latest_search_query = query; + self.latest_search_query = Some(query); self.latest_search_did_cancel = did_cancel; cx.notify(); } @@ -209,14 +269,14 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); } - fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { - if query.is_empty() { + fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext) -> Task<()> { + if raw_query.is_empty() { self.latest_search_id = post_inc(&mut self.search_count); self.matches.clear(); cx.notify(); Task::ready(()) } else { - self.spawn_search(query, cx) + self.spawn_search(FileSearchQuery::new(raw_query), cx) } } @@ -230,6 +290,8 @@ impl PickerDelegate for FileFinderDelegate { workspace.update(cx, |workspace, cx| { workspace + // TODO kb need to pass row and column here + // use self.latest_search_query .open_path(project_path.clone(), None, true, cx) .detach_and_log_err(cx); workspace.dismiss_modal(cx); @@ -371,7 +433,7 @@ mod tests { ) }); - let query = "hi".to_string(); + let query = FileSearchQuery::new("hi".to_string()); finder .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) .await; @@ -455,7 +517,10 @@ mod tests { ) }); finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut() + .spawn_search(FileSearchQuery::new("hi".to_string()), cx) + }) .await; finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); } @@ -491,7 +556,10 @@ mod tests { // Even though there is only one worktree, that worktree's filename // is included in the matching, because the worktree is a single file. finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut() + .spawn_search(FileSearchQuery::new("thf".to_string()), cx) + }) .await; cx.read(|cx| { let finder = finder.read(cx); @@ -509,7 +577,10 @@ mod tests { // Since the worktree root is a file, searching for its name followed by a slash does // not match anything. finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut() + .spawn_search(FileSearchQuery::new("thf/".to_string()), cx) + }) .await; finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); } @@ -553,7 +624,10 @@ mod tests { // Run a search that matches two files with the same relative path. finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut() + .spawn_search(FileSearchQuery::new("a.t".to_string()), cx) + }) .await; // Can switch between different matches with the same relative path. @@ -609,7 +683,8 @@ mod tests { finder .update(cx, |f, cx| { - f.delegate_mut().spawn_search("a.txt".into(), cx) + f.delegate_mut() + .spawn_search(FileSearchQuery::new("a.txt".to_string()), cx) }) .await; @@ -651,7 +726,10 @@ mod tests { ) }); finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut() + .spawn_search(FileSearchQuery::new("dir".to_string()), cx) + }) .await; cx.read(|cx| { let finder = finder.read(cx); From 54c1e77aff03eeb7f203256dd760269404faae4f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 10 May 2023 15:03:51 +0300 Subject: [PATCH 82/97] Move the caret to the opened file --- crates/file_finder/src/file_finder.rs | 83 +++++++++++++++++++++------ 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 62eae72cc5..8181d6a1f0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,3 +1,4 @@ +use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use fuzzy::PathMatch; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, @@ -60,12 +61,12 @@ pub enum Event { Dismissed, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] struct FileSearchQuery { raw_query: String, file_path_end: Option, - file_row: Option, - file_column: Option, + file_row: Option, + file_column: Option, } impl FileSearchQuery { @@ -77,30 +78,43 @@ impl FileSearchQuery { file_column: None, }; - let mut possible_path_and_coordinates = raw_query.as_str().rsplitn(3, ':').fuse(); + let mut possible_path_and_coordinates = + // TODO kb go_to_line.rs uses ',' as a separator?? + raw_query.as_str().splitn(3, ':').map(str::trim).fuse(); match ( possible_path_and_coordinates.next(), possible_path_and_coordinates.next(), possible_path_and_coordinates.next(), ) { - (Some(column_number_str), Some(row_number_str), Some(file_path_part)) => Self { + (Some(file_path_part), Some(row_number_str), Some(column_number_str)) + if !row_number_str.is_empty() && !column_number_str.is_empty() => + { + Self { + file_path_end: Some(file_path_part.len()), + file_row: match row_number_str.parse().ok() { + None => return fallback_query, + row => row, + }, + file_column: match column_number_str.parse().ok() { + None => return fallback_query, + column => column, + }, + raw_query, + } + } + (Some(file_path_part), Some(row_number_str), _) if !row_number_str.is_empty() => Self { file_path_end: Some(file_path_part.len()), file_row: match row_number_str.parse().ok() { None => return fallback_query, row => row, }, - file_column: match column_number_str.parse().ok() { - None => return fallback_query, - column => column, - }, + file_column: None, raw_query, }, - (Some(row_number_str), Some(file_path_part), None) => Self { + // Covers inputs like `foo.rs:` trimming all extra colons + (Some(file_path_part), _, _) => Self { file_path_end: Some(file_path_part.len()), - file_row: match row_number_str.parse().ok() { - None => return fallback_query, - row => row, - }, + file_row: None, file_column: None, raw_query, }, @@ -229,7 +243,13 @@ impl FileFinderDelegate { ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; - if self.latest_search_did_cancel && Some(&query) == self.latest_search_query.as_ref() { + if self.latest_search_did_cancel + && Some(query.path_query()) + == self + .latest_search_query + .as_ref() + .map(|query| query.path_query()) + { util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); } else { self.matches = matches; @@ -290,12 +310,39 @@ impl PickerDelegate for FileFinderDelegate { workspace.update(cx, |workspace, cx| { workspace - // TODO kb need to pass row and column here - // use self.latest_search_query .open_path(project_path.clone(), None, true, cx) .detach_and_log_err(cx); + }); + + workspace.update(cx, |workspace, cx| { + if let Some(query) = &self.latest_search_query { + let row = query.file_row; + let column = query.file_column; + if let Some(row) = row { + if let Some(active_editor) = workspace + .active_item(cx) + .and_then(|active_item| active_item.downcast::()) + { + // TODO kb does not open proper lines for the first time + active_editor.update(cx, |active_editor, cx| { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let point = DisplayPoint::new( + row.saturating_sub(1), + column.map(|column| column.saturating_sub(1)).unwrap_or(0), + ) + .to_point(&snapshot); + active_editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + } + } + } + workspace.dismiss_modal(cx); - }) + }); } } } From 0db7f4202ab242584e147c4e8720df6d2dca5d74 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 10 May 2023 21:37:59 +0300 Subject: [PATCH 83/97] Properly place the caret into the window of the file opened co-authored-by: Mikayla Maki --- crates/file_finder/src/file_finder.rs | 65 ++++++++++++++++----------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 8181d6a1f0..3ad777b7fa 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,4 +1,4 @@ -use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; +use editor::{scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor}; use fuzzy::PathMatch; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, @@ -308,41 +308,54 @@ impl PickerDelegate for FileFinderDelegate { path: m.path.clone(), }; - workspace.update(cx, |workspace, cx| { - workspace - .open_path(project_path.clone(), None, true, cx) - .detach_and_log_err(cx); + let open_task = workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) }); - workspace.update(cx, |workspace, cx| { - if let Some(query) = &self.latest_search_query { - let row = query.file_row; - let column = query.file_column; - if let Some(row) = row { - if let Some(active_editor) = workspace - .active_item(cx) - .and_then(|active_item| active_item.downcast::()) - { - // TODO kb does not open proper lines for the first time - active_editor.update(cx, |active_editor, cx| { - let snapshot = active_editor.snapshot(cx).display_snapshot; + let workspace = workspace.downgrade(); + + cx.spawn(|file_finder, mut cx| async move { + let item = open_task.await.log_err()?; + + let (row, col) = file_finder + .read_with(&cx, |file_finder, _| { + file_finder + .delegate() + .latest_search_query + .as_ref() + .map(|query| (query.file_row, query.file_column)) + }) + .log_err() + .flatten()?; + + if let Some(row) = row { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; let point = DisplayPoint::new( row.saturating_sub(1), - column.map(|column| column.saturating_sub(1)).unwrap_or(0), + col.map(|column| column.saturating_sub(1)).unwrap_or(0), ) .to_point(&snapshot); - active_editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); + let point = + snapshot.buffer_snapshot.clip_point(point, Bias::Left); + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); }) - } + .log_err(); } } - workspace.dismiss_modal(cx); - }); + workspace + .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) + .log_err(); + + Some(()) + }) + .detach(); } } } From e9606982e6ed2ecec14b085d2a017cbae8a37cba Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 11 May 2023 11:32:41 +0300 Subject: [PATCH 84/97] Use ':' instead of ',' to separate files, rows and columns --- crates/editor/src/editor.rs | 1 + crates/editor/src/items.rs | 7 ++++++- crates/go_to_line/src/go_to_line.rs | 17 ++++++++++------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b6d44397a9..915bd14786 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -98,6 +98,7 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); +pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d2b9c20803..a99f9c3d08 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -2,6 +2,7 @@ use crate::{ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, + FILE_ROW_COLUMN_DELIMITER, }; use anyhow::{Context, Result}; use collections::HashSet; @@ -1112,7 +1113,11 @@ impl View for CursorPosition { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if let Some(position) = self.position { let theme = &cx.global::().theme.workspace.status_bar; - let mut text = format!("{},{}", position.row + 1, position.column + 1); + let mut text = format!( + "{}{FILE_ROW_COLUMN_DELIMITER}{}", + position.row + 1, + position.column + 1 + ); if self.selected_count > 0 { write!(text, " ({} selected)", self.selected_count).unwrap(); } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 90287e9270..d6db685906 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; +use editor::{ + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor, + FILE_ROW_COLUMN_DELIMITER, +}; use gpui::{ actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity, View, ViewContext, ViewHandle, @@ -97,14 +100,14 @@ impl GoToLine { editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::BufferEdited { .. } => { let line_editor = self.line_editor.read(cx).text(cx); - let mut components = line_editor.trim().split(&[',', ':'][..]); + let mut components = line_editor + .splitn(2, FILE_ROW_COLUMN_DELIMITER) + .map(str::trim) + .fuse(); let row = components.next().and_then(|row| row.parse::().ok()); let column = components.next().and_then(|row| row.parse::().ok()); if let Some(point) = row.map(|row| { - Point::new( - row.saturating_sub(1), - column.map(|column| column.saturating_sub(1)).unwrap_or(0), - ) + Point::new(row.saturating_sub(1), column.unwrap_or(0).saturating_sub(1)) }) { self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; @@ -147,7 +150,7 @@ impl View for GoToLine { let theme = &cx.global::().theme.picker; let label = format!( - "{},{} of {} lines", + "{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines", self.cursor_point.row + 1, self.cursor_point.column + 1, self.max_point.row + 1 From e5bca9c8710ecf70377bc1ca61277fadb0005907 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 11 May 2023 12:17:47 +0300 Subject: [PATCH 85/97] Simplify file-row-column parsing --- crates/file_finder/src/file_finder.rs | 89 +++++++++------------------ 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3ad777b7fa..3d1a9a0c99 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,4 +1,4 @@ -use editor::{scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor}; +use editor::{scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor, FILE_ROW_COLUMN_DELIMITER}; use fuzzy::PathMatch; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, @@ -71,54 +71,20 @@ struct FileSearchQuery { impl FileSearchQuery { fn new(raw_query: String) -> Self { - let fallback_query = Self { - raw_query: raw_query.clone(), - file_path_end: None, - file_row: None, - file_column: None, - }; + let mut components = raw_query + .as_str() + .splitn(3, FILE_ROW_COLUMN_DELIMITER) + .map(str::trim) + .fuse(); + let file_query = components.next().filter(|str| !str.is_empty()); + let file_row = components.next().and_then(|row| row.parse::().ok()); + let file_column = components.next().and_then(|col| col.parse::().ok()); - let mut possible_path_and_coordinates = - // TODO kb go_to_line.rs uses ',' as a separator?? - raw_query.as_str().splitn(3, ':').map(str::trim).fuse(); - match ( - possible_path_and_coordinates.next(), - possible_path_and_coordinates.next(), - possible_path_and_coordinates.next(), - ) { - (Some(file_path_part), Some(row_number_str), Some(column_number_str)) - if !row_number_str.is_empty() && !column_number_str.is_empty() => - { - Self { - file_path_end: Some(file_path_part.len()), - file_row: match row_number_str.parse().ok() { - None => return fallback_query, - row => row, - }, - file_column: match column_number_str.parse().ok() { - None => return fallback_query, - column => column, - }, - raw_query, - } - } - (Some(file_path_part), Some(row_number_str), _) if !row_number_str.is_empty() => Self { - file_path_end: Some(file_path_part.len()), - file_row: match row_number_str.parse().ok() { - None => return fallback_query, - row => row, - }, - file_column: None, - raw_query, - }, - // Covers inputs like `foo.rs:` trimming all extra colons - (Some(file_path_part), _, _) => Self { - file_path_end: Some(file_path_part.len()), - file_row: None, - file_column: None, - raw_query, - }, - _no_colons_query => fallback_query, + Self { + file_path_end: file_query.map(|query| query.len()), + file_row, + file_column, + raw_query, } } @@ -314,19 +280,18 @@ impl PickerDelegate for FileFinderDelegate { let workspace = workspace.downgrade(); - cx.spawn(|file_finder, mut cx| async move { - let item = open_task.await.log_err()?; - - let (row, col) = file_finder - .read_with(&cx, |file_finder, _| { - file_finder - .delegate() - .latest_search_query - .as_ref() - .map(|query| (query.file_row, query.file_column)) - }) - .log_err() - .flatten()?; + if let Some(row) = self + .latest_search_query + .as_ref() + .and_then(|query| query.file_row) + .map(|row| row.saturating_sub(1)) + { + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.file_column) + .unwrap_or(0) + .saturating_sub(1); if let Some(row) = row { if let Some(active_editor) = item.downcast::() { @@ -339,6 +304,8 @@ impl PickerDelegate for FileFinderDelegate { col.map(|column| column.saturating_sub(1)).unwrap_or(0), ) .to_point(&snapshot); + let point = + snapshot.buffer_snapshot.clip_point(point, Bias::Left); let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); editor.change_selections(Some(Autoscroll::center()), cx, |s| { From 477bc8da05c5771bce4189c6393cfb3f01d155c3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 11 May 2023 12:23:05 +0300 Subject: [PATCH 86/97] Make Go To Line to respect column numbers --- crates/go_to_line/src/go_to_line.rs | 41 ++++++++++++++++------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index d6db685906..072ba2f199 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,8 +1,7 @@ use std::sync::Arc; use editor::{ - display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor, - FILE_ROW_COLUMN_DELIMITER, + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor, FILE_ROW_COLUMN_DELIMITER, }; use gpui::{ actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity, @@ -78,15 +77,16 @@ impl GoToLine { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { self.prev_scroll_position.take(); - self.active_editor.update(cx, |active_editor, cx| { - if let Some(rows) = active_editor.highlighted_rows() { + if let Some(point) = self.point_from_query(cx) { + self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; - let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); + let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([position..position]) + s.select_ranges([point..point]) }); - } - }); + }); + } + cx.emit(Event::Dismissed); } @@ -99,16 +99,7 @@ impl GoToLine { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::BufferEdited { .. } => { - let line_editor = self.line_editor.read(cx).text(cx); - let mut components = line_editor - .splitn(2, FILE_ROW_COLUMN_DELIMITER) - .map(str::trim) - .fuse(); - let row = components.next().and_then(|row| row.parse::().ok()); - let column = components.next().and_then(|row| row.parse::().ok()); - if let Some(point) = row.map(|row| { - Point::new(row.saturating_sub(1), column.unwrap_or(0).saturating_sub(1)) - }) { + if let Some(point) = self.point_from_query(cx) { self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); @@ -123,6 +114,20 @@ impl GoToLine { _ => {} } } + + fn point_from_query(&self, cx: &ViewContext) -> Option { + let line_editor = self.line_editor.read(cx).text(cx); + let mut components = line_editor + .splitn(2, FILE_ROW_COLUMN_DELIMITER) + .map(str::trim) + .fuse(); + let row = components.next().and_then(|row| row.parse::().ok())?; + let column = components.next().and_then(|col| col.parse::().ok()); + Some(Point::new( + row.saturating_sub(1), + column.unwrap_or(0).saturating_sub(1), + )) + } } impl Entity for GoToLine { From 89fe5c6b0990f484032c0ff22f3b6059664326d8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 11 May 2023 11:45:29 +0300 Subject: [PATCH 87/97] Test caret selection in file finder co-authored-by: Max --- Cargo.lock | 1 + crates/editor/src/hover_popover.rs | 3 +- crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/file_finder.rs | 216 +++++++++++++++++++++++--- 4 files changed, 198 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e009cfd342..871a95c190 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2185,6 +2185,7 @@ dependencies = [ "project", "serde_json", "settings", + "text", "theme", "util", "workspace", diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 438c662ed1..85edb97da4 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1006,8 +1006,7 @@ mod tests { .zip(expected_styles.iter().cloned()) .collect::>(); assert_eq!( - rendered.text, - dbg!(expected_text), + rendered.text, expected_text, "wrong text for input {blocks:?}" ); assert_eq!( diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 30a4650ad7..8b99cc5856 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -16,6 +16,7 @@ menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } +text = { path = "../text" } util = { path = "../util" } theme = { path = "../theme" } workspace = { path = "../workspace" } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3d1a9a0c99..6447706358 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,4 +1,4 @@ -use editor::{scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor, FILE_ROW_COLUMN_DELIMITER}; +use editor::{scroll::autoscroll::Autoscroll, Bias, Editor, FILE_ROW_COLUMN_DELIMITER}; use fuzzy::PathMatch; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, @@ -13,6 +13,7 @@ use std::{ Arc, }, }; +use text::Point; use util::{post_inc, ResultExt}; use workspace::Workspace; @@ -64,7 +65,7 @@ pub enum Event { #[derive(Debug, Clone)] struct FileSearchQuery { raw_query: String, - file_path_end: Option, + file_query_end: Option, file_row: Option, file_column: Option, } @@ -81,7 +82,9 @@ impl FileSearchQuery { let file_column = components.next().and_then(|col| col.parse::().ok()); Self { - file_path_end: file_query.map(|query| query.len()), + file_query_end: file_query + .filter(|_| file_row.is_some()) + .map(|query| query.len()), file_row, file_column, raw_query, @@ -89,7 +92,7 @@ impl FileSearchQuery { } fn path_query(&self) -> &str { - match self.file_path_end { + match self.file_query_end { Some(file_path_end) => &self.raw_query[..file_path_end], None => &self.raw_query, } @@ -280,32 +283,28 @@ impl PickerDelegate for FileFinderDelegate { let workspace = workspace.downgrade(); - if let Some(row) = self + let row = self .latest_search_query .as_ref() .and_then(|query| query.file_row) - .map(|row| row.saturating_sub(1)) - { - let col = self - .latest_search_query - .as_ref() - .and_then(|query| query.file_column) - .unwrap_or(0) - .saturating_sub(1); - + .map(|row| row.saturating_sub(1)); + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.file_column) + .unwrap_or(0) + .saturating_sub(1); + cx.spawn(|_, mut cx| async move { + let item = open_task.await.log_err()?; if let Some(row) = row { if let Some(active_editor) = item.downcast::() { active_editor .downgrade() .update(&mut cx, |editor, cx| { let snapshot = editor.snapshot(cx).display_snapshot; - let point = DisplayPoint::new( - row.saturating_sub(1), - col.map(|column| column.saturating_sub(1)).unwrap_or(0), - ) - .to_point(&snapshot); - let point = - snapshot.buffer_snapshot.clip_point(point, Bias::Left); + let point = snapshot + .buffer_snapshot + .clip_point(Point::new(row, col), Bias::Left); let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); editor.change_selections(Some(Autoscroll::center()), cx, |s| { @@ -359,6 +358,7 @@ impl PickerDelegate for FileFinderDelegate { mod tests { use super::*; use editor::Editor; + use gpui::executor::Deterministic; use menu::{Confirm, SelectNext}; use serde_json::json; use workspace::{AppState, Workspace}; @@ -426,6 +426,180 @@ mod tests { }); } + #[gpui::test] + async fn test_row_column_numbers_query_inside_file( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = cx.update(|cx| { + super::init(cx); + editor::init(cx); + AppState::test(cx) + }); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + cx.dispatch_action(window_id, Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + + let file_query = &first_file_name[..3]; + let file_row = 1; + let file_column = 3; + assert!(file_column <= first_file_contents.len()); + let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(query_inside_file.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let finder = finder.delegate(); + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.raw_query, query_inside_file); + assert_eq!(latest_search_query.file_row, Some(file_row)); + assert_eq!(latest_search_query.file_column, Some(file_column as u32)); + assert_eq!(latest_search_query.file_query_end, Some(file_query.len())); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window_id, SelectNext); + cx.dispatch_action(window_id, Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + deterministic.advance_clock(std::time::Duration::from_secs(2)); + deterministic.start_waiting(); + deterministic.finish_waiting(); + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(file_row, caret_selection.start.row + 1, + "Query inside file should get caret with the same focus row"); + assert_eq!(file_column, caret_selection.start.column as usize + 1, + "Query inside file should get caret with the same focus column"); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_outside_file( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = cx.update(|cx| { + super::init(cx); + editor::init(cx); + AppState::test(cx) + }); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + cx.dispatch_action(window_id, Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + + let file_query = &first_file_name[..3]; + let file_row = 200; + let file_column = 300; + assert!(file_column > first_file_contents.len()); + let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(query_outside_file.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let finder = finder.delegate(); + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.raw_query, query_outside_file); + assert_eq!(latest_search_query.file_row, Some(file_row)); + assert_eq!(latest_search_query.file_column, Some(file_column as u32)); + assert_eq!(latest_search_query.file_query_end, Some(file_query.len())); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window_id, SelectNext); + cx.dispatch_action(window_id, Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + deterministic.advance_clock(std::time::Duration::from_secs(2)); + deterministic.start_waiting(); + deterministic.finish_waiting(); + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(0, caret_selection.start.row, + "Excessive rows (as in query outside file borders) should get trimmed to last file row"); + assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, + "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); + }); + } + #[gpui::test] async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) { let app_state = cx.update(AppState::test); From d7193521520d08b3f166434d952fc67d0b990c91 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 12 May 2023 16:52:07 +0300 Subject: [PATCH 88/97] Unify path:row:column parsing, use it in CLI --- Cargo.lock | 2 + crates/cli/Cargo.toml | 1 + crates/cli/src/main.rs | 30 +++++-- crates/editor/src/editor.rs | 1 - crates/editor/src/items.rs | 3 +- crates/file_finder/src/file_finder.rs | 112 +++++++++++++------------- crates/go_to_line/Cargo.toml | 1 + crates/go_to_line/src/go_to_line.rs | 5 +- crates/util/src/paths.rs | 37 +++++++++ 9 files changed, 127 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 871a95c190..41c3eab821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1097,6 +1097,7 @@ dependencies = [ "plist", "serde", "serde_derive", + "util", ] [[package]] @@ -2675,6 +2676,7 @@ dependencies = [ "postage", "settings", "text", + "util", "workspace", ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9b8009dd69..2b4a375a5b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,6 +19,7 @@ dirs = "3.0" ipc-channel = "0.16" serde.workspace = true serde_derive.workspace = true +util = { path = "../util" } [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 0ae4d2477e..80ec2bbf99 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -16,6 +16,7 @@ use std::{ path::{Path, PathBuf}, ptr, }; +use util::paths::PathLikeWithPosition; #[derive(Parser)] #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] @@ -24,8 +25,11 @@ struct Args { #[clap(short, long)] wait: bool, /// A sequence of space-separated paths that you want to open. - #[clap()] - paths: Vec, + /// + /// Use `path:line:row` syntax to open a file at a specific location. + /// Non-existing paths and directories will ignore `:line:row` suffix. + #[clap(value_parser = parse_path_with_position)] + paths_with_position: Vec>, /// Print Zed's version and the app path. #[clap(short, long)] version: bool, @@ -34,6 +38,14 @@ struct Args { bundle_path: Option, } +fn parse_path_with_position( + argument_str: &str, +) -> Result, std::convert::Infallible> { + PathLikeWithPosition::parse_str(argument_str, |path_str| { + Ok(Path::new(path_str).to_path_buf()) + }) +} + #[derive(Debug, Deserialize)] struct InfoPlist { #[serde(rename = "CFBundleShortVersionString")] @@ -50,7 +62,11 @@ fn main() -> Result<()> { return Ok(()); } - for path in args.paths.iter() { + for path in args + .paths_with_position + .iter() + .map(|path_with_position| &path_with_position.path_like) + { if !path.exists() { touch(path.as_path())?; } @@ -60,9 +76,13 @@ fn main() -> Result<()> { tx.send(CliRequest::Open { paths: args - .paths + .paths_with_position .into_iter() - .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error))) + // TODO kb continue sendint path with the position further + .map(|path_with_position| path_with_position.path_like) + .map(|path| { + fs::canonicalize(&path).with_context(|| format!("path {path:?} canonicalization")) + }) .collect::>>()?, wait: args.wait, })?; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 915bd14786..b6d44397a9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -98,7 +98,6 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a99f9c3d08..1a9fcc963e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -2,7 +2,6 @@ use crate::{ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, - FILE_ROW_COLUMN_DELIMITER, }; use anyhow::{Context, Result}; use collections::HashSet; @@ -28,7 +27,7 @@ use std::{ path::{Path, PathBuf}, }; use text::Selection; -use util::{ResultExt, TryFutureExt}; +use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 6447706358..fae9bd565c 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,4 +1,4 @@ -use editor::{scroll::autoscroll::Autoscroll, Bias, Editor, FILE_ROW_COLUMN_DELIMITER}; +use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::PathMatch; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, @@ -14,7 +14,7 @@ use std::{ }, }; use text::Point; -use util::{post_inc, ResultExt}; +use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::Workspace; pub type FileFinder = Picker; @@ -25,7 +25,7 @@ pub struct FileFinderDelegate { search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, - latest_search_query: Option, + latest_search_query: Option>, relative_to: Option>, matches: Vec, selected: Option<(usize, Arc)>, @@ -66,31 +66,9 @@ pub enum Event { struct FileSearchQuery { raw_query: String, file_query_end: Option, - file_row: Option, - file_column: Option, } impl FileSearchQuery { - fn new(raw_query: String) -> Self { - let mut components = raw_query - .as_str() - .splitn(3, FILE_ROW_COLUMN_DELIMITER) - .map(str::trim) - .fuse(); - let file_query = components.next().filter(|str| !str.is_empty()); - let file_row = components.next().and_then(|row| row.parse::().ok()); - let file_column = components.next().and_then(|col| col.parse::().ok()); - - Self { - file_query_end: file_query - .filter(|_| file_row.is_some()) - .map(|query| query.len()), - file_row, - file_column, - raw_query, - } - } - fn path_query(&self) -> &str { match self.file_query_end { Some(file_path_end) => &self.raw_query[..file_path_end], @@ -152,7 +130,7 @@ impl FileFinderDelegate { fn spawn_search( &mut self, - query: FileSearchQuery, + query: PathLikeWithPosition, cx: &mut ViewContext, ) -> Task<()> { let relative_to = self.relative_to.clone(); @@ -183,7 +161,7 @@ impl FileFinderDelegate { cx.spawn(|picker, mut cx| async move { let matches = fuzzy::match_path_sets( candidate_sets.as_slice(), - query.path_query(), + query.path_like.path_query(), relative_to, false, 100, @@ -206,18 +184,18 @@ impl FileFinderDelegate { &mut self, search_id: usize, did_cancel: bool, - query: FileSearchQuery, + query: PathLikeWithPosition, matches: Vec, cx: &mut ViewContext, ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; if self.latest_search_did_cancel - && Some(query.path_query()) + && Some(query.path_like.path_query()) == self .latest_search_query .as_ref() - .map(|query| query.path_query()) + .map(|query| query.path_like.path_query()) { util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); } else { @@ -265,7 +243,19 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); Task::ready(()) } else { - self.spawn_search(FileSearchQuery::new(raw_query), cx) + let raw_query = &raw_query; + let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: raw_query.to_owned(), + file_query_end: if path_like_str == raw_query { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .expect("infallible"); + self.spawn_search(query, cx) } } @@ -286,12 +276,12 @@ impl PickerDelegate for FileFinderDelegate { let row = self .latest_search_query .as_ref() - .and_then(|query| query.file_row) + .and_then(|query| query.row) .map(|row| row.saturating_sub(1)); let col = self .latest_search_query .as_ref() - .and_then(|query| query.file_column) + .and_then(|query| query.column) .unwrap_or(0) .saturating_sub(1); cx.spawn(|_, mut cx| async move { @@ -477,10 +467,13 @@ mod tests { .latest_search_query .as_ref() .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.raw_query, query_inside_file); - assert_eq!(latest_search_query.file_row, Some(file_row)); - assert_eq!(latest_search_query.file_column, Some(file_column as u32)); - assert_eq!(latest_search_query.file_query_end, Some(file_query.len())); + assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); @@ -564,10 +557,13 @@ mod tests { .latest_search_query .as_ref() .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.raw_query, query_outside_file); - assert_eq!(latest_search_query.file_row, Some(file_row)); - assert_eq!(latest_search_query.file_column, Some(file_column as u32)); - assert_eq!(latest_search_query.file_query_end, Some(file_query.len())); + assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); @@ -634,7 +630,7 @@ mod tests { ) }); - let query = FileSearchQuery::new("hi".to_string()); + let query = test_path_like("hi"); finder .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) .await; @@ -719,8 +715,7 @@ mod tests { }); finder .update(cx, |f, cx| { - f.delegate_mut() - .spawn_search(FileSearchQuery::new("hi".to_string()), cx) + f.delegate_mut().spawn_search(test_path_like("hi"), cx) }) .await; finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); @@ -758,8 +753,7 @@ mod tests { // is included in the matching, because the worktree is a single file. finder .update(cx, |f, cx| { - f.delegate_mut() - .spawn_search(FileSearchQuery::new("thf".to_string()), cx) + f.delegate_mut().spawn_search(test_path_like("thf"), cx) }) .await; cx.read(|cx| { @@ -779,8 +773,7 @@ mod tests { // not match anything. finder .update(cx, |f, cx| { - f.delegate_mut() - .spawn_search(FileSearchQuery::new("thf/".to_string()), cx) + f.delegate_mut().spawn_search(test_path_like("thf/"), cx) }) .await; finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); @@ -826,8 +819,7 @@ mod tests { // Run a search that matches two files with the same relative path. finder .update(cx, |f, cx| { - f.delegate_mut() - .spawn_search(FileSearchQuery::new("a.t".to_string()), cx) + f.delegate_mut().spawn_search(test_path_like("a.t"), cx) }) .await; @@ -884,8 +876,7 @@ mod tests { finder .update(cx, |f, cx| { - f.delegate_mut() - .spawn_search(FileSearchQuery::new("a.txt".to_string()), cx) + f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) }) .await; @@ -928,8 +919,7 @@ mod tests { }); finder .update(cx, |f, cx| { - f.delegate_mut() - .spawn_search(FileSearchQuery::new("dir".to_string()), cx) + f.delegate_mut().spawn_search(test_path_like("dir"), cx) }) .await; cx.read(|cx| { @@ -937,4 +927,18 @@ mod tests { assert_eq!(finder.delegate().matches.len(), 0); }); } + + fn test_path_like(test_str: &str) -> PathLikeWithPosition { + PathLikeWithPosition::parse_str(test_str, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: test_str.to_owned(), + file_query_end: if path_like_str == test_str { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .unwrap() + } } diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index f279aca569..8f99aa366c 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -16,3 +16,4 @@ settings = { path = "../settings" } text = { path = "../text" } workspace = { path = "../workspace" } postage.workspace = true +util = { path = "../util" } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 072ba2f199..967f17b794 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,8 +1,6 @@ use std::sync::Arc; -use editor::{ - display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor, FILE_ROW_COLUMN_DELIMITER, -}; +use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity, View, ViewContext, ViewHandle, @@ -10,6 +8,7 @@ use gpui::{ use menu::{Cancel, Confirm}; use settings::Settings; use text::{Bias, Point}; +use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::{Modal, Workspace}; actions!(go_to_line, [Toggle]); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index a324b21a31..280d3cad02 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -70,3 +70,40 @@ pub fn compact(path: &Path) -> PathBuf { path.to_path_buf() } } + +pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; + +#[derive(Debug, Clone)] +pub struct PathLikeWithPosition

{ + pub path_like: P, + pub row: Option, + pub column: Option, +} + +impl

PathLikeWithPosition

{ + pub fn parse_str(s: &str, parse_path_like_str: F) -> Result + where + F: Fn(&str) -> Result, + { + let mut components = s.splitn(3, FILE_ROW_COLUMN_DELIMITER).map(str::trim).fuse(); + let path_like_str = components.next().filter(|str| !str.is_empty()); + let row = components.next().and_then(|row| row.parse::().ok()); + let column = components + .next() + .filter(|_| row.is_some()) + .and_then(|col| col.parse::().ok()); + + Ok(match path_like_str { + Some(path_like_str) => Self { + path_like: parse_path_like_str(path_like_str)?, + row, + column, + }, + None => Self { + path_like: parse_path_like_str(s)?, + row: None, + column: None, + }, + }) + } +} From 628558aa3901caf1c44bd62fb85ec889c4507028 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 12 May 2023 18:13:28 +0300 Subject: [PATCH 89/97] Attempt to open rows and columns from CLI input --- crates/cli/src/cli.rs | 7 ++- crates/cli/src/main.rs | 13 ++-- crates/file_finder/src/file_finder.rs | 2 - crates/util/src/paths.rs | 15 ++++- crates/zed/src/main.rs | 86 ++++++++++++++++++++------- 5 files changed, 91 insertions(+), 32 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index de7b14e142..8281bcb651 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -1,6 +1,7 @@ pub use ipc_channel::ipc; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use util::paths::PathLikeWithPosition; #[derive(Serialize, Deserialize)] pub struct IpcHandshake { @@ -10,7 +11,11 @@ pub struct IpcHandshake { #[derive(Debug, Serialize, Deserialize)] pub enum CliRequest { - Open { paths: Vec, wait: bool }, + Open { + // TODO kb old cli won't be able to communicate to new Zed with this change + paths: Vec>, + wait: bool, + }, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 80ec2bbf99..ff7a65c2fc 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -21,7 +21,7 @@ use util::paths::PathLikeWithPosition; #[derive(Parser)] #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] struct Args { - /// Wait for all of the given paths to be closed before exiting. + /// Wait for all of the given paths to be opened/closed before exiting. #[clap(short, long)] wait: bool, /// A sequence of space-separated paths that you want to open. @@ -78,12 +78,13 @@ fn main() -> Result<()> { paths: args .paths_with_position .into_iter() - // TODO kb continue sendint path with the position further - .map(|path_with_position| path_with_position.path_like) - .map(|path| { - fs::canonicalize(&path).with_context(|| format!("path {path:?} canonicalization")) + .map(|path_with_position| { + path_with_position.convert_path(|path| { + fs::canonicalize(&path) + .with_context(|| format!("path {path:?} canonicalization")) + }) }) - .collect::>>()?, + .collect::>()?, wait: args.wait, })?; diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index fae9bd565c..063067891a 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -295,8 +295,6 @@ impl PickerDelegate for FileFinderDelegate { let point = snapshot .buffer_snapshot .clip_point(Point::new(row, col), Bias::Left); - let point = - snapshot.buffer_snapshot.clip_point(point, Bias::Left); editor.change_selections(Some(Autoscroll::center()), cx, |s| { s.select_ranges([point..point]) }); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 280d3cad02..8a0c91aba6 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; + lazy_static::lazy_static! { pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); @@ -73,7 +75,7 @@ pub fn compact(path: &Path) -> PathBuf { pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct PathLikeWithPosition

{ pub path_like: P, pub row: Option, @@ -106,4 +108,15 @@ impl

PathLikeWithPosition

{ }, }) } + + pub fn convert_path( + self, + mapping: impl FnOnce(P) -> Result, + ) -> Result, E> { + Ok(PathLikeWithPosition { + path_like: mapping(self.path_like)?, + row: self.row, + column: self.column, + }) + } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 60a2fc66be..369a657527 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -10,7 +10,7 @@ use cli::{ }; use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; -use editor::Editor; +use editor::{scroll::autoscroll::Autoscroll, Editor}; use futures::{ channel::{mpsc, oneshot}, FutureExt, SinkExt, StreamExt, @@ -30,6 +30,7 @@ use settings::{ use simplelog::ConfigBuilder; use smol::process::Command; use std::{ + collections::HashMap, env, ffi::OsStr, fs::OpenOptions, @@ -44,7 +45,9 @@ use std::{ thread, time::Duration, }; +use sum_tree::Bias; use terminal_view::{get_working_directory, TerminalView}; +use text::Point; use util::http::{self, HttpClient}; use welcome::{show_welcome_experience, FIRST_OPEN}; @@ -678,13 +681,29 @@ async fn handle_cli_connection( if let Some(request) = requests.next().await { match request { CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::new(); + let paths = if paths.is_empty() { workspace::last_opened_workspace_paths() .await .map(|location| location.paths().to_vec()) - .unwrap_or(paths) + .unwrap_or_default() } else { paths + .into_iter() + .map(|path_with_position| { + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + path + }) + .collect() }; let mut errored = false; @@ -694,11 +713,37 @@ async fn handle_cli_connection( { Ok((workspace, items)) => { let mut item_release_futures = Vec::new(); - cx.update(|cx| { - for (item, path) in items.into_iter().zip(&paths) { - match item { - Some(Ok(item)) => { - let released = oneshot::channel(); + + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(item)) => { + if let Some(point) = caret_positions.remove(path) { + // TODO kb does not work + log::info!("@@@@@@@@ {path:?}@{point:?}"); + if let Some(active_editor) = item.downcast::() { + log::info!("@@@@@@@@ editor"); + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + log::info!("@@@@@@@@ update"); + let snapshot = + editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(point, Bias::Left); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + log::info!("@@@@@@@@ finished"); + }) + .log_err(); + } + } + + let released = oneshot::channel(); + cx.update(|cx| { item.on_release( cx, Box::new(move |_| { @@ -706,23 +751,20 @@ async fn handle_cli_connection( }), ) .detach(); - item_release_futures.push(released.1); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: format!( - "error opening {:?}: {}", - path, err - ), - }) - .log_err(); - errored = true; - } - None => {} + }); + item_release_futures.push(released.1); } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", path, err), + }) + .log_err(); + errored = true; + } + None => {} } - }); + } if wait { let background = cx.background(); From 106064c73402c035b92028aecb4b0abf17e1e27c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 13 May 2023 11:39:35 +0300 Subject: [PATCH 90/97] Do not break Zed & Zed CLI compatibility --- crates/cli/src/cli.rs | 13 ++++++------- crates/cli/src/main.rs | 5 +++-- crates/util/src/paths.rs | 16 ++++++++++++++++ crates/zed/src/main.rs | 20 ++++++++++++++++---- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 8281bcb651..3a0abbaec7 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -1,7 +1,5 @@ pub use ipc_channel::ipc; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use util::paths::PathLikeWithPosition; #[derive(Serialize, Deserialize)] pub struct IpcHandshake { @@ -11,11 +9,12 @@ pub struct IpcHandshake { #[derive(Debug, Serialize, Deserialize)] pub enum CliRequest { - Open { - // TODO kb old cli won't be able to communicate to new Zed with this change - paths: Vec>, - wait: bool, - }, + // The filed is named `path` for compatibility, but now CLI can request + // opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`. + // + // Since Zed CLI has to be installed separately, there can be situations when old CLI is + // querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later. + Open { paths: Vec, wait: bool }, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index ff7a65c2fc..d4b75f9533 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -79,10 +79,11 @@ fn main() -> Result<()> { .paths_with_position .into_iter() .map(|path_with_position| { - path_with_position.convert_path(|path| { + let path_with_position = path_with_position.convert_path(|path| { fs::canonicalize(&path) .with_context(|| format!("path {path:?} canonicalization")) - }) + })?; + Ok(path_with_position.to_string(|path| path.display().to_string())) }) .collect::>()?, wait: args.wait, diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 8a0c91aba6..96311fabf8 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -119,4 +119,20 @@ impl

PathLikeWithPosition

{ column: self.column, }) } + + pub fn to_string(&self, path_like_to_string: F) -> String + where + F: Fn(&P) -> String, + { + let path_like_string = path_like_to_string(&self.path_like); + if let Some(row) = self.row { + if let Some(column) = self.column { + format!("{path_like_string}:{row}:{column}") + } else { + format!("{path_like_string}:{row}") + } + } else { + path_like_string + } + } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 369a657527..03fdbf7067 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -37,7 +37,7 @@ use std::{ io::Write as _, os::unix::prelude::OsStrExt, panic, - path::PathBuf, + path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering}, Arc, Weak, @@ -48,7 +48,10 @@ use std::{ use sum_tree::Bias; use terminal_view::{get_working_directory, TerminalView}; use text::Point; -use util::http::{self, HttpClient}; +use util::{ + http::{self, HttpClient}, + paths::PathLikeWithPosition, +}; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; @@ -691,7 +694,16 @@ async fn handle_cli_connection( } else { paths .into_iter() - .map(|path_with_position| { + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); let path = path_with_position.path_like; if let Some(row) = path_with_position.row { if path.is_file() { @@ -701,7 +713,7 @@ async fn handle_cli_connection( caret_positions.insert(path.clone(), Point::new(row, col)); } } - path + Some(path) }) .collect() }; From 0c6f1038996a42d217c2a5e5b911eb06a669744a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 16 May 2023 00:18:31 +0300 Subject: [PATCH 91/97] Return proper items on workspace restoration. co-authored-by: Mikayla --- crates/workspace/src/dock.rs | 7 +- crates/workspace/src/persistence/model.rs | 29 +- crates/workspace/src/workspace.rs | 338 ++++++++++++++-------- crates/zed/src/main.rs | 4 + 4 files changed, 243 insertions(+), 135 deletions(-) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 7efcb7f9d3..a1fce13df4 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -462,7 +462,6 @@ mod tests { let (_, _workspace) = cx.add_window(|cx| { Workspace::new( - Some(serialized_workspace), 0, project.clone(), Arc::new(AppState { @@ -480,6 +479,11 @@ mod tests { ) }); + cx.update(|cx| { + Workspace::load_workspace(_workspace.downgrade(), serialized_workspace, Vec::new(), cx) + }) + .await; + cx.foreground().run_until_parked(); //Should terminate } @@ -605,7 +609,6 @@ mod tests { let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| { Workspace::new( - None, 0, project.clone(), Arc::new(AppState { diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index a92c369e7a..46a8ab49b2 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,5 +1,6 @@ use crate::{ - dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId, + dock::DockPosition, item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, + WorkspaceId, }; use anyhow::{anyhow, Context, Result}; use async_recursion::async_recursion; @@ -97,17 +98,23 @@ impl SerializedPaneGroup { workspace_id: WorkspaceId, workspace: &WeakViewHandle, cx: &mut AsyncAppContext, - ) -> Option<(Member, Option>)> { + ) -> Option<( + Member, + Option>, + Vec>>, + )> { match self { SerializedPaneGroup::Group { axis, children } => { let mut current_active_pane = None; let mut members = Vec::new(); + let mut items = Vec::new(); for child in children { - if let Some((new_member, active_pane)) = child + if let Some((new_member, active_pane, new_items)) = child .deserialize(project, workspace_id, workspace, cx) .await { members.push(new_member); + items.extend(new_items); current_active_pane = current_active_pane.or(active_pane); } } @@ -117,7 +124,7 @@ impl SerializedPaneGroup { } if members.len() == 1 { - return Some((members.remove(0), current_active_pane)); + return Some((members.remove(0), current_active_pane, items)); } Some(( @@ -126,6 +133,7 @@ impl SerializedPaneGroup { members, }), current_active_pane, + items, )) } SerializedPaneGroup::Pane(serialized_pane) => { @@ -133,7 +141,7 @@ impl SerializedPaneGroup { .update(cx, |workspace, cx| workspace.add_pane(cx).downgrade()) .log_err()?; let active = serialized_pane.active; - serialized_pane + let new_items = serialized_pane .deserialize_to(project, &pane, workspace_id, workspace, cx) .await .log_err()?; @@ -143,7 +151,7 @@ impl SerializedPaneGroup { .log_err()? { let pane = pane.upgrade(cx)?; - Some((Member::Pane(pane.clone()), active.then(|| pane))) + Some((Member::Pane(pane.clone()), active.then(|| pane), new_items)) } else { let pane = pane.upgrade(cx)?; workspace @@ -174,7 +182,8 @@ impl SerializedPane { workspace_id: WorkspaceId, workspace: &WeakViewHandle, cx: &mut AsyncAppContext, - ) -> Result<()> { + ) -> Result>>> { + let mut items = Vec::new(); let mut active_item_index = None; for (index, item) in self.children.iter().enumerate() { let project = project.clone(); @@ -192,6 +201,10 @@ impl SerializedPane { .await .log_err(); + items.push(item_handle.clone()); + + log::info!("ACTUALLY SHOWN ITEMS: {:?}", &item_handle); + if let Some(item_handle) = item_handle { workspace.update(cx, |workspace, cx| { let pane_handle = pane_handle @@ -213,7 +226,7 @@ impl SerializedPane { })?; } - anyhow::Ok(()) + anyhow::Ok(items) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6350b43415..fe8ea65697 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -82,7 +82,7 @@ use status_bar::StatusBar; pub use status_bar::StatusItemView; use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{paths, ResultExt}; +use util::{async_iife, paths, ResultExt}; lazy_static! { static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") @@ -493,7 +493,6 @@ struct FollowerState { impl Workspace { pub fn new( - serialized_workspace: Option, workspace_id: WorkspaceId, project: ModelHandle, app_state: Arc, @@ -659,16 +658,6 @@ impl Workspace { this.project_remote_id_changed(project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); - if let Some(serialized_workspace) = serialized_workspace { - cx.defer(move |_, cx| { - Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx) - }); - } else if project.read(cx).is_local() { - if cx.global::().default_dock_anchor != DockAnchor::Expanded { - Dock::show(&mut this, false, cx); - } - } - this } @@ -690,8 +679,7 @@ impl Workspace { ); cx.spawn(|mut cx| async move { - let mut serialized_workspace = - persistence::DB.workspace_for_roots(&abs_paths.as_slice()); + let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice()); let paths_to_open = serialized_workspace .as_ref() @@ -700,8 +688,9 @@ impl Workspace { // Get project paths for all of the abs_paths let mut worktree_roots: HashSet> = Default::default(); - let mut project_paths = Vec::new(); - for path in paths_to_open.iter() { + let mut project_paths: Vec<(PathBuf, Option)> = + Vec::with_capacity(paths_to_open.len()); + for path in paths_to_open.iter().cloned() { if let Some((worktree, project_entry)) = cx .update(|cx| { Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) @@ -710,9 +699,9 @@ impl Workspace { .log_err() { worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path())); - project_paths.push(Some(project_entry)); + project_paths.push((path, Some(project_entry))); } else { - project_paths.push(None); + project_paths.push((path, None)); } } @@ -732,27 +721,17 @@ impl Workspace { )) }); - let build_workspace = - |cx: &mut ViewContext, - serialized_workspace: Option| { - let mut workspace = Workspace::new( - serialized_workspace, - workspace_id, - project_handle.clone(), - app_state.clone(), - cx, - ); - (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - workspace - }; + let build_workspace = |cx: &mut ViewContext| { + let mut workspace = + Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx); + (app_state.initialize_workspace)(&mut workspace, &app_state, cx); + + workspace + }; let workspace = requesting_window_id .and_then(|window_id| { - cx.update(|cx| { - cx.replace_root_view(window_id, |cx| { - build_workspace(cx, serialized_workspace.take()) - }) - }) + cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx))) }) .unwrap_or_else(|| { let (bounds, display) = if let Some(bounds) = window_bounds_override { @@ -790,44 +769,21 @@ impl Workspace { // Use the serialized workspace to construct the new window cx.add_window( (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - |cx| build_workspace(cx, serialized_workspace), + |cx| build_workspace(cx), ) .1 }); let workspace = workspace.downgrade(); notify_if_database_failed(&workspace, &mut cx); - - // Call open path for each of the project paths - // (this will bring them to the front if they were in the serialized workspace) - debug_assert!(paths_to_open.len() == project_paths.len()); - let tasks = paths_to_open - .iter() - .cloned() - .zip(project_paths.into_iter()) - .map(|(abs_path, project_path)| { - let workspace = workspace.clone(); - cx.spawn(|mut cx| { - let fs = app_state.fs.clone(); - async move { - let project_path = project_path?; - if fs.is_file(&abs_path).await { - Some( - workspace - .update(&mut cx, |workspace, cx| { - workspace.open_path(project_path, None, true, cx) - }) - .log_err()? - .await, - ) - } else { - None - } - } - }) - }); - - let opened_items = futures::future::join_all(tasks.into_iter()).await; + let opened_items = open_items( + serialized_workspace, + &workspace, + project_paths, + app_state, + cx, + ) + .await; (workspace, opened_items) }) @@ -2536,13 +2492,15 @@ impl Workspace { } } - fn load_from_serialized_workspace( + pub(crate) fn load_workspace( workspace: WeakViewHandle, serialized_workspace: SerializedWorkspace, + paths_to_open: Vec>, cx: &mut AppContext, - ) { + ) -> Task, anyhow::Error>>>> { cx.spawn(|mut cx| async move { - let (project, dock_pane_handle, old_center_pane) = + let result = async_iife! {{ + let (project, dock_pane_handle, old_center_pane) = workspace.read_with(&cx, |workspace, _| { ( workspace.project().clone(), @@ -2551,74 +2509,109 @@ impl Workspace { ) })?; - serialized_workspace - .dock_pane - .deserialize_to( + let dock_items = serialized_workspace + .dock_pane + .deserialize_to( &project, &dock_pane_handle, serialized_workspace.id, &workspace, &mut cx, - ) - .await?; + ) + .await?; - // Traverse the splits tree and add to things - let center_group = serialized_workspace + // Traverse the splits tree and add to things + let something = serialized_workspace .center_group .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) .await; - // Remove old panes from workspace panes list - workspace.update(&mut cx, |workspace, cx| { - if let Some((center_group, active_pane)) = center_group { - workspace.remove_panes(workspace.center.root.clone(), cx); - - // Swap workspace center group - workspace.center = PaneGroup::with_root(center_group); - - // Change the focus to the workspace first so that we retrigger focus in on the pane. - cx.focus_self(); - - if let Some(active_pane) = active_pane { - cx.focus(&active_pane); - } else { - cx.focus(workspace.panes.last().unwrap()); - } - } else { - let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); - if let Some(old_center_handle) = old_center_handle { - cx.focus(&old_center_handle) - } else { - cx.focus_self() - } + let mut center_items = None; + let mut center_group = None; + if let Some((group, active_pane, items)) = something { + center_items = Some(items); + center_group = Some((group, active_pane)) } - if workspace.left_sidebar().read(cx).is_open() - != serialized_workspace.left_sidebar_open - { - workspace.toggle_sidebar(SidebarSide::Left, cx); - } + let resulting_list = cx.read(|cx| { + let mut opened_items = center_items + .unwrap_or_default() + .into_iter() + .chain(dock_items.into_iter()) + .filter_map(|item| { + let item = item?; + let project_path = item.project_path(cx)?; + Some((project_path, item)) + }) + .collect::>(); - // Note that without after_window, the focus_self() and - // the focus the dock generates start generating alternating - // focus due to the deferred execution each triggering each other - cx.after_window_update(move |workspace, cx| { - Dock::set_dock_position( - workspace, - serialized_workspace.dock_position, - false, - cx, - ); + paths_to_open + .into_iter() + .map(|path_to_open| { + path_to_open.map(|path_to_open| { + Ok(opened_items.remove(&path_to_open)) + }) + .transpose() + .map(|item| item.flatten()) + .transpose() + }) + .collect::>() }); - cx.notify(); - })?; + // Remove old panes from workspace panes list + workspace.update(&mut cx, |workspace, cx| { + if let Some((center_group, active_pane)) = center_group { + workspace.remove_panes(workspace.center.root.clone(), cx); - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; - anyhow::Ok(()) + // Swap workspace center group + workspace.center = PaneGroup::with_root(center_group); + + // Change the focus to the workspace first so that we retrigger focus in on the pane. + cx.focus_self(); + + if let Some(active_pane) = active_pane { + cx.focus(&active_pane); + } else { + cx.focus(workspace.panes.last().unwrap()); + } + } else { + let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); + if let Some(old_center_handle) = old_center_handle { + cx.focus(&old_center_handle) + } else { + cx.focus_self() + } + } + + if workspace.left_sidebar().read(cx).is_open() + != serialized_workspace.left_sidebar_open + { + workspace.toggle_sidebar(SidebarSide::Left, cx); + } + + // Note that without after_window, the focus_self() and + // the focus the dock generates start generating alternating + // focus due to the deferred execution each triggering each other + cx.after_window_update(move |workspace, cx| { + Dock::set_dock_position( + workspace, + serialized_workspace.dock_position, + false, + cx, + ); + }); + + cx.notify(); + })?; + + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated + workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; + + Ok::<_, anyhow::Error>(resulting_list) + }}; + + result.await.unwrap_or_default() }) - .detach_and_log_err(cx); } #[cfg(any(test, feature = "test-support"))] @@ -2634,10 +2627,106 @@ impl Workspace { dock_default_item_factory: |_, _| None, background_actions: || &[], }); - Self::new(None, 0, project, app_state, cx) + Self::new(0, project, app_state, cx) } } +async fn open_items( + serialized_workspace: Option, + workspace: &WeakViewHandle, + mut project_paths_to_open: Vec<(PathBuf, Option)>, + app_state: Arc, + mut cx: AsyncAppContext, +) -> Vec>>> { + let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); + + if let Some(serialized_workspace) = serialized_workspace { + // TODO kb + // If the user is opening a serialized workspace, force open the requested paths + // Requested items: (CLI args or whatever) + // Restored items: What came from the database + // Remaining items = Requested - restored + // For each remaining item, call workspace.open_path() (as below) + + let workspace = workspace.clone(); + let restored_items = cx + .update(|cx| { + Workspace::load_workspace( + workspace, + serialized_workspace, + project_paths_to_open + .iter() + .map(|(_, project_path)| project_path) + .cloned() + .collect(), + cx, + ) + }) + .await; + + let restored_project_paths = cx.read(|cx| { + restored_items + .iter() + .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx)) + .collect::>() + }); + + opened_items = restored_items; + project_paths_to_open + .iter_mut() + .for_each(|(_, project_path)| { + if let Some(project_path_to_open) = project_path { + if restored_project_paths.contains(project_path_to_open) { + *project_path = None; + } + } + }); + } else { + for _ in 0..project_paths_to_open.len() { + opened_items.push(None); + } + } + assert!(opened_items.len() == project_paths_to_open.len()); + + let tasks = + project_paths_to_open + .into_iter() + .enumerate() + .map(|(i, (abs_path, project_path))| { + let workspace = workspace.clone(); + cx.spawn(|mut cx| { + let fs = app_state.fs.clone(); + async move { + let file_project_path = project_path?; + if fs.is_file(&abs_path).await { + Some(( + i, + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path(file_project_path, None, true, cx) + }) + .log_err()? + .await, + )) + } else { + None + } + } + }) + }); + + for maybe_opened_path in futures::future::join_all(tasks.into_iter()) + .await + .into_iter() + { + if let Some((i, path_open_result)) = maybe_opened_path { + opened_items[i] = Some(path_open_result); + } + } + + opened_items +} + fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; @@ -3008,8 +3097,7 @@ pub fn join_remote_project( let (_, workspace) = cx.add_window( (app_state.build_window_options)(None, None, cx.platform().as_ref()), |cx| { - let mut workspace = - Workspace::new(Default::default(), 0, project, app_state.clone(), cx); + let mut workspace = Workspace::new(0, project, app_state.clone(), cx); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); workspace }, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 03fdbf7067..8a659fd0cc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -729,6 +729,10 @@ async fn handle_cli_connection( for (item, path) in items.into_iter().zip(&paths) { match item { Some(Ok(item)) => { + log::info!("UPDATED ITEMS: {:?}", item); + log::info!( + "caret_positions: {caret_positions:?}, path: {path:?}", + ); if let Some(point) = caret_positions.remove(path) { // TODO kb does not work log::info!("@@@@@@@@ {path:?}@{point:?}"); From be7a58b5082e78794c9f08197a37c4356d29184c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 16 May 2023 17:37:23 +0300 Subject: [PATCH 92/97] Finalize the CLI opening part --- crates/workspace/src/persistence/model.rs | 2 - crates/workspace/src/workspace.rs | 46 +++++++++-------------- crates/zed/src/main.rs | 9 ----- 3 files changed, 17 insertions(+), 40 deletions(-) diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 46a8ab49b2..dd81109d8c 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -203,8 +203,6 @@ impl SerializedPane { items.push(item_handle.clone()); - log::info!("ACTUALLY SHOWN ITEMS: {:?}", &item_handle); - if let Some(item_handle) = item_handle { workspace.update(cx, |workspace, cx| { let pane_handle = pane_handle diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fe8ea65697..00093639e3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -681,10 +681,7 @@ impl Workspace { cx.spawn(|mut cx| async move { let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice()); - let paths_to_open = serialized_workspace - .as_ref() - .map(|workspace| workspace.location.paths()) - .unwrap_or(Arc::new(abs_paths)); + let paths_to_open = Arc::new(abs_paths); // Get project paths for all of the abs_paths let mut worktree_roots: HashSet> = Default::default(); @@ -1074,6 +1071,8 @@ impl Workspace { visible: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { + log::info!("open paths {:?}", abs_paths); + let fs = self.app_state.fs.clone(); // Sort the paths to ensure we add worktrees for parents before their children. @@ -2512,25 +2511,23 @@ impl Workspace { let dock_items = serialized_workspace .dock_pane .deserialize_to( - &project, - &dock_pane_handle, - serialized_workspace.id, - &workspace, - &mut cx, + &project, + &dock_pane_handle, + serialized_workspace.id, + &workspace, + &mut cx, ) .await?; - // Traverse the splits tree and add to things - let something = serialized_workspace - .center_group - .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) - .await; - let mut center_items = None; let mut center_group = None; - if let Some((group, active_pane, items)) = something { - center_items = Some(items); - center_group = Some((group, active_pane)) + // Traverse the splits tree and add to things + if let Some((group, active_pane, items)) = serialized_workspace + .center_group + .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) + .await { + center_items = Some(items); + center_group = Some((group, active_pane)) } let resulting_list = cx.read(|cx| { @@ -2584,7 +2581,7 @@ impl Workspace { } if workspace.left_sidebar().read(cx).is_open() - != serialized_workspace.left_sidebar_open + != serialized_workspace.left_sidebar_open { workspace.toggle_sidebar(SidebarSide::Left, cx); } @@ -2641,13 +2638,6 @@ async fn open_items( let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); if let Some(serialized_workspace) = serialized_workspace { - // TODO kb - // If the user is opening a serialized workspace, force open the requested paths - // Requested items: (CLI args or whatever) - // Restored items: What came from the database - // Remaining items = Requested - restored - // For each remaining item, call workspace.open_path() (as below) - let workspace = workspace.clone(); let restored_items = cx .update(|cx| { @@ -2656,7 +2646,7 @@ async fn open_items( serialized_workspace, project_paths_to_open .iter() - .map(|(_, project_path)| project_path) + .map(|(_, project_path)| dbg!(project_path)) .cloned() .collect(), cx, @@ -2966,8 +2956,6 @@ pub fn open_paths( Vec, anyhow::Error>>>, )>, > { - log::info!("open paths {:?}", abs_paths); - let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); cx.spawn(|mut cx| async move { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8a659fd0cc..1947095bf5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -729,19 +729,11 @@ async fn handle_cli_connection( for (item, path) in items.into_iter().zip(&paths) { match item { Some(Ok(item)) => { - log::info!("UPDATED ITEMS: {:?}", item); - log::info!( - "caret_positions: {caret_positions:?}, path: {path:?}", - ); if let Some(point) = caret_positions.remove(path) { - // TODO kb does not work - log::info!("@@@@@@@@ {path:?}@{point:?}"); if let Some(active_editor) = item.downcast::() { - log::info!("@@@@@@@@ editor"); active_editor .downgrade() .update(&mut cx, |editor, cx| { - log::info!("@@@@@@@@ update"); let snapshot = editor.snapshot(cx).display_snapshot; let point = snapshot @@ -752,7 +744,6 @@ async fn handle_cli_connection( cx, |s| s.select_ranges([point..point]), ); - log::info!("@@@@@@@@ finished"); }) .log_err(); } From 5d4fc9975038a5be6bae3ed9be537ab54c2a6110 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 16 May 2023 20:48:19 +0300 Subject: [PATCH 93/97] Unit test file:row:column parsing --- crates/cli/src/main.rs | 2 +- crates/util/src/paths.rs | 197 +++++++++++++++++++++++++++++++++------ 2 files changed, 170 insertions(+), 29 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d4b75f9533..feebbff61b 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -79,7 +79,7 @@ fn main() -> Result<()> { .paths_with_position .into_iter() .map(|path_with_position| { - let path_with_position = path_with_position.convert_path(|path| { + let path_with_position = path_with_position.map_path_like(|path| { fs::canonicalize(&path) .with_context(|| format!("path {path:?} canonicalization")) })?; diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 96311fabf8..f998fc319f 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -73,43 +73,80 @@ pub fn compact(path: &Path) -> PathBuf { } } +/// A delimiter to use in `path_query:row_number:column_number` strings parsing. pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; -#[derive(Debug, Clone, Serialize, Deserialize)] +/// A representation of a path-like string with optional row and column numbers. +/// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PathLikeWithPosition

{ pub path_like: P, pub row: Option, + // Absent if row is absent. pub column: Option, } impl

PathLikeWithPosition

{ - pub fn parse_str(s: &str, parse_path_like_str: F) -> Result - where - F: Fn(&str) -> Result, - { - let mut components = s.splitn(3, FILE_ROW_COLUMN_DELIMITER).map(str::trim).fuse(); - let path_like_str = components.next().filter(|str| !str.is_empty()); - let row = components.next().and_then(|row| row.parse::().ok()); - let column = components - .next() - .filter(|_| row.is_some()) - .and_then(|col| col.parse::().ok()); - - Ok(match path_like_str { - Some(path_like_str) => Self { - path_like: parse_path_like_str(path_like_str)?, - row, - column, - }, - None => Self { - path_like: parse_path_like_str(s)?, + /// Parses a string that possibly has `:row:column` suffix. + /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`. + /// If any of the row/column component parsing fails, the whole string is then parsed as a path like. + pub fn parse_str( + s: &str, + parse_path_like_str: impl Fn(&str) -> Result, + ) -> Result { + let fallback = |fallback_str| { + Ok(Self { + path_like: parse_path_like_str(fallback_str)?, row: None, column: None, - }, - }) + }) + }; + + match s.trim().split_once(FILE_ROW_COLUMN_DELIMITER) { + Some((path_like_str, maybe_row_and_col_str)) => { + let path_like_str = path_like_str.trim(); + let maybe_row_and_col_str = maybe_row_and_col_str.trim(); + if path_like_str.is_empty() { + fallback(s) + } else if maybe_row_and_col_str.is_empty() { + fallback(path_like_str) + } else { + let (row_parse_result, maybe_col_str) = + match maybe_row_and_col_str.split_once(FILE_ROW_COLUMN_DELIMITER) { + Some((maybe_row_str, maybe_col_str)) => { + (maybe_row_str.parse::(), maybe_col_str.trim()) + } + None => (maybe_row_and_col_str.parse::(), ""), + }; + + match row_parse_result { + Ok(row) => { + if maybe_col_str.is_empty() { + Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: Some(row), + column: None, + }) + } else { + match maybe_col_str.parse::() { + Ok(col) => Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: Some(row), + column: Some(col), + }), + Err(_) => fallback(s), + } + } + } + Err(_) => fallback(s), + } + } + } + None => fallback(s), + } } - pub fn convert_path( + pub fn map_path_like( self, mapping: impl FnOnce(P) -> Result, ) -> Result, E> { @@ -120,10 +157,7 @@ impl

PathLikeWithPosition

{ }) } - pub fn to_string(&self, path_like_to_string: F) -> String - where - F: Fn(&P) -> String, - { + pub fn to_string(&self, path_like_to_string: impl Fn(&P) -> String) -> String { let path_like_string = path_like_to_string(&self.path_like); if let Some(row) = self.row { if let Some(column) = self.column { @@ -136,3 +170,110 @@ impl

PathLikeWithPosition

{ } } } + +#[cfg(test)] +mod tests { + use super::*; + + type TestPath = PathLikeWithPosition; + + fn parse_str(s: &str) -> TestPath { + TestPath::parse_str(s, |s| Ok::<_, std::convert::Infallible>(s.to_string())) + .expect("infallible") + } + + #[test] + fn path_with_position_parsing_positive() { + let input_and_expected = [ + ( + "test_file.rs", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: None, + column: None, + }, + ), + ( + "test_file.rs:1", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: None, + }, + ), + ( + "test_file.rs:1:2", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: Some(2), + }, + ), + ]; + + for (input, expected) in input_and_expected { + let actual = parse_str(input); + assert_eq!( + actual, expected, + "For positive case input str '{input}', got a parse mismatch" + ); + } + } + + #[test] + fn path_with_position_parsing_negative() { + for input in [ + "test_file.rs:a", + "test_file.rs:a:b", + "test_file.rs::", + "test_file.rs::1", + "test_file.rs:1::", + "test_file.rs::1:2", + "test_file.rs:1::2", + "test_file.rs:1:2:", + "test_file.rs:1:2:3", + ] { + let actual = parse_str(input); + assert_eq!( + actual, + PathLikeWithPosition { + path_like: input.to_string(), + row: None, + column: None, + }, + "For negative case input str '{input}', got a parse mismatch" + ); + } + } + + // Trim off trailing `:`s for otherwise valid input. + #[test] + fn path_with_position_parsing_special() { + let input_and_expected = [ + ( + "test_file.rs:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: None, + column: None, + }, + ), + ( + "test_file.rs:1:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: None, + }, + ), + ]; + + for (input, expected) in input_and_expected { + let actual = parse_str(input); + assert_eq!( + actual, expected, + "For special case input str '{input}', got a parse mismatch" + ); + } + } +} From 55950e52c20459af3481ac544d4fc2c3e803a384 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 16 May 2023 22:15:56 +0300 Subject: [PATCH 94/97] Remove extra dbg! --- crates/workspace/src/workspace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 00093639e3..4bdaaedc64 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2646,7 +2646,7 @@ async fn open_items( serialized_workspace, project_paths_to_open .iter() - .map(|(_, project_path)| dbg!(project_path)) + .map(|(_, project_path)| project_path) .cloned() .collect(), cx, From ffd503951ba4a3cc87403cf66177325aae3876ac Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 17:19:05 -0400 Subject: [PATCH 95/97] Don't make events for every rejected suggestion --- crates/client/src/telemetry.rs | 2 +- crates/editor/src/editor.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index d11a78bb62..5e3c78420d 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -87,7 +87,7 @@ pub enum ClickhouseEvent { copilot_enabled_for_language: bool, }, Copilot { - suggestion_id: String, + suggestion_id: Option, suggestion_accepted: bool, file_extension: Option, }, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c51ed1a14a..ff4c0846cb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3098,7 +3098,7 @@ impl Editor { .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .detach_and_log_err(cx); - self.report_copilot_event(completion.uuid.clone(), true, cx) + self.report_copilot_event(Some(completion.uuid.clone()), true, cx) } self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx); cx.notify(); @@ -3117,9 +3117,7 @@ impl Editor { }) .detach_and_log_err(cx); - for completion in &self.copilot_state.completions { - self.report_copilot_event(completion.uuid.clone(), false, cx) - } + self.report_copilot_event(None, false, cx) } self.display_map @@ -6884,7 +6882,7 @@ impl Editor { fn report_copilot_event( &self, - suggestion_id: String, + suggestion_id: Option, suggestion_accepted: bool, cx: &AppContext, ) { From c27859871fe7a74e2d29ab2a6d4f128cfbbae04a Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 16 May 2023 18:16:09 -0400 Subject: [PATCH 96/97] Send editor event when saving a new file --- crates/editor/src/editor.rs | 76 +++++++++++++++++++++---------------- crates/editor/src/items.rs | 7 +++- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ff4c0846cb..21d495d6ee 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1330,7 +1330,7 @@ impl Editor { cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); } - this.report_editor_event("open", cx); + this.report_editor_event("open", None, cx); this } @@ -6897,7 +6897,8 @@ impl Editor { .as_singleton() .and_then(|b| b.read(cx).file()) .and_then(|file| Path::new(file.file_name(cx)).extension()) - .and_then(|e| e.to_str()); + .and_then(|e| e.to_str()) + .map(|a| a.to_string()); let telemetry = project.read(cx).client().telemetry().clone(); let telemetry_settings = cx.global::().telemetry(); @@ -6905,49 +6906,58 @@ impl Editor { let event = ClickhouseEvent::Copilot { suggestion_id, suggestion_accepted, - file_extension: file_extension.map(ToString::to_string), + file_extension, }; telemetry.report_clickhouse_event(event, telemetry_settings); } - fn report_editor_event(&self, name: &'static str, cx: &AppContext) { - if let Some((project, file)) = self.project.as_ref().zip( - self.buffer - .read(cx) - .as_singleton() - .and_then(|b| b.read(cx).file()), - ) { - let settings = cx.global::(); + fn report_editor_event( + &self, + name: &'static str, + file_extension: Option, + cx: &AppContext, + ) { + let Some(project) = &self.project else { + return + }; - let extension = Path::new(file.file_name(cx)) - .extension() - .and_then(|e| e.to_str()); - let telemetry = project.read(cx).client().telemetry().clone(); - telemetry.report_mixpanel_event( + // If None, we are in a file without an extension + let file_extension = file_extension.or(self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()) + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()) + .map(|a| a.to_string())); + + let settings = cx.global::(); + + let telemetry = project.read(cx).client().telemetry().clone(); + telemetry.report_mixpanel_event( match name { "open" => "open editor", "save" => "save editor", _ => name, }, - json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }), + json!({ "File Extension": file_extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }), settings.telemetry(), ); - let event = ClickhouseEvent::Editor { - file_extension: extension.map(ToString::to_string), - vim_mode: settings.vim_mode, - operation: name, - copilot_enabled: settings.features.copilot, - copilot_enabled_for_language: settings.show_copilot_suggestions( - self.language_at(0, cx) - .map(|language| language.name()) - .as_deref(), - self.file_at(0, cx) - .map(|file| file.path().clone()) - .as_deref(), - ), - }; - telemetry.report_clickhouse_event(event, settings.telemetry()) - } + let event = ClickhouseEvent::Editor { + file_extension, + vim_mode: settings.vim_mode, + operation: name, + copilot_enabled: settings.features.copilot, + copilot_enabled_for_language: settings.show_copilot_suggestions( + self.language_at(0, cx) + .map(|language| language.name()) + .as_deref(), + self.file_at(0, cx) + .map(|file| file.path().clone()) + .as_deref(), + ), + }; + telemetry.report_clickhouse_event(event, settings.telemetry()) } /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1a9fcc963e..9e122cc63d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -637,7 +637,7 @@ impl Item for Editor { project: ModelHandle, cx: &mut ViewContext, ) -> Task> { - self.report_editor_event("save", cx); + self.report_editor_event("save", None, cx); let format = self.perform_format(project.clone(), FormatTrigger::Save, cx); let buffers = self.buffer().clone().read(cx).all_buffers(); cx.spawn(|_, mut cx| async move { @@ -686,6 +686,11 @@ impl Item for Editor { .as_singleton() .expect("cannot call save_as on an excerpt list"); + let file_extension = abs_path + .extension() + .map(|a| a.to_string_lossy().to_string()); + self.report_editor_event("save", file_extension, cx); + project.update(cx, |project, cx| { project.save_buffer_as(buffer, abs_path, cx) }) From d61b12a05b089515bfaf44526b14bbdc81e5fdf7 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 May 2023 18:44:16 -0400 Subject: [PATCH 97/97] Disable usvg's text feature flags to include less dependency code --- crates/gpui/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 35c5010cdd..04862d1814 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -48,7 +48,7 @@ smallvec.workspace = true smol.workspace = true time.workspace = true tiny-skia = "0.5" -usvg = "0.14" +usvg = { version = "0.14", features = [] } uuid = { version = "1.1.2", features = ["v4"] } waker-fn = "1.1.0"