From 48581167b73165fe9f9c780a73a6be7c1602f7a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Jun 2024 11:16:58 -0700 Subject: [PATCH] Remove dependencies from the Worktree crate and make it more focused (#12747) The `worktree` crate mainly provides an in-memory model of a directory and its git repositories. But because it was originally extracted from the Project crate, it also contained lingering bits of code that were outside of that area: * it had a little bit of logic related to buffers (though most buffer management lives in `project`) * it had a *little* bit of logic for storing diagnostics (though the vast majority of LSP and diagnostic logic lives in `project`) * it had a little bit of logic for sending RPC message (though the *receiving* logic for those RPC messages lived in `project`) In this PR, I've moved those concerns entirely to the project crate (where they were already dealt with for the most part), so that the worktree crate can be more focused on its main job, and have fewer dependencies. Worktree no longer depends on `client` or `lsp`. It still depends on `language`, but only because of `impl language::File for worktree::File`. Release Notes: - N/A --- Cargo.lock | 2 - crates/collab/src/tests/editor_tests.rs | 10 +- crates/collab/src/tests/integration_tests.rs | 37 +- crates/copilot/src/copilot.rs | 12 - crates/language/src/buffer.rs | 19 - crates/project/src/project.rs | 577 +++++++++++++++---- crates/project/src/project_tests.rs | 5 +- crates/worktree/Cargo.toml | 3 - crates/worktree/src/worktree.rs | 503 +--------------- crates/worktree/src/worktree_tests.rs | 75 +-- 10 files changed, 554 insertions(+), 689 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10d80c2518..93ea32f333 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12887,7 +12887,6 @@ name = "worktree" version = "0.1.0" dependencies = [ "anyhow", - "client", "clock", "collections", "env_logger", @@ -12902,7 +12901,6 @@ dependencies = [ "itertools 0.11.0", "language", "log", - "lsp", "parking_lot", "postage", "pretty_assertions", diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 261fdcb165..d6b099d175 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -83,7 +83,10 @@ async fn test_host_disconnect( let project_b = client_b.build_dev_server_project(project_id, cx_b).await; cx_a.background_executor.run_until_parked(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + assert!(worktree_a.read_with(cx_a, |tree, _| tree + .as_local() + .unwrap() + .has_update_observer())); let workspace_b = cx_b .add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx)); @@ -120,7 +123,10 @@ async fn test_host_disconnect( project_b.read_with(cx_b, |project, _| project.is_read_only()); - assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + assert!(worktree_a.read_with(cx_a, |tree, _| !tree + .as_local() + .unwrap() + .has_update_observer())); // Ensure client B's edited state is reset and that the whole window is blurred. diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 3cf9c302dd..77cc9bab29 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1378,7 +1378,10 @@ async fn test_unshare_project( let project_b = client_b.build_dev_server_project(project_id, cx_b).await; executor.run_until_parked(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + assert!(worktree_a.read_with(cx_a, |tree, _| tree + .as_local() + .unwrap() + .has_update_observer())); project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) @@ -1403,7 +1406,10 @@ async fn test_unshare_project( .unwrap(); executor.run_until_parked(); - assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + assert!(worktree_a.read_with(cx_a, |tree, _| !tree + .as_local() + .unwrap() + .has_update_observer())); assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected())); @@ -1415,7 +1421,10 @@ async fn test_unshare_project( let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await; executor.run_until_parked(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + assert!(worktree_a.read_with(cx_a, |tree, _| tree + .as_local() + .unwrap() + .has_update_observer())); project_c2 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await @@ -1522,7 +1531,7 @@ async fn test_project_reconnect( executor.run_until_parked(); let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| { - assert!(worktree.as_local().unwrap().is_shared()); + assert!(worktree.as_local().unwrap().has_update_observer()); worktree.id() }); let (worktree_a2, _) = project_a1 @@ -1534,7 +1543,7 @@ async fn test_project_reconnect( executor.run_until_parked(); let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| { - assert!(tree.as_local().unwrap().is_shared()); + assert!(tree.as_local().unwrap().has_update_observer()); tree.id() }); executor.run_until_parked(); @@ -1568,7 +1577,7 @@ async fn test_project_reconnect( }); worktree_a1.read_with(cx_a, |tree, _| { - assert!(tree.as_local().unwrap().is_shared()) + assert!(tree.as_local().unwrap().has_update_observer()) }); // While client A is disconnected, add and remove files from client A's project. @@ -1611,7 +1620,7 @@ async fn test_project_reconnect( .await; let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| { - assert!(!tree.as_local().unwrap().is_shared()); + assert!(!tree.as_local().unwrap().has_update_observer()); tree.id() }); executor.run_until_parked(); @@ -1634,7 +1643,11 @@ async fn test_project_reconnect( project_a1.read_with(cx_a, |project, cx| { assert!(project.is_shared()); - assert!(worktree_a1.read(cx).as_local().unwrap().is_shared()); + assert!(worktree_a1 + .read(cx) + .as_local() + .unwrap() + .has_update_observer()); assert_eq!( worktree_a1 .read(cx) @@ -1652,7 +1665,11 @@ async fn test_project_reconnect( "subdir2/i.txt" ] ); - assert!(worktree_a3.read(cx).as_local().unwrap().is_shared()); + assert!(worktree_a3 + .read(cx) + .as_local() + .unwrap() + .has_update_observer()); assert_eq!( worktree_a3 .read(cx) @@ -1733,7 +1750,7 @@ async fn test_project_reconnect( executor.run_until_parked(); let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| { - assert!(tree.as_local().unwrap().is_shared()); + assert!(tree.as_local().unwrap().has_update_observer()); tree.id() }); project_a1.update(cx_a, |project, cx| { diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 9a836eb9be..db3b222812 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1044,7 +1044,6 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { mod tests { use super::*; use gpui::TestAppContext; - use language::BufferId; #[gpui::test(iterations = 10)] async fn test_buffer_management(cx: &mut TestAppContext) { @@ -1258,16 +1257,5 @@ mod tests { fn load(&self, _: &AppContext) -> Task> { unimplemented!() } - - fn buffer_reloaded( - &self, - _: BufferId, - _: &clock::Global, - _: language::LineEnding, - _: Option, - _: &mut AppContext, - ) { - unimplemented!() - } } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 68dd36201c..36f905c961 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -382,16 +382,6 @@ pub trait LocalFile: File { /// Loads the file's contents from disk. fn load(&self, cx: &AppContext) -> Task>; - /// Called when the buffer is reloaded from disk. - fn buffer_reloaded( - &self, - buffer_id: BufferId, - version: &clock::Global, - line_ending: LineEnding, - mtime: Option, - cx: &mut AppContext, - ); - /// Returns true if the file should not be shared with collaborators. fn is_private(&self, _: &AppContext) -> bool { false @@ -884,15 +874,6 @@ impl Buffer { self.saved_version = version; self.text.set_line_ending(line_ending); self.saved_mtime = mtime; - if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) { - file.buffer_reloaded( - self.remote_id(), - &self.saved_version, - self.line_ending(), - self.saved_mtime, - cx, - ); - } cx.emit(Event::Reloaded); cx.notify(); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2255efb4f7..fab00fb83f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -44,7 +44,7 @@ use language::{ markdown, point_to_lsp, prepare_completion_documentation, proto::{ deserialize_anchor, deserialize_line_ending, deserialize_version, serialize_anchor, - serialize_version, split_operations, + serialize_line_ending, serialize_version, split_operations, }, range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel, ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, @@ -121,9 +121,9 @@ pub use task_inventory::{ BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind, }; pub use worktree::{ - DiagnosticSummary, Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, - RepositoryEntry, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, - WorktreeSettings, FS_WATCH_LATENCY, + Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry, + UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, + FS_WATCH_LATENCY, }; const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4; @@ -180,6 +180,18 @@ pub struct Project { next_entry_id: Arc, join_project_response_message_id: u32, next_diagnostic_group_id: usize, + diagnostic_summaries: + HashMap, HashMap>>, + diagnostics: HashMap< + WorktreeId, + HashMap< + Arc, + Vec<( + LanguageServerId, + Vec>>, + )>, + >, + >, user_store: Model, fs: Arc, client_state: ProjectClientState, @@ -749,6 +761,8 @@ impl Project { fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), + diagnostics: Default::default(), + diagnostic_summaries: Default::default(), supplementary_language_servers: HashMap::default(), language_servers: Default::default(), language_server_ids: HashMap::default(), @@ -842,8 +856,7 @@ 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(remote_id, replica_id, worktree, client.clone(), cx); + let worktree = Worktree::remote(replica_id, worktree, cx); worktrees.push(worktree); } @@ -873,6 +886,8 @@ impl Project { fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), + diagnostic_summaries: Default::default(), + diagnostics: Default::default(), client_subscriptions: Default::default(), _subscriptions: vec![ cx.on_release(Self::release), @@ -1733,6 +1748,7 @@ impl Project { let worktrees = this.update(&mut cx, |this, _cx| { this.worktrees().collect::>() })?; + let update_project = this .update(&mut cx, |this, cx| { this.client.request(proto::UpdateProject { @@ -1741,14 +1757,49 @@ impl Project { }) })? .await; - if update_project.log_err().is_some() { + if update_project.log_err().is_none() { + continue; + } + + this.update(&mut cx, |this, cx| { for worktree in worktrees { - worktree.update(&mut cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree.share(project_id, cx).detach_and_log_err(cx) + worktree.update(cx, |worktree, cx| { + if let Some(summaries) = + this.diagnostic_summaries.get(&worktree.id()) + { + for (path, summaries) in summaries { + for (&server_id, summary) in summaries { + this.client.send( + proto::UpdateDiagnosticSummary { + project_id, + worktree_id: cx.entity_id().as_u64(), + summary: Some( + summary.to_proto(server_id, path), + ), + }, + )?; + } + } + } + + worktree.as_local_mut().unwrap().observe_updates( + project_id, + cx, + { + let client = client.clone(); + move |update| { + client + .request(update) + .map(|result| result.is_ok()) + } + }, + ); + + anyhow::Ok(()) })?; } - } + anyhow::Ok(()) + })??; } LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id } => { let buffer = this.update(&mut cx, |this, _| { @@ -1893,7 +1944,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().unshare(); + worktree.as_local_mut().unwrap().stop_observing_updates(); worktree.is_visible() }); if !is_visible { @@ -2177,23 +2228,47 @@ impl Project { ) -> Task>> { let load_buffer = worktree.update(cx, |worktree, cx| { let worktree = worktree.as_local_mut().unwrap(); - worktree.load_buffer(&path, cx) + let 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 text_buffer = cx + .background_executor() + .spawn(async move { text::Buffer::new(0, buffer_id, contents) }) + .await; + cx.insert_model(reservation, |_| { + Buffer::build( + text_buffer, + diff_base, + Some(Arc::new(file)), + Capability::ReadWrite, + ) + }) + }) }); - fn is_not_found_error(error: &anyhow::Error) -> bool { - error - .root_cause() - .downcast_ref::() - .is_some_and(|err| err.kind() == io::ErrorKind::NotFound) - } + cx.spawn(move |this, mut cx| async move { let buffer = match load_buffer.await { Ok(buffer) => Ok(buffer), - Err(error) if is_not_found_error(&error) => { - worktree.update(&mut cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree.new_buffer(path, cx) - }) - } + Err(error) if is_not_found_error(&error) => cx.new_model(|cx| { + let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); + let text_buffer = text::Buffer::new(0, buffer_id, "".into()); + Buffer::build( + text_buffer, + None, + Some(Arc::new(File { + worktree, + path, + mtime: None, + entry_id: None, + is_local: true, + is_deleted: false, + is_private: false, + })), + Capability::ReadWrite, + ) + }), Err(e) => Err(e), }?; this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))??; @@ -2321,8 +2396,8 @@ impl Project { let worktree = file.worktree.clone(); let path = file.path.clone(); worktree.update(cx, |worktree, cx| match worktree { - Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx), - Worktree::Remote(worktree) => worktree.save_buffer(buffer, None, cx), + Worktree::Local(worktree) => self.save_local_buffer(&worktree, buffer, path, false, cx), + Worktree::Remote(_) => self.save_remote_buffer(buffer, None, cx), }) } @@ -2340,21 +2415,20 @@ impl Project { }; cx.spawn(move |this, mut cx| async move { - if let Some(old_file) = &old_file { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { + if let Some(old_file) = &old_file { this.unregister_buffer_from_language_servers(&buffer, old_file, cx); - })?; - } - worktree - .update(&mut cx, |worktree, cx| match worktree { + } + worktree.update(cx, |worktree, cx| match worktree { Worktree::Local(worktree) => { - worktree.save_buffer(buffer.clone(), path.path, true, cx) + this.save_local_buffer(worktree, buffer.clone(), path.path, true, cx) } - Worktree::Remote(worktree) => { - worktree.save_buffer(buffer.clone(), Some(path.to_proto()), cx) + Worktree::Remote(_) => { + this.save_remote_buffer(buffer.clone(), Some(path.to_proto()), cx) } - })? - .await?; + }) + })? + .await?; this.update(&mut cx, |this, cx| { this.detect_language_for_buffer(&buffer, cx); @@ -2364,6 +2438,129 @@ impl Project { }) } + pub fn save_local_buffer( + &self, + worktree: &LocalWorktree, + buffer_handle: Model, + path: Arc, + mut has_changed_file: bool, + cx: &mut ModelContext, + ) -> Task> { + 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(); + + 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) + } + }; + + 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(); + } + + buffer_handle.update(&mut cx, |buffer, cx| { + if has_changed_file { + buffer.file_updated(new_file, cx); + } + })?; + } + + if let Some(project_id) = project_id { + rpc.send(proto::BufferSaved { + project_id, + buffer_id, + version: serialize_version(&version), + mtime: mtime.map(|time| time.into()), + })?; + } + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version.clone(), mtime, cx); + })?; + + Ok(()) + }) + } + + pub fn save_remote_buffer( + &self, + buffer_handle: Model, + new_path: Option, + cx: &mut ModelContext, + ) -> Task> { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id().into(); + let version = buffer.version(); + let rpc = self.client.clone(); + let project_id = self.remote_id(); + cx.spawn(move |_, mut cx| async move { + let response = rpc + .request(proto::SaveBuffer { + project_id: project_id.ok_or_else(|| anyhow!("project_id is not set"))?, + buffer_id, + new_path, + version: serialize_version(&version), + }) + .await?; + let version = deserialize_version(&response.version); + let mtime = response.mtime.map(|mtime| mtime.into()); + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version.clone(), mtime, cx); + })?; + + Ok(()) + }) + } + pub fn get_open_buffer( &mut self, path: &ProjectPath, @@ -2489,8 +2686,10 @@ impl Project { let language = buffer.language().cloned(); let worktree_id = file.worktree_id(cx); - if let Some(local_worktree) = file.worktree.read(cx).as_local() { - for (server_id, diagnostics) in local_worktree.diagnostics_for_path(file.path()) { + if let Some(diagnostics) = self.diagnostics.get(&worktree_id) { + for (server_id, diagnostics) in + diagnostics.get(file.path()).cloned().unwrap_or_default() + { self.update_buffer_diagnostics(buffer_handle, server_id, None, diagnostics, cx) .log_err(); } @@ -2729,6 +2928,23 @@ impl Project { .ok(); } + BufferEvent::Reloaded => { + if self.is_local() { + if let Some(project_id) = self.remote_id() { + let buffer = buffer.read(cx); + self.client + .send(proto::BufferReloaded { + project_id, + buffer_id: buffer.remote_id().to_proto(), + version: serialize_version(&buffer.version()), + mtime: buffer.saved_mtime().map(|t| t.into()), + line_ending: serialize_line_ending(buffer.line_ending()) as i32, + }) + .log_err(); + } + } + } + BufferEvent::Edited { .. } => { let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; @@ -3929,14 +4145,43 @@ impl Project { }); } } - for worktree in &self.worktrees { - if let Some(worktree) = worktree.upgrade() { - worktree.update(cx, |worktree, cx| { - if let Some(worktree) = worktree.as_local_mut() { - worktree.clear_diagnostics_for_language_server(server_id, cx); + + let project_id = self.remote_id(); + for (worktree_id, summaries) in self.diagnostic_summaries.iter_mut() { + summaries.retain(|path, summaries_by_server_id| { + if summaries_by_server_id.remove(&server_id).is_some() { + if let Some(project_id) = project_id { + self.client + .send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + language_server_id: server_id.0 as u64, + error_count: 0, + warning_count: 0, + }), + }) + .log_err(); } - }); - } + !summaries_by_server_id.is_empty() + } else { + true + } + }); + } + + for diagnostics in self.diagnostics.values_mut() { + diagnostics.retain(|_, diagnostics_by_server_id| { + if let Ok(ix) = + diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) + { + diagnostics_by_server_id.remove(ix); + !diagnostics_by_server_id.is_empty() + } else { + true + } + }); } self.language_server_watched_paths.remove(&server_id); @@ -4673,10 +4918,13 @@ impl Project { } let updated = worktree.update(cx, |worktree, cx| { - worktree - .as_local_mut() - .ok_or_else(|| anyhow!("not a local worktree"))? - .update_diagnostics(server_id, project_path.path.clone(), diagnostics, cx) + self.update_worktree_diagnostics( + worktree.id(), + server_id, + project_path.path.clone(), + diagnostics, + cx, + ) })?; if updated { cx.emit(Event::DiagnosticsUpdated { @@ -4687,6 +4935,67 @@ impl Project { Ok(()) } + pub fn update_worktree_diagnostics( + &mut self, + worktree_id: WorktreeId, + server_id: LanguageServerId, + worktree_path: Arc, + diagnostics: Vec>>, + _: &mut ModelContext, + ) -> Result { + let summaries_for_tree = self.diagnostic_summaries.entry(worktree_id).or_default(); + let diagnostics_for_tree = self.diagnostics.entry(worktree_id).or_default(); + let summaries_by_server_id = summaries_for_tree.entry(worktree_path.clone()).or_default(); + + let old_summary = summaries_by_server_id + .remove(&server_id) + .unwrap_or_default(); + + let new_summary = DiagnosticSummary::new(&diagnostics); + if new_summary.is_empty() { + if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&worktree_path) { + if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { + diagnostics_by_server_id.remove(ix); + } + if diagnostics_by_server_id.is_empty() { + diagnostics_for_tree.remove(&worktree_path); + } + } + } else { + summaries_by_server_id.insert(server_id, new_summary); + let diagnostics_by_server_id = diagnostics_for_tree + .entry(worktree_path.clone()) + .or_default(); + match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { + Ok(ix) => { + diagnostics_by_server_id[ix] = (server_id, diagnostics); + } + Err(ix) => { + diagnostics_by_server_id.insert(ix, (server_id, diagnostics)); + } + } + } + + if !old_summary.is_empty() || !new_summary.is_empty() { + if let Some(project_id) = self.remote_id() { + self.client + .send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: worktree_path.to_string_lossy().to_string(), + language_server_id: server_id.0 as u64, + error_count: new_summary.error_count as u32, + warning_count: new_summary.warning_count as u32, + }), + }) + .log_err(); + } + } + + Ok(!old_summary.is_empty() || !new_summary.is_empty()) + } + fn update_buffer_diagnostics( &mut self, buffer: &Model, @@ -7453,7 +7762,6 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let fs = self.fs.clone(); - let client = self.client.clone(); let next_entry_id = self.next_entry_id.clone(); let path: Arc = abs_path.as_ref().into(); let task = self @@ -7462,15 +7770,9 @@ impl Project { .or_insert_with(|| { cx.spawn(move |project, mut cx| { async move { - let worktree = Worktree::local( - client.clone(), - path.clone(), - visible, - fs, - next_entry_id, - &mut cx, - ) - .await; + let worktree = + Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx) + .await; project.update(&mut cx, |project, _| { project.loading_local_worktrees.remove(&path); @@ -7503,6 +7805,9 @@ impl Project { } pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { + self.diagnostics.remove(&id_to_remove); + self.diagnostic_summaries.remove(&id_to_remove); + let mut servers_to_remove = HashMap::default(); let mut servers_to_preserve = HashSet::default(); for ((worktree_id, server_name), &server_id) in &self.language_server_ids { @@ -8128,23 +8433,34 @@ impl Project { include_ignored: bool, cx: &'a AppContext, ) -> impl Iterator + 'a { - self.visible_worktrees(cx).flat_map(move |worktree| { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - worktree - .diagnostic_summaries() - .filter_map(move |(path, server_id, summary)| { - if include_ignored - || worktree - .entry_for_path(path.as_ref()) - .map_or(false, |entry| !entry.is_ignored) - { - Some((ProjectPath { worktree_id, path }, server_id, summary)) - } else { - None - } - }) - }) + self.visible_worktrees(cx) + .filter_map(|worktree| { + let worktree = worktree.read(cx); + Some((worktree, self.diagnostic_summaries.get(&worktree.id())?)) + }) + .flat_map(move |(worktree, summaries)| { + let worktree_id = worktree.id(); + summaries + .iter() + .filter(move |(path, _)| { + include_ignored + || worktree + .entry_for_path(path.as_ref()) + .map_or(false, |entry| !entry.is_ignored) + }) + .flat_map(move |(path, summaries)| { + summaries.iter().map(move |(server_id, summary)| { + ( + ProjectPath { + worktree_id, + path: path.clone(), + }, + *server_id, + *summary, + ) + }) + }) + }) } pub fn disk_based_diagnostics_started( @@ -8805,23 +9121,41 @@ impl Project { ) -> Result<()> { this.update(&mut cx, |this, cx| { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { - if let Some(summary) = envelope.payload.summary { - let project_path = ProjectPath { - worktree_id, - path: Path::new(&summary.path).into(), - }; - worktree.update(cx, |worktree, _| { - worktree - .as_remote_mut() - .unwrap() - .update_diagnostic_summary(project_path.path.clone(), &summary); - }); - cx.emit(Event::DiagnosticsUpdated { - language_server_id: LanguageServerId(summary.language_server_id as usize), - path: project_path, - }); + if let Some(message) = envelope.payload.summary { + let project_path = ProjectPath { + worktree_id, + path: Path::new(&message.path).into(), + }; + let path = project_path.path.clone(); + let server_id = LanguageServerId(message.language_server_id as usize); + let summary = DiagnosticSummary { + error_count: message.error_count as usize, + warning_count: message.warning_count as usize, + }; + + if summary.is_empty() { + if let Some(worktree_summaries) = + this.diagnostic_summaries.get_mut(&worktree_id) + { + if let Some(summaries) = worktree_summaries.get_mut(&path) { + summaries.remove(&server_id); + if summaries.is_empty() { + worktree_summaries.remove(&path); + } + } + } + } else { + this.diagnostic_summaries + .entry(worktree_id) + .or_default() + .entry(path) + .or_default() + .insert(server_id, summary); } + cx.emit(Event::DiagnosticsUpdated { + language_server_id: LanguageServerId(message.language_server_id as usize), + path: project_path, + }); } Ok(()) })? @@ -10229,7 +10563,6 @@ impl Project { cx: &mut ModelContext, ) -> 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 @@ -10246,8 +10579,7 @@ impl Project { { self.worktrees.push(WorktreeHandle::Strong(old_worktree)); } else { - let worktree = - Worktree::remote(remote_id, replica_id, worktree, self.client.clone(), cx); + let worktree = Worktree::remote(replica_id, worktree, cx); let _ = self.add_worktree(&worktree, cx); } } @@ -11502,6 +11834,13 @@ async fn wait_for_loading_buffer( } } +fn is_not_found_error(error: &anyhow::Error) -> bool { + error + .root_cause() + .downcast_ref::() + .is_some_and(|err| err.kind() == io::ErrorKind::NotFound) +} + fn include_text(server: &lsp::LanguageServer) -> bool { server .capabilities() @@ -11740,3 +12079,47 @@ fn deserialize_location( }) }) } + +#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)] +pub struct DiagnosticSummary { + pub error_count: usize, + pub warning_count: usize, +} + +impl DiagnosticSummary { + pub fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { + let mut this = Self { + error_count: 0, + warning_count: 0, + }; + + for entry in diagnostics { + if entry.diagnostic.is_primary { + match entry.diagnostic.severity { + DiagnosticSeverity::ERROR => this.error_count += 1, + DiagnosticSeverity::WARNING => this.warning_count += 1, + _ => {} + } + } + } + + this + } + + pub fn is_empty(&self) -> bool { + self.error_count == 0 && self.warning_count == 0 + } + + pub fn to_proto( + &self, + language_server_id: LanguageServerId, + path: &Path, + ) -> proto::DiagnosticSummary { + proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + language_server_id: language_server_id.0 as u64, + error_count: self.error_count as u32, + warning_count: self.warning_count as u32, + } + } +} diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 843a69e9f6..d5096c78a2 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2955,7 +2955,6 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { })); let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await; - let rpc = project.update(cx, |p, _| p.client.clone()); let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx)); @@ -2987,7 +2986,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { let updates = Arc::new(Mutex::new(Vec::new())); tree.update(cx, |tree, cx| { - let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { + tree.as_local_mut().unwrap().observe_updates(0, cx, { let updates = updates.clone(); move |update| { updates.lock().push(update); @@ -2996,7 +2995,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { }); }); - let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx)); + let remote = cx.update(|cx| Worktree::remote(1, metadata, cx)); cx.executor().run_until_parked(); diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index c590c41577..fb6bad9b29 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [features] test-support = [ - "client/test-support", "language/test-support", "settings/test-support", "text/test-support", @@ -24,7 +23,6 @@ test-support = [ [dependencies] anyhow.workspace = true -client.workspace = true clock.workspace = true collections.workspace = true fs.workspace = true @@ -36,7 +34,6 @@ ignore.workspace = true itertools.workspace = true language.workspace = true log.workspace = true -lsp.workspace = true parking_lot.workspace = true postage.workspace = true rpc.workspace = true diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 11527dbd99..e24782bac6 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -5,11 +5,9 @@ mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; -use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use fs::{copy_recursive, RemoveOptions}; -use fs::{Fs, Watcher}; +use fs::{copy_recursive, Fs, RemoveOptions, Watcher}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -21,9 +19,9 @@ use futures::{ FutureExt as _, Stream, StreamExt, }; use fuzzy::CharBag; -use git::status::GitStatus; use git::{ repository::{GitFileStatus, GitRepository, RepoPath}, + status::GitStatus, DOT_GIT, GITIGNORE, }; use gpui::{ @@ -32,21 +30,15 @@ use gpui::{ }; use ignore::IgnoreStack; use itertools::Itertools; -use language::{ - proto::{deserialize_version, serialize_line_ending, serialize_version}, - Buffer, Capability, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, Unclipped, -}; -use lsp::{DiagnosticSeverity, LanguageServerId}; use parking_lot::Mutex; use postage::{ barrier, prelude::{Sink as _, Stream as _}, watch, }; -use serde::Serialize; +use rpc::proto; use settings::{Settings, SettingsLocation, SettingsStore}; use smol::channel::{self, Sender}; -use std::time::Instant; use std::{ any::Any, cmp::{self, Ordering}, @@ -62,10 +54,10 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, - time::{Duration, SystemTime}, + time::{Duration, Instant, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use text::BufferId; +use text::{LineEnding, Rope}; use util::{ paths::{PathMatcher, HOME}, ResultExt, @@ -106,36 +98,16 @@ pub enum CreatedEntry { Excluded { abs_path: PathBuf }, } -#[cfg(any(test, feature = "test-support"))] -impl CreatedEntry { - pub fn to_included(self) -> Option { - match self { - CreatedEntry::Included(entry) => Some(entry), - CreatedEntry::Excluded { .. } => None, - } - } -} - pub struct LocalWorktree { snapshot: LocalSnapshot, scan_requests_tx: channel::Sender, path_prefixes_to_scan_tx: channel::Sender>, is_scanning: (watch::Sender, watch::Receiver), _background_scanner_tasks: Vec>, - share: Option, - diagnostics: HashMap< - Arc, - Vec<( - LanguageServerId, - Vec>>, - )>, - >, - diagnostic_summaries: HashMap, HashMap>, - client: Arc, + update_observer: Option, fs: Arc, fs_case_sensitive: bool, visible: bool, - next_entry_id: Arc, } @@ -147,12 +119,9 @@ struct ScanRequest { pub struct RemoteWorktree { snapshot: Snapshot, background_snapshot: Arc>, - project_id: u64, - client: Arc, updates_tx: Option>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, replica_id: ReplicaId, - diagnostic_summaries: HashMap, HashMap>, visible: bool, disconnected: bool, } @@ -365,7 +334,6 @@ enum ScanState { } struct ShareState { - project_id: u64, snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>, resume_updates: watch::Sender<()>, @@ -382,7 +350,6 @@ impl EventEmitter for Worktree {} impl Worktree { pub async fn local( - client: Arc, path: impl Into>, visible: bool, fs: Arc, @@ -502,7 +469,7 @@ impl Worktree { next_entry_id: Arc::clone(&next_entry_id), snapshot, is_scanning: watch::channel_with(true), - share: None, + update_observer: None, scan_requests_tx, path_prefixes_to_scan_tx, _background_scanner_tasks: start_background_scan_tasks( @@ -514,9 +481,6 @@ impl Worktree { Arc::clone(&fs), cx, ), - diagnostics: Default::default(), - diagnostic_summaries: Default::default(), - client, fs, fs_case_sensitive, visible, @@ -525,10 +489,8 @@ impl Worktree { } pub fn remote( - project_remote_id: u64, replica_id: ReplicaId, worktree: proto::WorktreeMetadata, - client: Arc, cx: &mut AppContext, ) -> Model { cx.new_model(|cx: &mut ModelContext| { @@ -590,14 +552,11 @@ impl Worktree { .detach(); Worktree::Remote(RemoteWorktree { - project_id: project_remote_id, replica_id, snapshot: snapshot.clone(), background_snapshot, updates_tx: Some(updates_tx), snapshot_subscriptions: Default::default(), - client: client.clone(), - diagnostic_summaries: Default::default(), visible: worktree.visible, disconnected: false, }) @@ -679,21 +638,6 @@ impl Worktree { } } - pub fn diagnostic_summaries( - &self, - ) -> impl Iterator, LanguageServerId, DiagnosticSummary)> + '_ { - match self { - Worktree::Local(worktree) => &worktree.diagnostic_summaries, - Worktree::Remote(worktree) => &worktree.diagnostic_summaries, - } - .iter() - .flat_map(|(path, summaries)| { - summaries - .iter() - .map(move |(&server_id, &summary)| (path.clone(), server_id, summary)) - }) - } - pub fn abs_path(&self) -> Arc { match self { Worktree::Local(worktree) => worktree.abs_path.clone(), @@ -807,168 +751,6 @@ impl LocalWorktree { path.starts_with(&self.abs_path) } - pub fn load_buffer( - &mut self, - path: &Path, - cx: &mut ModelContext, - ) -> Task>> { - let path = Arc::from(path); - let reservation = cx.reserve_model(); - let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - cx.spawn(move |this, mut cx| async move { - let (file, contents, diff_base) = this - .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))? - .await?; - let text_buffer = cx - .background_executor() - .spawn(async move { text::Buffer::new(0, buffer_id, contents) }) - .await; - cx.insert_model(reservation, |_| { - Buffer::build( - text_buffer, - diff_base, - Some(Arc::new(file)), - Capability::ReadWrite, - ) - }) - }) - } - - pub fn new_buffer( - &mut self, - path: Arc, - cx: &mut ModelContext, - ) -> Model { - let worktree = cx.handle(); - cx.new_model(|cx| { - let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); - let text_buffer = text::Buffer::new(0, buffer_id, "".into()); - Buffer::build( - text_buffer, - None, - Some(Arc::new(File { - worktree, - path, - mtime: None, - entry_id: None, - is_local: true, - is_deleted: false, - is_private: false, - })), - Capability::ReadWrite, - ) - }) - } - - pub fn diagnostics_for_path( - &self, - path: &Path, - ) -> Vec<( - LanguageServerId, - Vec>>, - )> { - self.diagnostics.get(path).cloned().unwrap_or_default() - } - - pub fn clear_diagnostics_for_language_server( - &mut self, - server_id: LanguageServerId, - _: &mut ModelContext, - ) { - let worktree_id = self.id().to_proto(); - self.diagnostic_summaries - .retain(|path, summaries_by_server_id| { - if summaries_by_server_id.remove(&server_id).is_some() { - if let Some(share) = self.share.as_ref() { - self.client - .send(proto::UpdateDiagnosticSummary { - project_id: share.project_id, - worktree_id, - summary: Some(proto::DiagnosticSummary { - path: path.to_string_lossy().to_string(), - language_server_id: server_id.0 as u64, - error_count: 0, - warning_count: 0, - }), - }) - .log_err(); - } - !summaries_by_server_id.is_empty() - } else { - true - } - }); - - self.diagnostics.retain(|_, diagnostics_by_server_id| { - if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - diagnostics_by_server_id.remove(ix); - !diagnostics_by_server_id.is_empty() - } else { - true - } - }); - } - - pub fn update_diagnostics( - &mut self, - server_id: LanguageServerId, - worktree_path: Arc, - diagnostics: Vec>>, - _: &mut ModelContext, - ) -> Result { - let summaries_by_server_id = self - .diagnostic_summaries - .entry(worktree_path.clone()) - .or_default(); - - let old_summary = summaries_by_server_id - .remove(&server_id) - .unwrap_or_default(); - - let new_summary = DiagnosticSummary::new(&diagnostics); - if new_summary.is_empty() { - if let Some(diagnostics_by_server_id) = self.diagnostics.get_mut(&worktree_path) { - if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - diagnostics_by_server_id.remove(ix); - } - if diagnostics_by_server_id.is_empty() { - self.diagnostics.remove(&worktree_path); - } - } - } else { - summaries_by_server_id.insert(server_id, new_summary); - let diagnostics_by_server_id = - self.diagnostics.entry(worktree_path.clone()).or_default(); - match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - Ok(ix) => { - diagnostics_by_server_id[ix] = (server_id, diagnostics); - } - Err(ix) => { - diagnostics_by_server_id.insert(ix, (server_id, diagnostics)); - } - } - } - - if !old_summary.is_empty() || !new_summary.is_empty() { - if let Some(share) = self.share.as_ref() { - self.client - .send(proto::UpdateDiagnosticSummary { - project_id: share.project_id, - worktree_id: self.id().to_proto(), - summary: Some(proto::DiagnosticSummary { - path: worktree_path.to_string_lossy().to_string(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count as u32, - warning_count: new_summary.warning_count as u32, - }), - }) - .log_err(); - } - } - - Ok(!old_summary.is_empty() || !new_summary.is_empty()) - } - fn restart_background_scanners(&mut self, cx: &mut ModelContext) { let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); @@ -997,7 +779,7 @@ impl LocalWorktree { new_snapshot.share_private_files = self.snapshot.share_private_files; self.snapshot = new_snapshot; - if let Some(share) = self.share.as_mut() { + if let Some(share) = self.update_observer.as_mut() { share .snapshots_tx .unbounded_send(( @@ -1138,7 +920,7 @@ impl LocalWorktree { } } - fn load( + pub fn load_file( &self, path: &Path, cx: &mut ModelContext, @@ -1232,97 +1014,6 @@ impl LocalWorktree { }) } - pub fn save_buffer( - &self, - buffer_handle: Model, - path: Arc, - mut has_changed_file: bool, - cx: &mut ModelContext, - ) -> Task> { - let buffer = buffer_handle.read(cx); - - let rpc = self.client.clone(); - let buffer_id: u64 = buffer.remote_id().into(); - let project_id = self.share.as_ref().map(|share| share.project_id); - - 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 = self.write_file(path.as_ref(), text, buffer.line_ending(), cx); - let fs = Arc::clone(&self.fs); - let abs_path = self.absolutize(&path); - let is_private = self.snapshot.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_dotenv) = 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) - } - }; - - if has_changed_file { - let new_file = Arc::new(File { - entry_id, - worktree: this, - path, - mtime, - is_local: true, - is_deleted: false, - is_private: is_dotenv, - }); - - if let Some(project_id) = project_id { - rpc.send(proto::UpdateBufferFile { - project_id, - buffer_id, - file: Some(new_file.to_proto()), - }) - .log_err(); - } - - buffer_handle.update(&mut cx, |buffer, cx| { - if has_changed_file { - buffer.file_updated(new_file, cx); - } - })?; - } - - if let Some(project_id) = project_id { - rpc.send(proto::BufferSaved { - project_id, - buffer_id, - version: serialize_version(&version), - mtime: mtime.map(|time| time.into()), - })?; - } - - buffer_handle.update(&mut cx, |buffer, cx| { - buffer.did_save(version.clone(), mtime, cx); - })?; - - Ok(()) - }) - } - /// Find the lowest path in the worktree's datastructures that is an ancestor fn lowest_ancestor(&self, path: &Path) -> PathBuf { let mut lowest_ancestor = None; @@ -1400,7 +1091,7 @@ impl LocalWorktree { }) } - pub(crate) fn write_file( + pub fn write_file( &self, path: impl Into>, text: Rope, @@ -1630,8 +1321,7 @@ impl LocalWorktree { project_id: u64, cx: &mut ModelContext, callback: F, - ) -> oneshot::Receiver<()> - where + ) where F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, Fut: Send + Future, { @@ -1640,12 +1330,9 @@ impl LocalWorktree { #[cfg(not(any(test, feature = "test-support")))] const MAX_CHUNK_SIZE: usize = 256; - let (share_tx, share_rx) = oneshot::channel(); - - if let Some(share) = self.share.as_mut() { - share_tx.send(()).ok(); - *share.resume_updates.borrow_mut() = (); - return share_rx; + if let Some(observer) = self.update_observer.as_mut() { + *observer.resume_updates.borrow_mut() = (); + return; } let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>(); @@ -1683,47 +1370,23 @@ impl LocalWorktree { } } } - share_tx.send(()).ok(); Some(()) }); - self.share = Some(ShareState { - project_id, + self.update_observer = Some(ShareState { snapshots_tx, resume_updates: resume_updates_tx, _maintain_remote_snapshot, }); - share_rx } - pub fn share(&mut self, project_id: u64, cx: &mut ModelContext) -> Task> { - let client = self.client.clone(); - - for (path, summaries) in &self.diagnostic_summaries { - for (&server_id, summary) in summaries { - if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id: cx.entity_id().as_u64(), - summary: Some(summary.to_proto(server_id, path)), - }) { - return Task::ready(Err(e)); - } - } - } - - let rx = self.observe_updates(project_id, cx, move |update| { - client.request(update).map(|result| result.is_ok()) - }); - cx.background_executor() - .spawn(async move { rx.await.map_err(|_| anyhow!("share ended")) }) + pub fn stop_observing_updates(&mut self) { + self.update_observer.take(); } - pub fn unshare(&mut self) { - self.share.take(); - } - - pub fn is_shared(&self) -> bool { - self.share.is_some() + #[cfg(any(test, feature = "test-support"))] + pub fn has_update_observer(&self) -> bool { + self.update_observer.is_some() } pub fn share_private_files(&mut self, cx: &mut ModelContext) { @@ -1743,37 +1406,6 @@ impl RemoteWorktree { self.disconnected = true; } - pub fn save_buffer( - &self, - buffer_handle: Model, - new_path: Option, - cx: &mut ModelContext, - ) -> Task> { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id().into(); - let version = buffer.version(); - let rpc = self.client.clone(); - let project_id = self.project_id; - cx.spawn(move |_, mut cx| async move { - let response = rpc - .request(proto::SaveBuffer { - project_id, - buffer_id, - new_path, - version: serialize_version(&version), - }) - .await?; - let version = deserialize_version(&response.version); - let mtime = response.mtime.map(|mtime| mtime.into()); - - buffer_handle.update(&mut cx, |buffer, cx| { - buffer.did_save(version.clone(), mtime, cx); - })?; - - Ok(()) - }) - } - pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { if let Some(updates_tx) = &self.updates_tx { updates_tx @@ -1807,32 +1439,6 @@ impl RemoteWorktree { } } - pub fn update_diagnostic_summary( - &mut self, - path: Arc, - summary: &proto::DiagnosticSummary, - ) { - let server_id = LanguageServerId(summary.language_server_id as usize); - let summary = DiagnosticSummary { - error_count: summary.error_count as usize, - warning_count: summary.warning_count as usize, - }; - - if summary.is_empty() { - if let Some(summaries) = self.diagnostic_summaries.get_mut(&path) { - summaries.remove(&server_id); - if summaries.is_empty() { - self.diagnostic_summaries.remove(&path); - } - } - } else { - self.diagnostic_summaries - .entry(path) - .or_default() - .insert(server_id, summary); - } - } - pub fn insert_entry( &mut self, entry: proto::Entry, @@ -3023,29 +2629,6 @@ impl language::LocalFile for File { cx.background_executor() .spawn(async move { fs.load(&abs_path?).await }) } - - fn buffer_reloaded( - &self, - buffer_id: BufferId, - version: &clock::Global, - line_ending: LineEnding, - mtime: Option, - cx: &mut AppContext, - ) { - let worktree = self.worktree.read(cx).as_local().unwrap(); - if let Some(project_id) = worktree.share.as_ref().map(|share| share.project_id) { - worktree - .client - .send(proto::BufferReloaded { - project_id, - buffer_id: buffer_id.into(), - version: serialize_version(version), - mtime: mtime.map(|time| time.into()), - line_ending: serialize_line_ending(line_ending) as i32, - }) - .log_err(); - } - } } impl File { @@ -5109,46 +4692,12 @@ impl ProjectEntryId { } } -#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)] -pub struct DiagnosticSummary { - pub error_count: usize, - pub warning_count: usize, -} - -impl DiagnosticSummary { - fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { - let mut this = Self { - error_count: 0, - warning_count: 0, - }; - - for entry in diagnostics { - if entry.diagnostic.is_primary { - match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => this.error_count += 1, - DiagnosticSeverity::WARNING => this.warning_count += 1, - _ => {} - } - } - } - - this - } - - pub fn is_empty(&self) -> bool { - self.error_count == 0 && self.warning_count == 0 - } - - pub fn to_proto( - &self, - language_server_id: LanguageServerId, - path: &Path, - ) -> proto::DiagnosticSummary { - proto::DiagnosticSummary { - path: path.to_string_lossy().to_string(), - language_server_id: language_server_id.0 as u64, - error_count: self.error_count as u32, - warning_count: self.warning_count as u32, +#[cfg(any(test, feature = "test-support"))] +impl CreatedEntry { + pub fn to_included(self) -> Option { + match self { + CreatedEntry::Included(entry) => Some(entry), + CreatedEntry::Excluded { .. } => None, } } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 46c7fe5865..14948eca82 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -3,12 +3,9 @@ use crate::{ WorktreeModelHandle, }; use anyhow::Result; -use client::Client; -use clock::FakeSystemClock; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; use git::{repository::GitFileStatus, GITIGNORE}; use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext}; -use http::FakeHttpClient; use parking_lot::Mutex; use postage::stream::Stream; use pretty_assertions::assert_eq; @@ -35,7 +32,6 @@ async fn test_traversal(cx: &mut TestAppContext) { .await; let tree = Worktree::local( - build_client(cx), Path::new("/root"), true, fs, @@ -100,7 +96,6 @@ async fn test_circular_symlinks(cx: &mut TestAppContext) { .unwrap(); let tree = Worktree::local( - build_client(cx), Path::new("/root"), true, fs.clone(), @@ -200,7 +195,6 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { .unwrap(); let tree = Worktree::local( - build_client(cx), Path::new("/root/dir1"), true, fs.clone(), @@ -351,7 +345,6 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) { })); let tree = Worktree::local( - build_client(cx), temp_root.path(), true, fs.clone(), @@ -428,7 +421,6 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { .await; let tree = Worktree::local( - build_client(cx), Path::new("/root"), true, fs.clone(), @@ -461,16 +453,16 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { // Open a file that is nested inside of a gitignored directory that // has not yet been expanded. let prev_read_dir_count = fs.read_dir_call_count(); - let buffer = tree + let (file, _, _) = tree .update(cx, |tree, cx| { tree.as_local_mut() .unwrap() - .load_buffer("one/node_modules/b/b1.js".as_ref(), cx) + .load_file("one/node_modules/b/b1.js".as_ref(), cx) }) .await .unwrap(); - tree.read_with(cx, |tree, cx| { + tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true) .map(|entry| (entry.path.as_ref(), entry.is_ignored)) @@ -491,10 +483,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { ] ); - assert_eq!( - buffer.read(cx).file().unwrap().path().as_ref(), - Path::new("one/node_modules/b/b1.js") - ); + assert_eq!(file.path.as_ref(), Path::new("one/node_modules/b/b1.js")); // Only the newly-expanded directories are scanned. assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2); @@ -503,16 +492,16 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { // Open another file in a different subdirectory of the same // gitignored directory. let prev_read_dir_count = fs.read_dir_call_count(); - let buffer = tree + let (file, _, _) = tree .update(cx, |tree, cx| { tree.as_local_mut() .unwrap() - .load_buffer("one/node_modules/a/a2.js".as_ref(), cx) + .load_file("one/node_modules/a/a2.js".as_ref(), cx) }) .await .unwrap(); - tree.read_with(cx, |tree, cx| { + tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true) .map(|entry| (entry.path.as_ref(), entry.is_ignored)) @@ -535,10 +524,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { ] ); - assert_eq!( - buffer.read(cx).file().unwrap().path().as_ref(), - Path::new("one/node_modules/a/a2.js") - ); + assert_eq!(file.path.as_ref(), Path::new("one/node_modules/a/a2.js")); // Only the newly-expanded directory is scanned. assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1); @@ -591,7 +577,6 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { .await; let tree = Worktree::local( - build_client(cx), Path::new("/root"), true, fs.clone(), @@ -711,7 +696,6 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { .await; let tree = Worktree::local( - build_client(cx), "/root/tree".as_ref(), true, fs.clone(), @@ -793,7 +777,6 @@ async fn test_update_gitignore(cx: &mut TestAppContext) { .await; let tree = Worktree::local( - build_client(cx), "/root".as_ref(), true, fs.clone(), @@ -848,7 +831,6 @@ async fn test_write_file(cx: &mut TestAppContext) { })); let tree = Worktree::local( - build_client(cx), dir.path(), true, Arc::new(RealFs::default()), @@ -928,7 +910,6 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { }); let tree = Worktree::local( - build_client(cx), dir.path(), true, Arc::new(RealFs::default()), @@ -1032,7 +1013,6 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { }); let tree = Worktree::local( - build_client(cx), dir.path(), true, Arc::new(RealFs::default()), @@ -1142,7 +1122,6 @@ async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) { let dot_git_worktree_dir = dir.path().join(".git"); let tree = Worktree::local( - build_client(cx), dot_git_worktree_dir.clone(), true, Arc::new(RealFs::default()), @@ -1181,7 +1160,6 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { .await; let tree = Worktree::local( - build_client(cx), "/root".as_ref(), true, fs, @@ -1194,7 +1172,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { let snapshot1 = tree.update(cx, |tree, cx| { let tree = tree.as_local_mut().unwrap(); let snapshot = Arc::new(Mutex::new(tree.snapshot())); - let _ = tree.observe_updates(0, cx, { + tree.observe_updates(0, cx, { let snapshot = snapshot.clone(); move |update| { snapshot.lock().apply_remote_update(update).unwrap(); @@ -1232,13 +1210,6 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); - let client_fake = cx.update(|cx| { - Client::new( - Arc::new(FakeSystemClock::default()), - FakeHttpClient::with_404_response(), - cx, - ) - }); let fs_fake = FakeFs::new(cx.background_executor.clone()); fs_fake @@ -1251,7 +1222,6 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .await; let tree_fake = Worktree::local( - client_fake, "/root".as_ref(), true, fs_fake, @@ -1280,21 +1250,12 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); }); - let client_real = cx.update(|cx| { - Client::new( - Arc::new(FakeSystemClock::default()), - FakeHttpClient::with_404_response(), - cx, - ) - }); - let fs_real = Arc::new(RealFs::default()); let temp_root = temp_tree(json!({ "a": {} })); let tree_real = Worktree::local( - client_real, temp_root.path(), true, fs_real, @@ -1385,7 +1346,6 @@ async fn test_random_worktree_operations_during_initial_scan( log::info!("generated initial tree"); let worktree = Worktree::local( - build_client(cx), root_dir, true, fs.clone(), @@ -1400,7 +1360,7 @@ async fn test_random_worktree_operations_during_initial_scan( worktree.update(cx, |tree, cx| { check_worktree_change_events(tree, cx); - let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { + tree.as_local_mut().unwrap().observe_updates(0, cx, { let updates = updates.clone(); move |update| { updates.lock().push(update); @@ -1475,7 +1435,6 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) log::info!("generated initial tree"); let worktree = Worktree::local( - build_client(cx), root_dir, true, fs.clone(), @@ -1489,7 +1448,7 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) worktree.update(cx, |tree, cx| { check_worktree_change_events(tree, cx); - let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { + tree.as_local_mut().unwrap().observe_updates(0, cx, { let updates = updates.clone(); move |update| { updates.lock().push(update); @@ -1548,7 +1507,6 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) { let new_worktree = Worktree::local( - build_client(cx), root_dir, true, fs.clone(), @@ -1892,7 +1850,6 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { let root_path = root.path(); let tree = Worktree::local( - build_client(cx), root_path, true, Arc::new(RealFs::default()), @@ -1971,7 +1928,6 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { })); let tree = Worktree::local( - build_client(cx), root.path(), true, Arc::new(RealFs::default()), @@ -2112,7 +2068,6 @@ async fn test_git_status(cx: &mut TestAppContext) { git_commit("Initial commit", &repo); let tree = Worktree::local( - build_client(cx), root.path(), true, Arc::new(RealFs::default()), @@ -2294,7 +2249,6 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { // Open the worktree in subfolder let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2"); let tree = Worktree::local( - build_client(cx), root.path().join(project_root), true, Arc::new(RealFs::default()), @@ -2392,7 +2346,6 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { ); let tree = Worktree::local( - build_client(cx), Path::new("/root"), true, fs.clone(), @@ -2471,12 +2424,6 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { } } -fn build_client(cx: &mut TestAppContext) -> Arc { - let clock = Arc::new(FakeSystemClock::default()); - let http_client = FakeHttpClient::with_404_response(); - cx.update(|cx| Client::new(clock, http_client, cx)) -} - #[track_caller] fn git_init(path: &Path) -> git2::Repository { git2::Repository::init(path).expect("Failed to initialize git repository")