diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7337b7b594..0276268784 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -638,6 +638,7 @@ mod tests { worktree.update(&mut cx, |worktree, cx| { worktree .update_diagnostic_entries( + "lsp".into(), Arc::from("/test/main.rs".as_ref()), None, vec![ @@ -764,6 +765,7 @@ mod tests { worktree.update(&mut cx, |worktree, cx| { worktree .update_diagnostic_entries( + "lsp".into(), Arc::from("/test/a.rs".as_ref()), None, vec![DiagnosticEntry { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 764b5e1278..461c88ae42 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -66,7 +66,7 @@ pub struct Buffer { parsing_in_background: bool, parse_count: usize, remote_selections: TreeMap]>>, - diagnostics: DiagnosticSet, + diagnostic_sets: Vec, diagnostics_update_count: usize, language_server: Option, deferred_ops: OperationQueue, @@ -77,7 +77,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, tree: Option, - diagnostics: HashMap<&'static str, DiagnosticSet>, + diagnostic_sets: Vec, remote_selections: TreeMap]>>, diagnostics_update_count: usize, is_parsing: bool, @@ -115,7 +115,8 @@ struct LanguageServerSnapshot { pub enum Operation { Buffer(text::Operation), UpdateDiagnostics { - diagnostic_set: Arc, + provider_name: String, + diagnostics: Arc<[DiagnosticEntry]>, lamport_timestamp: clock::Lamport, }, UpdateSelections { @@ -298,9 +299,15 @@ impl Buffer { proto::deserialize_selections(selection_set.selections), ); } + let snapshot = this.snapshot(); for diagnostic_set in message.diagnostic_sets { + let (provider_name, entries) = proto::deserialize_diagnostic_set(diagnostic_set); this.apply_diagnostic_update( - Arc::from(proto::deserialize_diagnostics(diagnostic_set)), + DiagnosticSet::from_sorted_entries( + provider_name, + entries.into_iter().cloned(), + &snapshot, + ), cx, ); } @@ -325,7 +332,13 @@ impl Buffer { selections: proto::serialize_selections(selections), }) .collect(), - diagnostics: proto::serialize_diagnostic_set(self.diagnostics.iter()), + diagnostic_sets: self + .diagnostic_sets + .iter() + .map(|set| { + proto::serialize_diagnostic_set(set.provider_name().to_string(), set.iter()) + }) + .collect(), } } @@ -360,7 +373,7 @@ impl Buffer { pending_autoindent: Default::default(), language: None, remote_selections: Default::default(), - diagnostics: Default::default(), + diagnostic_sets: Default::default(), diagnostics_update_count: 0, language_server: None, deferred_ops: OperationQueue::new(), @@ -374,7 +387,7 @@ impl Buffer { text: self.text.snapshot(), tree: self.syntax_tree(), remote_selections: self.remote_selections.clone(), - diagnostics: self.diagnostics.clone(), + diagnostic_sets: self.diagnostic_sets.clone(), diagnostics_update_count: self.diagnostics_update_count, is_parsing: self.parsing_in_background, language: self.language.clone(), @@ -723,11 +736,13 @@ impl Buffer { } pub fn all_diagnostics<'a>(&'a self) -> impl 'a + Iterator> { - self.diagnostics.iter() + // TODO - enforce ordering between sets + self.diagnostic_sets.iter().flat_map(|set| set.iter()) } pub fn update_diagnostics( &mut self, + provider_name: Arc, version: Option, mut diagnostics: Vec>, cx: &mut ModelContext, @@ -809,12 +824,12 @@ impl Buffer { ix += 1; } drop(edits_since_save); - self.diagnostics = DiagnosticSet::new(diagnostics, content); - self.diagnostics_update_count += 1; - cx.notify(); - cx.emit(Event::DiagnosticsUpdated); + + let set = DiagnosticSet::new(provider_name, diagnostics, content); + self.apply_diagnostic_update(set.clone(), cx); Ok(Operation::UpdateDiagnostics { - diagnostics: Arc::from(self.diagnostics.iter().cloned().collect::>()), + provider_name: set.provider_name().to_string(), + diagnostics: set.iter().cloned().collect(), lamport_timestamp: self.text.lamport_clock.tick(), }) } @@ -1303,12 +1318,13 @@ impl Buffer { Operation::Buffer(_) => { unreachable!("buffer operations should never be applied at this layer") } - Operation::UpdateDiagnostics { diagnostics, .. } => { - diagnostics.iter().all(|diagnostic| { - self.text.can_resolve(&diagnostic.range.start) - && self.text.can_resolve(&diagnostic.range.end) - }) - } + Operation::UpdateDiagnostics { + diagnostics: diagnostic_set, + .. + } => diagnostic_set.iter().all(|diagnostic| { + self.text.can_resolve(&diagnostic.range.start) + && self.text.can_resolve(&diagnostic.range.end) + }), Operation::UpdateSelections { selections, .. } => selections .iter() .all(|s| self.can_resolve(&s.start) && self.can_resolve(&s.end)), @@ -1321,8 +1337,20 @@ impl Buffer { Operation::Buffer(_) => { unreachable!("buffer operations should never be applied at this layer") } - Operation::UpdateDiagnostics { diagnostics, .. } => { - self.apply_diagnostic_update(diagnostics, cx); + Operation::UpdateDiagnostics { + provider_name, + diagnostics: diagnostic_set, + .. + } => { + let snapshot = self.snapshot(); + self.apply_diagnostic_update( + DiagnosticSet::from_sorted_entries( + provider_name, + diagnostic_set.iter().cloned(), + &snapshot, + ), + cx, + ); } Operation::UpdateSelections { replica_id, @@ -1342,14 +1370,18 @@ impl Buffer { } } - fn apply_diagnostic_update( - &mut self, - diagnostics: Arc<[DiagnosticEntry]>, - cx: &mut ModelContext, - ) { - self.diagnostics = DiagnosticSet::from_sorted_entries(diagnostics.iter().cloned(), self); + fn apply_diagnostic_update(&mut self, set: DiagnosticSet, cx: &mut ModelContext) { + match self + .diagnostic_sets + .binary_search_by_key(&set.provider_name(), |set| set.provider_name()) + { + Ok(ix) => self.diagnostic_sets[ix] = set.clone(), + Err(ix) => self.diagnostic_sets.insert(ix, set.clone()), + } + self.diagnostics_update_count += 1; cx.notify(); + cx.emit(Event::DiagnosticsUpdated); } #[cfg(not(test))] @@ -1584,10 +1616,7 @@ impl BufferSnapshot { let mut highlights = None; let mut diagnostic_endpoints = Vec::::new(); if let Some(theme) = theme { - for entry in self - .diagnostics - .range::<_, usize>(range.clone(), self, true) - { + for entry in self.diagnostics_in_range::<_, usize>(range.clone()) { diagnostic_endpoints.push(DiagnosticEndpoint { offset: entry.range.start, is_start: true, @@ -1720,14 +1749,20 @@ impl BufferSnapshot { search_range: Range, ) -> impl 'a + Iterator> where - T: 'a + ToOffset, + T: 'a + Clone + ToOffset, O: 'a + FromAnchor, { - self.diagnostics.range(search_range, self, true) + self.diagnostic_sets + .iter() + .flat_map(move |set| set.range(search_range.clone(), self, true)) } pub fn diagnostic_groups(&self) -> Vec> { - self.diagnostics.groups(self) + let mut groups = Vec::new(); + for set in &self.diagnostic_sets { + set.groups(&mut groups, self); + } + groups } pub fn diagnostic_group<'a, O>( @@ -1737,7 +1772,8 @@ impl BufferSnapshot { where O: 'a + FromAnchor, { - self.diagnostics.group(group_id, self) + todo!(); + [].into_iter() } pub fn diagnostics_update_count(&self) -> usize { @@ -1755,7 +1791,7 @@ impl Clone for BufferSnapshot { text: self.text.clone(), tree: self.tree.clone(), remote_selections: self.remote_selections.clone(), - diagnostics: self.diagnostics.clone(), + diagnostic_sets: self.diagnostic_sets.clone(), diagnostics_update_count: self.diagnostics_update_count, is_parsing: self.is_parsing, language: self.language.clone(), diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 2f27593575..7cd13c00dd 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -4,13 +4,14 @@ use std::{ cmp::{Ordering, Reverse}, iter, ops::Range, + sync::Arc, }; use sum_tree::{self, Bias, SumTree}; use text::{Anchor, FromAnchor, PointUtf16, ToOffset}; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct DiagnosticSet { - provider_name: String, + provider_name: Arc, diagnostics: SumTree>, } @@ -40,7 +41,7 @@ impl DiagnosticSet { } pub fn from_sorted_entries( - provider_name: String, + provider_name: impl Into>, iter: I, buffer: &text::BufferSnapshot, ) -> Self @@ -48,12 +49,12 @@ impl DiagnosticSet { I: IntoIterator>, { Self { - provider_name, + provider_name: provider_name.into(), diagnostics: SumTree::from_iter(iter, buffer), } } - pub fn new(provider_name: &'static str, iter: I, buffer: &text::BufferSnapshot) -> Self + pub fn new(provider_name: Arc, iter: I, buffer: &text::BufferSnapshot) -> Self where I: IntoIterator>, { @@ -115,7 +116,7 @@ impl DiagnosticSet { }) } - pub fn groups(&self, buffer: &text::BufferSnapshot) -> Vec> { + pub fn groups(&self, output: &mut Vec>, buffer: &text::BufferSnapshot) { let mut groups = HashMap::default(); for entry in self.diagnostics.iter() { groups @@ -124,27 +125,24 @@ impl DiagnosticSet { .push(entry.clone()); } - let mut groups = groups - .into_values() - .filter_map(|mut entries| { - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap()); - entries - .iter() - .position(|entry| entry.diagnostic.is_primary) - .map(|primary_ix| DiagnosticGroup { - entries, - primary_ix, - }) - }) - .collect::>(); - groups.sort_unstable_by(|a, b| { + let start_ix = output.len(); + output.extend(groups.into_values().filter_map(|mut entries| { + entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap()); + entries + .iter() + .position(|entry| entry.diagnostic.is_primary) + .map(|primary_ix| DiagnosticGroup { + entries, + primary_ix, + }) + })); + output[start_ix..].sort_unstable_by(|a, b| { a.entries[a.primary_ix] .range .start .cmp(&b.entries[b.primary_ix].range.start, buffer) .unwrap() }); - groups } pub fn group<'a, O: FromAnchor>( @@ -158,6 +156,15 @@ impl DiagnosticSet { } } +impl Default for DiagnosticSet { + fn default() -> Self { + Self { + provider_name: "".into(), + diagnostics: Default::default(), + } + } +} + impl sum_tree::Item for DiagnosticEntry { type Summary = Summary; diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 8dcc741e1d..727b6a4d79 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,4 +1,4 @@ -use crate::{diagnostic_set::DiagnosticEntry, Diagnostic, DiagnosticSet, Operation}; +use crate::{diagnostic_set::DiagnosticEntry, Diagnostic, Operation}; use anyhow::{anyhow, Result}; use clock::ReplicaId; use lsp::DiagnosticSeverity; @@ -57,12 +57,16 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation { lamport_timestamp: lamport_timestamp.value, }), Operation::UpdateDiagnostics { - diagnostic_set, + provider_name, + diagnostics, lamport_timestamp, } => proto::operation::Variant::UpdateDiagnosticSet(proto::UpdateDiagnosticSet { replica_id: lamport_timestamp.replica_id as u32, lamport_timestamp: lamport_timestamp.value, - diagnostic_set: Some(serialize_diagnostic_set(&diagnostic_set)), + diagnostic_set: Some(serialize_diagnostic_set( + provider_name.clone(), + diagnostics.iter(), + )), }), }), } @@ -99,11 +103,14 @@ pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec proto::DiagnosticSet { +pub fn serialize_diagnostic_set<'a>( + provider_name: String, + diagnostics: impl IntoIterator>, +) -> proto::DiagnosticSet { proto::DiagnosticSet { - provider_name: set.provider_name().to_string(), - diagnostics: set - .iter() + provider_name, + diagnostics: diagnostics + .into_iter() .map(|entry| proto::Diagnostic { start: Some(serialize_anchor(&entry.range.start)), end: Some(serialize_anchor(&entry.range.end)), @@ -209,8 +216,14 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { }, }, proto::operation::Variant::UpdateDiagnosticSet(message) => { + let (provider_name, diagnostics) = deserialize_diagnostic_set( + message + .diagnostic_set + .ok_or_else(|| anyhow!("missing diagnostic set"))?, + ); Operation::UpdateDiagnostics { - diagnostics: Arc::from(deserialize_diagnostic_set(message.diagnostic_set?)), + provider_name, + diagnostics, lamport_timestamp: clock::Lamport { replica_id: message.replica_id as ReplicaId, value: message.lamport_timestamp, @@ -258,31 +271,37 @@ pub fn deserialize_selections(selections: Vec) -> Arc<[Selecti pub fn deserialize_diagnostic_set( message: proto::DiagnosticSet, - buffer: &BufferSnapshot, -) -> DiagnosticSet { - DiagnosticSet::from_sorted_entries( +) -> (String, Arc<[DiagnosticEntry]>) { + ( message.provider_name, - message.diagnostics.into_iter().filter_map(|diagnostic| { - Some(DiagnosticEntry { - range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?, - diagnostic: Diagnostic { - severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? { - proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR, - proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING, - proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION, - proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT, - proto::diagnostic::Severity::None => return None, + message + .diagnostics + .into_iter() + .filter_map(|diagnostic| { + Some(DiagnosticEntry { + range: deserialize_anchor(diagnostic.start?)? + ..deserialize_anchor(diagnostic.end?)?, + diagnostic: Diagnostic { + severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? + { + proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR, + proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING, + proto::diagnostic::Severity::Information => { + DiagnosticSeverity::INFORMATION + } + proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT, + proto::diagnostic::Severity::None => return None, + }, + message: diagnostic.message, + group_id: diagnostic.group_id as usize, + code: diagnostic.code, + is_valid: diagnostic.is_valid, + is_primary: diagnostic.is_primary, + is_disk_based: diagnostic.is_disk_based, }, - message: diagnostic.message, - group_id: diagnostic.group_id as usize, - code: diagnostic.code, - is_valid: diagnostic.is_valid, - is_primary: diagnostic.is_primary, - is_disk_based: diagnostic.is_disk_based, - }, + }) }) - }), - buffer, + .collect(), ) } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 8a177fc472..73a2bb0bf8 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -512,6 +512,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { // Receive diagnostics for an earlier version of the buffer. buffer .update_diagnostics( + "lsp".into(), Some(open_notification.text_document.version), vec![ DiagnosticEntry { @@ -607,6 +608,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { // Ensure overlapping diagnostics are highlighted correctly. buffer .update_diagnostics( + "lsp".into(), Some(open_notification.text_document.version), vec![ DiagnosticEntry { @@ -697,6 +699,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { buffer.update(&mut cx, |buffer, cx| { buffer .update_diagnostics( + "lsp".into(), Some(change_notification_2.text_document.version), vec![ DiagnosticEntry { @@ -819,7 +822,7 @@ async fn test_preserving_old_group_ids_and_disk_based_diagnostics(mut cx: gpui:: ]; buffer.update(&mut cx, |buffer, cx| { buffer - .update_diagnostics(None, diagnostics.clone(), cx) + .update_diagnostics("lsp".into(), None, diagnostics.clone(), cx) .unwrap(); assert_eq!( buffer @@ -837,7 +840,7 @@ async fn test_preserving_old_group_ids_and_disk_based_diagnostics(mut cx: gpui:: buffer.update(&mut cx, |buffer, cx| { buffer - .update_diagnostics(None, new_diagnostics.clone(), cx) + .update_diagnostics("lsp".into(), None, new_diagnostics.clone(), cx) .unwrap(); assert_eq!( buffer @@ -882,6 +885,7 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { buffer.set_language(Some(Arc::new(rust_lang())), None, cx); buffer .update_diagnostics( + "lsp".into(), None, vec![ DiagnosticEntry { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 02789e2f27..b1151b2d16 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,7 @@ use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; -use language::{Buffer, DiagnosticEntry, Language, LanguageRegistry}; +use language::{Buffer, DiagnosticEntry, LanguageRegistry}; use lsp::DiagnosticSeverity; use postage::{prelude::Stream, watch}; use std::{ @@ -510,13 +510,15 @@ impl Project { if let Some(diagnostic_source) = language.diagnostic_source().cloned() { let worktree_path = worktree.abs_path().clone(); let worktree_handle = worktree_handle.downgrade(); - cx.spawn_weak(|_, cx| async move { + cx.spawn_weak(|_, mut cx| async move { if let Some(diagnostics) = diagnostic_source.diagnose(worktree_path).await.log_err() { if let Some(worktree_handle) = worktree_handle.upgrade(&cx) { worktree_handle.update(&mut cx, |worktree, cx| { - for (path, diagnostics) in diagnostics {} + for (path, diagnostics) in diagnostics { + todo!() + } }) } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8349a2aa27..aa7d76b6ab 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -674,6 +674,7 @@ impl Worktree { pub fn update_lsp_diagnostics( &mut self, + provider_name: Arc, mut params: lsp::PublishDiagnosticsParams, disk_based_sources: &HashSet, cx: &mut ModelContext, @@ -742,11 +743,18 @@ impl Worktree { }) .collect::>(); - self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx) + self.update_diagnostic_entries( + provider_name, + worktree_path, + params.version, + diagnostics, + cx, + ) } pub fn update_diagnostics( &mut self, + provider_name: Arc, mut params: lsp::PublishDiagnosticsParams, disk_based_sources: &HashSet, cx: &mut ModelContext, @@ -815,11 +823,18 @@ impl Worktree { }) .collect::>(); - self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx) + self.update_diagnostic_entries( + provider_name, + worktree_path, + params.version, + diagnostics, + cx, + ) } pub fn update_diagnostic_entries( &mut self, + provider_name: Arc, path: Arc, version: Option, diagnostics: Vec>, @@ -836,7 +851,12 @@ impl Worktree { let (remote_id, operation) = buffer.update(cx, |buffer, cx| { ( buffer.remote_id(), - buffer.update_diagnostics(version, diagnostics.clone(), cx), + buffer.update_diagnostics( + provider_name, + version, + diagnostics.clone(), + cx, + ), ) }); self.send_buffer_update(remote_id, operation?, cx); @@ -1100,6 +1120,8 @@ impl LocalWorktree { return Some(server.clone()); } + let name: Arc = language.name().into(); + if let Some(language_server) = language .start_server(self.abs_path(), cx) .log_err() @@ -1115,15 +1137,23 @@ impl LocalWorktree { smol::block_on(diagnostics_tx.send(params)).ok(); }) .detach(); - cx.spawn_weak(|this, mut cx| async move { - while let Ok(diagnostics) = diagnostics_rx.recv().await { - if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { - handle.update(&mut cx, |this, cx| { - this.update_lsp_diagnostics(diagnostics, &disk_based_sources, cx) + cx.spawn_weak(|this, mut cx| { + let provider_name = name.clone(); + async move { + while let Ok(diagnostics) = diagnostics_rx.recv().await { + if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { + handle.update(&mut cx, |this, cx| { + this.update_lsp_diagnostics( + provider_name.clone(), + diagnostics, + &disk_based_sources, + cx, + ) .log_err(); - }); - } else { - break; + }); + } else { + break; + } } } }) @@ -1187,7 +1217,9 @@ impl LocalWorktree { let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx); buffer.set_language(language, language_server, cx); if let Some(diagnostics) = diagnostics { - buffer.update_diagnostics(None, diagnostics, cx).unwrap(); + buffer + .update_diagnostics(todo!(), None, diagnostics, cx) + .unwrap(); } buffer }); @@ -3908,7 +3940,7 @@ mod tests { worktree .update(&mut cx, |tree, cx| { - tree.update_lsp_diagnostics(message, &Default::default(), cx) + tree.update_lsp_diagnostics("lsp".into(), message, &Default::default(), cx) }) .unwrap(); let buffer = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index bd5d1c384f..7d4adededd 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -401,7 +401,7 @@ mod tests { content: "path/one content".to_string(), history: vec![], selections: vec![], - diagnostics: vec![], + diagnostic_sets: vec![], }), } ); @@ -424,7 +424,7 @@ mod tests { content: "path/two content".to_string(), history: vec![], selections: vec![], - diagnostics: vec![], + diagnostic_sets: vec![], }), } ); @@ -455,7 +455,7 @@ mod tests { content: "path/one content".to_string(), history: vec![], selections: vec![], - diagnostics: vec![], + diagnostic_sets: vec![], }), } } @@ -467,7 +467,7 @@ mod tests { content: "path/two content".to_string(), history: vec![], selections: vec![], - diagnostics: vec![], + diagnostic_sets: vec![], }), } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 713a9a3cac..04a6fc8495 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -392,7 +392,7 @@ mod tests { .read(cx) .worktrees(cx) .iter() - .map(|w| w.read(cx).as_local().unwrap().abs_path()) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); assert_eq!( worktree_roots,