Refactor: Make it possible to share a remote worktree (#12775)

This PR is an internal refactor in preparation for remote editing. It
restructures the public interface of `Worktree`, reducing the number of
call sites that assume that a worktree is local or remote.

* The Project no longer calls `worktree.as_local_mut().unwrap()` in code
paths related to basic file operations
* Fewer code paths in the app rely on the worktree's `LocalSnapshot`
* Worktree-related RPC message handling is more fully encapsulated by
the `Worktree` type.

to do:
* [x] file manipulation operations
* [x] sending worktree updates when sharing

for later
* opening buffers
* updating open buffers upon worktree changes

Release Notes:

- N/A
This commit is contained in:
Max Brunsfeld 2024-06-07 12:53:01 -07:00 committed by GitHub
parent aa60fc2f19
commit e174f16d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 952 additions and 839 deletions

View file

@ -27,6 +27,7 @@ use futures::{
oneshot,
},
future::{join_all, try_join_all, Shared},
prelude::future::BoxFuture,
select,
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
@ -38,6 +39,7 @@ use gpui::{
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel, WindowContext,
};
use http::{HttpClient, Url};
use itertools::Itertools;
use language::{
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
@ -66,19 +68,16 @@ use postage::watch;
use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{LspSettings, ProjectSettings};
use rand::prelude::*;
use search_history::SearchHistory;
use snippet::Snippet;
use worktree::{CreatedEntry, LocalSnapshot};
use http::{HttpClient, Url};
use rpc::{ErrorCode, ErrorExt as _};
use search::SearchQuery;
use search_history::SearchHistory;
use serde::Serialize;
use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore};
use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff};
use smol::channel::{Receiver, Sender};
use smol::lock::Semaphore;
use snippet::Snippet;
use std::{
borrow::Cow,
cmp::{self, Ordering},
@ -111,7 +110,7 @@ use util::{
},
post_inc, ResultExt, TryFutureExt as _,
};
use worktree::{Snapshot, Traversal};
use worktree::{CreatedEntry, RemoteWorktreeClient, Snapshot, Traversal};
pub use fs::*;
pub use language::Location;
@ -858,7 +857,13 @@ impl Project {
// That's because Worktree's identifier is entity id, which should probably be changed.
let mut worktrees = Vec::new();
for worktree in response.payload.worktrees {
let worktree = Worktree::remote(replica_id, worktree, cx);
let worktree = Worktree::remote(
remote_id,
replica_id,
worktree,
Box::new(CollabRemoteWorktreeClient(client.clone())),
cx,
);
worktrees.push(worktree);
}
@ -1453,47 +1458,9 @@ impl Project {
"No worktree for path {project_path:?}"
))));
};
if self.is_local() {
worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.create_entry(project_path.path, is_directory, cx)
})
} else {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::CreateProjectEntry {
worktree_id: project_path.worktree_id.to_proto(),
project_id,
path: project_path.path.to_string_lossy().into(),
is_directory,
})
.await?;
match response.entry {
Some(entry) => worktree
.update(&mut cx, |worktree, cx| {
worktree.as_remote_mut().unwrap().insert_entry(
entry,
response.worktree_scan_id as usize,
cx,
)
})?
.await
.map(CreatedEntry::Included),
None => {
let abs_path = worktree.update(&mut cx, |worktree, _| {
worktree
.absolutize(&project_path.path)
.with_context(|| format!("absolutizing {project_path:?}"))
})??;
Ok(CreatedEntry::Excluded { abs_path })
}
}
})
}
worktree.update(cx, |worktree, cx| {
worktree.create_entry(project_path.path, is_directory, cx)
})
}
pub fn copy_entry(
@ -1505,41 +1472,9 @@ impl Project {
let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
return Task::ready(Ok(None));
};
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.copy_entry(entry_id, new_path, cx)
})
} else {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::CopyProjectEntry {
project_id,
entry_id: entry_id.to_proto(),
new_path: new_path.to_string_lossy().into(),
})
.await?;
match response.entry {
Some(entry) => worktree
.update(&mut cx, |worktree, cx| {
worktree.as_remote_mut().unwrap().insert_entry(
entry,
response.worktree_scan_id as usize,
cx,
)
})?
.await
.map(Some),
None => Ok(None),
}
})
}
worktree.update(cx, |worktree, cx| {
worktree.copy_entry(entry_id, new_path, cx)
})
}
pub fn rename_entry(
@ -1551,48 +1486,9 @@ impl Project {
let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
return Task::ready(Err(anyhow!(format!("No worktree for entry {entry_id:?}"))));
};
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.rename_entry(entry_id, new_path, cx)
})
} else {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::RenameProjectEntry {
project_id,
entry_id: entry_id.to_proto(),
new_path: new_path.to_string_lossy().into(),
})
.await?;
match response.entry {
Some(entry) => worktree
.update(&mut cx, |worktree, cx| {
worktree.as_remote_mut().unwrap().insert_entry(
entry,
response.worktree_scan_id as usize,
cx,
)
})?
.await
.map(CreatedEntry::Included),
None => {
let abs_path = worktree.update(&mut cx, |worktree, _| {
worktree
.absolutize(&new_path)
.with_context(|| format!("absolutizing {new_path:?}"))
})??;
Ok(CreatedEntry::Excluded { abs_path })
}
}
})
}
worktree.update(cx, |worktree, cx| {
worktree.rename_entry(entry_id, new_path, cx)
})
}
pub fn delete_entry(
@ -1602,38 +1498,10 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Option<Task<Result<()>>> {
let worktree = self.worktree_for_entry(entry_id, cx)?;
cx.emit(Event::DeletedEntry(entry_id));
if self.is_local() {
worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.delete_entry(entry_id, trash, cx)
})
} else {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
Some(cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::DeleteProjectEntry {
project_id,
entry_id: entry_id.to_proto(),
use_trash: trash,
})
.await?;
worktree
.update(&mut cx, move |worktree, cx| {
worktree.as_remote_mut().unwrap().delete_entry(
entry_id,
response.worktree_scan_id as usize,
cx,
)
})?
.await
}))
}
worktree.update(cx, |worktree, cx| {
worktree.delete_entry(entry_id, trash, cx)
})
}
pub fn expand_entry(
@ -1643,31 +1511,7 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Option<Task<Result<()>>> {
let worktree = self.worktree_for_id(worktree_id, cx)?;
if self.is_local() {
worktree.update(cx, |worktree, cx| {
worktree.as_local_mut().unwrap().expand_entry(entry_id, cx)
})
} else {
let worktree = worktree.downgrade();
let request = self.client.request(proto::ExpandProjectEntry {
project_id: self.remote_id().unwrap(),
entry_id: entry_id.to_proto(),
});
Some(cx.spawn(move |_, mut cx| async move {
let response = request.await?;
if let Some(worktree) = worktree.upgrade() {
worktree
.update(&mut cx, |worktree, _| {
worktree
.as_remote_mut()
.unwrap()
.wait_for_snapshot(response.worktree_scan_id as usize)
})?
.await?;
}
Ok(())
}))
}
worktree.update(cx, |worktree, cx| worktree.expand_entry(entry_id, cx))
}
pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {
@ -1785,18 +1629,12 @@ impl Project {
}
}
worktree.as_local_mut().unwrap().observe_updates(
project_id,
cx,
{
let client = client.clone();
move |update| {
client
.request(update)
.map(|result| result.is_ok())
}
},
);
worktree.observe_updates(project_id, cx, {
let client = client.clone();
move |update| {
client.request(update).map(|result| result.is_ok())
}
});
anyhow::Ok(())
})?;
@ -1947,7 +1785,7 @@ impl Project {
for worktree_handle in self.worktrees.iter_mut() {
if let WorktreeHandle::Strong(worktree) = worktree_handle {
let is_visible = worktree.update(cx, |worktree, _| {
worktree.as_local_mut().unwrap().stop_observing_updates();
worktree.stop_observing_updates();
worktree.is_visible()
});
if !is_visible {
@ -2230,21 +2068,20 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
let load_buffer = worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
let file = worktree.load_file(path.as_ref(), cx);
let load_file = worktree.load_file(path.as_ref(), cx);
let reservation = cx.reserve_model();
let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64());
cx.spawn(move |_, mut cx| async move {
let (file, contents, diff_base) = file.await?;
let loaded = load_file.await?;
let text_buffer = cx
.background_executor()
.spawn(async move { text::Buffer::new(0, buffer_id, contents) })
.spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) })
.await;
cx.insert_model(reservation, |_| {
Buffer::build(
text_buffer,
diff_base,
Some(Arc::new(file)),
loaded.diff_base,
Some(loaded.file),
Capability::ReadWrite,
)
})
@ -2398,10 +2235,11 @@ impl Project {
};
let worktree = file.worktree.clone();
let path = file.path.clone();
worktree.update(cx, |worktree, cx| match worktree {
Worktree::Local(worktree) => self.save_local_buffer(&worktree, buffer, path, false, cx),
Worktree::Remote(_) => self.save_remote_buffer(buffer, None, cx),
})
if self.is_local() {
self.save_local_buffer(worktree, buffer, path, false, cx)
} else {
self.save_remote_buffer(buffer, None, cx)
}
}
pub fn save_buffer_as(
@ -2410,26 +2248,21 @@ impl Project {
path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let old_file = File::from_dyn(buffer.read(cx).file())
.filter(|f| f.is_local())
.cloned();
let old_file = File::from_dyn(buffer.read(cx).file()).cloned();
let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) else {
return Task::ready(Err(anyhow!("worktree does not exist")));
};
cx.spawn(move |this, mut cx| async move {
this.update(&mut cx, |this, cx| {
if let Some(old_file) = &old_file {
this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
if this.is_local() {
if let Some(old_file) = &old_file {
this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
}
this.save_local_buffer(worktree, buffer.clone(), path.path, true, cx)
} else {
this.save_remote_buffer(buffer.clone(), Some(path.to_proto()), cx)
}
worktree.update(cx, |worktree, cx| match worktree {
Worktree::Local(worktree) => {
this.save_local_buffer(worktree, buffer.clone(), path.path, true, cx)
}
Worktree::Remote(_) => {
this.save_remote_buffer(buffer.clone(), Some(path.to_proto()), cx)
}
})
})?
.await?;
@ -2443,70 +2276,39 @@ impl Project {
pub fn save_local_buffer(
&self,
worktree: &LocalWorktree,
worktree: Model<Worktree>,
buffer_handle: Model<Buffer>,
path: Arc<Path>,
mut has_changed_file: bool,
cx: &mut ModelContext<Worktree>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
let rpc = self.client.clone();
let buffer_id: u64 = buffer.remote_id().into();
let project_id = self.remote_id();
let buffer_id = buffer.remote_id();
let text = buffer.as_rope().clone();
let line_ending = buffer.line_ending();
let version = buffer.version();
if buffer.file().is_some_and(|file| !file.is_created()) {
has_changed_file = true;
}
let text = buffer.as_rope().clone();
let version = buffer.version();
let save = worktree.write_file(path.as_ref(), text, buffer.line_ending(), cx);
let fs = Arc::clone(&self.fs);
let abs_path = worktree.absolutize(&path);
let is_private = worktree.is_path_private(&path);
cx.spawn(move |this, mut cx| async move {
let entry = save.await?;
let abs_path = abs_path?;
let this = this.upgrade().context("worktree dropped")?;
let (entry_id, mtime, path, is_private) = match entry {
Some(entry) => (Some(entry.id), entry.mtime, entry.path, entry.is_private),
None => {
let metadata = fs
.metadata(&abs_path)
.await
.with_context(|| {
format!(
"Fetching metadata after saving the excluded buffer {abs_path:?}"
)
})?
.with_context(|| {
format!("Excluded buffer {path:?} got removed during saving")
})?;
(None, Some(metadata.mtime), path, is_private)
}
};
let save = worktree.update(cx, |worktree, cx| {
worktree.write_file(path.as_ref(), text, line_ending, cx)
});
let client = self.client.clone();
let project_id = self.remote_id();
cx.spawn(move |_, mut cx| async move {
let new_file = save.await?;
let mtime = new_file.mtime;
if has_changed_file {
let new_file = Arc::new(File {
entry_id,
worktree: this,
path,
mtime,
is_local: true,
is_deleted: false,
is_private,
});
if let Some(project_id) = project_id {
rpc.send(proto::UpdateBufferFile {
project_id,
buffer_id,
file: Some(new_file.to_proto()),
})
.log_err();
client
.send(proto::UpdateBufferFile {
project_id,
buffer_id: buffer_id.into(),
file: Some(new_file.to_proto()),
})
.log_err();
}
buffer_handle.update(&mut cx, |buffer, cx| {
@ -2517,9 +2319,9 @@ impl Project {
}
if let Some(project_id) = project_id {
rpc.send(proto::BufferSaved {
client.send(proto::BufferSaved {
project_id,
buffer_id,
buffer_id: buffer_id.into(),
version: serialize_version(&version),
mtime: mtime.map(|time| time.into()),
})?;
@ -2537,7 +2339,7 @@ impl Project {
&self,
buffer_handle: Model<Buffer>,
new_path: Option<proto::ProjectPath>,
cx: &mut ModelContext<Worktree>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
let buffer_id = buffer.remote_id().into();
@ -2641,7 +2443,6 @@ impl Project {
self.detect_language_for_buffer(buffer, cx);
self.register_buffer_with_language_servers(buffer, cx);
// self.register_buffer_with_copilot(buffer, cx);
cx.observe_release(buffer, |this, buffer, cx| {
if let Some(file) = File::from_dyn(buffer.file()) {
if file.is_local() {
@ -2791,16 +2592,6 @@ impl Project {
});
}
// fn register_buffer_with_copilot(
// &self,
// buffer_handle: &Model<Buffer>,
// cx: &mut ModelContext<Self>,
// ) {
// if let Some(copilot) = Copilot::global(cx) {
// copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx));
// }
// }
async fn send_buffer_ordered_messages(
this: WeakModel<Self>,
rx: UnboundedReceiver<BufferOrderedMessage>,
@ -5521,7 +5312,7 @@ impl Project {
) -> Result<Option<Diff>> {
let working_dir_path = buffer.update(cx, |buffer, cx| {
let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx).as_local()?;
let worktree = file.worktree.read(cx);
let mut worktree_path = worktree.abs_path().to_path_buf();
if worktree.root_entry()?.is_file() {
worktree_path.pop();
@ -5708,9 +5499,6 @@ impl Project {
if !worktree.is_visible() {
continue;
}
let Some(worktree) = worktree.as_local() else {
continue;
};
let worktree_abs_path = worktree.abs_path().clone();
let (adapter, language, server) = match self.language_servers.get(server_id) {
@ -5874,15 +5662,14 @@ impl Project {
let worktree_abs_path = if let Some(worktree_abs_path) = self
.worktree_for_id(symbol.path.worktree_id, cx)
.and_then(|worktree| worktree.read(cx).as_local())
.map(|local_worktree| local_worktree.abs_path())
.map(|worktree| worktree.read(cx).abs_path())
{
worktree_abs_path
} else {
return Task::ready(Err(anyhow!("worktree not found for symbol")));
};
let symbol_abs_path = resolve_path(worktree_abs_path, &symbol.path.path);
let symbol_abs_path = resolve_path(&worktree_abs_path, &symbol.path.path);
let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) {
uri
} else {
@ -7234,8 +7021,8 @@ impl Project {
let snapshots = self
.visible_worktrees(cx)
.filter_map(|tree| {
let tree = tree.read(cx).as_local()?;
Some(tree.snapshot())
let tree = tree.read(cx);
Some((tree.snapshot(), tree.as_local()?.settings()))
})
.collect::<Vec<_>>();
let include_root = snapshots.len() > 1;
@ -7243,11 +7030,11 @@ impl Project {
let background = cx.background_executor().clone();
let path_count: usize = snapshots
.iter()
.map(|s| {
.map(|(snapshot, _)| {
if query.include_ignored() {
s.file_count()
snapshot.file_count()
} else {
s.visible_file_count()
snapshot.visible_file_count()
}
})
.sum();
@ -7403,7 +7190,7 @@ impl Project {
query: SearchQuery,
include_root: bool,
path_count: usize,
snapshots: Vec<LocalSnapshot>,
snapshots: Vec<(Snapshot, WorktreeSettings)>,
matching_paths_tx: Sender<SearchMatchCandidate>,
) {
let fs = &fs;
@ -7459,13 +7246,14 @@ impl Project {
}
if query.include_ignored() {
for snapshot in snapshots {
for (snapshot, settings) in snapshots {
for ignored_entry in snapshot.entries(true).filter(|e| e.is_ignored) {
let limiter = Arc::clone(&max_concurrent_workers);
scope.spawn(async move {
let _guard = limiter.acquire().await;
search_ignored_entry(
snapshot,
settings,
ignored_entry,
fs,
query,
@ -8302,7 +8090,7 @@ impl Project {
changes: &UpdatedEntriesSet,
cx: &mut ModelContext<Self>,
) {
if worktree.read(cx).as_local().is_none() {
if worktree.read(cx).is_remote() {
return;
}
let project_id = self.remote_id();
@ -8607,14 +8395,12 @@ impl Project {
self.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.as_local()?
.snapshot()
.local_git_repo(&project_path.path)
}
pub fn get_first_worktree_root_repo(&self, cx: &AppContext) -> Option<Arc<dyn GitRepository>> {
let worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?;
let root_entry = worktree.root_git_entry()?;
worktree.get_local_repo(&root_entry)?.repo().clone().into()
}
@ -9016,21 +8802,7 @@ impl Project {
this.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("worktree not found"))
})??;
let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
let entry = worktree
.update(&mut cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
let path = PathBuf::from(envelope.payload.path);
worktree.create_entry(path, envelope.payload.is_directory, cx)
})?
.await?;
Ok(proto::ProjectEntryResponse {
entry: match &entry {
CreatedEntry::Included(entry) => Some(entry.into()),
CreatedEntry::Excluded { .. } => None,
},
worktree_scan_id: worktree_scan_id as u64,
})
Worktree::handle_create_entry(worktree, envelope.payload, cx).await
}
async fn handle_rename_project_entry(
@ -9044,23 +8816,7 @@ impl Project {
this.worktree_for_entry(entry_id, cx)
.ok_or_else(|| anyhow!("worktree not found"))
})??;
let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
let entry = worktree
.update(&mut cx, |worktree, cx| {
let new_path = PathBuf::from(envelope.payload.new_path);
worktree
.as_local_mut()
.unwrap()
.rename_entry(entry_id, new_path, cx)
})?
.await?;
Ok(proto::ProjectEntryResponse {
entry: match &entry {
CreatedEntry::Included(entry) => Some(entry.into()),
CreatedEntry::Excluded { .. } => None,
},
worktree_scan_id: worktree_scan_id as u64,
})
Worktree::handle_rename_entry(worktree, envelope.payload, cx).await
}
async fn handle_copy_project_entry(
@ -9074,20 +8830,7 @@ impl Project {
this.worktree_for_entry(entry_id, cx)
.ok_or_else(|| anyhow!("worktree not found"))
})??;
let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
let entry = worktree
.update(&mut cx, |worktree, cx| {
let new_path = PathBuf::from(envelope.payload.new_path);
worktree
.as_local_mut()
.unwrap()
.copy_entry(entry_id, new_path, cx)
})?
.await?;
Ok(proto::ProjectEntryResponse {
entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
Worktree::handle_copy_entry(worktree, envelope.payload, cx).await
}
async fn handle_delete_project_entry(
@ -9097,28 +8840,12 @@ impl Project {
mut cx: AsyncAppContext,
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
let trash = envelope.payload.use_trash;
this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id)))?;
let worktree = this.update(&mut cx, |this, cx| {
this.worktree_for_entry(entry_id, cx)
.ok_or_else(|| anyhow!("worktree not found"))
})??;
let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
worktree
.update(&mut cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.delete_entry(entry_id, trash, cx)
.ok_or_else(|| anyhow!("invalid entry"))
})??
.await?;
Ok(proto::ProjectEntryResponse {
entry: None,
worktree_scan_id: worktree_scan_id as u64,
})
this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id)))?;
Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
}
async fn handle_expand_project_entry(
@ -9131,17 +8858,7 @@ impl Project {
let worktree = this
.update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
.ok_or_else(|| anyhow!("invalid request"))?;
worktree
.update(&mut cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.expand_entry(entry_id, cx)
.ok_or_else(|| anyhow!("invalid entry"))
})??
.await?;
let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())? as u64;
Ok(proto::ExpandProjectEntryResponse { worktree_scan_id })
Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
}
async fn handle_update_diagnostic_summary(
@ -10594,6 +10311,7 @@ impl Project {
cx: &mut ModelContext<Project>,
) -> Result<()> {
let replica_id = self.replica_id();
let remote_id = self.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
let mut old_worktrees_by_id = self
.worktrees
@ -10610,8 +10328,16 @@ impl Project {
{
self.worktrees.push(WorktreeHandle::Strong(old_worktree));
} else {
let worktree = Worktree::remote(replica_id, worktree, cx);
let _ = self.add_worktree(&worktree, cx);
self.add_worktree(
&Worktree::remote(
remote_id,
replica_id,
worktree,
Box::new(CollabRemoteWorktreeClient(self.client.clone())),
cx,
),
cx,
);
}
}
@ -11374,7 +11100,7 @@ fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::Co
#[allow(clippy::too_many_arguments)]
async fn search_snapshots(
snapshots: &Vec<LocalSnapshot>,
snapshots: &Vec<(Snapshot, WorktreeSettings)>,
worker_start_ix: usize,
worker_end_ix: usize,
query: &SearchQuery,
@ -11386,7 +11112,7 @@ async fn search_snapshots(
let mut snapshot_start_ix = 0;
let mut abs_path = PathBuf::new();
for snapshot in snapshots {
for (snapshot, _) in snapshots {
let snapshot_end_ix = snapshot_start_ix
+ if query.include_ignored() {
snapshot.file_count()
@ -11452,7 +11178,8 @@ async fn search_snapshots(
}
async fn search_ignored_entry(
snapshot: &LocalSnapshot,
snapshot: &Snapshot,
settings: &WorktreeSettings,
ignored_entry: &Entry,
fs: &Arc<dyn Fs>,
query: &SearchQuery,
@ -11486,7 +11213,7 @@ async fn search_ignored_entry(
}
} else if !fs_metadata.is_symlink {
if !query.file_matches(Some(&ignored_abs_path))
|| snapshot.is_path_excluded(&ignored_entry.path)
|| settings.is_path_excluded(&ignored_entry.path)
{
continue;
}
@ -11562,6 +11289,18 @@ impl OpenBuffer {
}
}
pub struct CollabRemoteWorktreeClient(Arc<Client>);
impl RemoteWorktreeClient for CollabRemoteWorktreeClient {
fn request(
&self,
envelope: proto::Envelope,
request_type: &'static str,
) -> BoxFuture<'static, Result<proto::Envelope>> {
self.0.request_dynamic(envelope, request_type).boxed()
}
}
pub struct PathMatchCandidateSet {
pub snapshot: Snapshot,
pub include_ignored: bool,

View file

@ -2981,21 +2981,26 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
// Create a remote copy of this worktree.
let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
let metadata = tree.update(cx, |tree, _| tree.as_local().unwrap().metadata_proto());
let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
let updates = Arc::new(Mutex::new(Vec::new()));
tree.update(cx, |tree, cx| {
tree.as_local_mut().unwrap().observe_updates(0, cx, {
let updates = updates.clone();
move |update| {
updates.lock().push(update);
async { true }
}
let updates = updates.clone();
tree.observe_updates(0, cx, move |update| {
updates.lock().push(update);
async { true }
});
});
let remote = cx.update(|cx| Worktree::remote(1, metadata, cx));
let remote = cx.update(|cx| {
Worktree::remote(
0,
1,
metadata,
Box::new(CollabRemoteWorktreeClient(project.read(cx).client())),
cx,
)
});
cx.executor().run_until_parked();