From f43dcd67635acd51dc1109ed6c1aaf556d7e4452 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 19 Jan 2022 14:05:06 -0800 Subject: [PATCH] Move logic for starting language servers to the project --- crates/diagnostics/src/diagnostics.rs | 6 + crates/editor/src/display_map.rs | 14 +- crates/editor/src/editor.rs | 35 +- crates/editor/src/items.rs | 2 +- crates/language/src/buffer.rs | 22 +- crates/language/src/tests.rs | 28 +- crates/project/src/project.rs | 517 +++++++++++++-------- crates/project/src/worktree.rs | 642 ++++++++++---------------- crates/server/src/rpc.rs | 29 +- 9 files changed, 639 insertions(+), 656 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index ed49a3226b3da86668dc931dcc2e82c480ec3c9d..4334429a52e227f63c3536a38349496c93181e9a 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -731,6 +731,8 @@ mod tests { // Create some diagnostics worktree.update(&mut cx, |worktree, cx| { worktree + .as_local_mut() + .unwrap() .update_diagnostic_entries( Arc::from("/test/main.rs".as_ref()), None, @@ -882,6 +884,8 @@ mod tests { // Diagnostics are added for another earlier path. worktree.update(&mut cx, |worktree, cx| { worktree + .as_local_mut() + .unwrap() .update_diagnostic_entries( Arc::from("/test/consts.rs".as_ref()), None, @@ -980,6 +984,8 @@ mod tests { // Diagnostics are added to the first path worktree.update(&mut cx, |worktree, cx| { worktree + .as_local_mut() + .unwrap() .update_diagnostic_entries( Arc::from("/test/consts.rs".as_ref()), None, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index faf770cb1bde70fea6ab314f542395223a3c84c6..8757e7593b58137d57c9cb341149e1e069576726 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -840,7 +840,7 @@ mod tests { ("mod.body".to_string(), Color::red().into()), ("fn.name".to_string(), Color::blue().into()), ]); - let lang = Arc::new( + let language = Arc::new( Language::new( LanguageConfig { name: "Test".to_string(), @@ -857,10 +857,9 @@ mod tests { ) .unwrap(), ); - lang.set_theme(&theme); + language.set_theme(&theme); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -928,7 +927,7 @@ mod tests { ("mod.body".to_string(), Color::red().into()), ("fn.name".to_string(), Color::blue().into()), ]); - let lang = Arc::new( + let language = Arc::new( Language::new( LanguageConfig { name: "Test".to_string(), @@ -945,10 +944,9 @@ mod tests { ) .unwrap(), ); - lang.set_theme(&theme); + language.set_theme(&theme); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 978f0caa578e012c2d90553edd85d2fbc50b721a..8a56170daa54d55bbae10f508414b2e1a66e5488 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -533,9 +533,8 @@ impl Editor { _: &workspace::OpenNew, cx: &mut ViewContext, ) { - let buffer = cx.add_model(|cx| { - Buffer::new(0, "", cx).with_language(Some(language::PLAIN_TEXT.clone()), None, cx) - }); + let buffer = cx + .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx)); workspace.open_item(BufferItemHandle(buffer), cx); } @@ -5746,10 +5745,10 @@ mod tests { #[gpui::test] async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig::default(), Some(tree_sitter_rust::language()), - ))); + )); let text = r#" use mod1::mod2::{mod3, mod4}; @@ -5760,7 +5759,7 @@ mod tests { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -5887,7 +5886,7 @@ mod tests { #[gpui::test] async fn test_autoindent_selections(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new( + let language = Arc::new( Language::new( LanguageConfig { brackets: vec![ @@ -5915,11 +5914,11 @@ mod tests { "#, ) .unwrap(), - )); + ); let text = "fn a() {}"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); editor @@ -5944,7 +5943,7 @@ mod tests { #[gpui::test] async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ BracketPair { @@ -5963,7 +5962,7 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); let text = r#" a @@ -5973,7 +5972,7 @@ mod tests { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -6055,13 +6054,13 @@ mod tests { #[gpui::test] async fn test_toggle_comment(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { line_comment: Some("// ".to_string()), ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); let text = " fn a() { @@ -6072,7 +6071,7 @@ mod tests { " .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); @@ -6323,7 +6322,7 @@ mod tests { #[gpui::test] async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ BracketPair { @@ -6342,7 +6341,7 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); let text = concat!( "{ }\n", // Suppress rustfmt @@ -6352,7 +6351,7 @@ mod tests { "{{} }\n", // ); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 290b8e6d50c4922b0c631da6d3b762298c7c2510..6b170e12e96bac341c2a1e729d56e13c17361ea0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -34,7 +34,7 @@ impl PathOpener for BufferOpener { ) -> Option>>> { let buffer = worktree.open_buffer(project_path.path, cx); let task = cx.spawn(|_, _| async move { - let buffer = buffer.await?; + let buffer = buffer.await?.0; Ok(Box::new(BufferItemHandle(buffer)) as Box) }); Some(task) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4cf0c8f0813eb26cd7309892274f41609fa9b9a8..d0410b47013b872c166a9d30b1541d38f20ea6c7 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -385,13 +385,17 @@ impl Buffer { } } - pub fn with_language( + pub fn with_language(mut self, language: Arc, cx: &mut ModelContext) -> Self { + self.set_language(Some(language), cx); + self + } + + pub fn with_language_server( mut self, - language: Option>, - language_server: Option>, + server: Arc, cx: &mut ModelContext, ) -> Self { - self.set_language(language, language_server, cx); + self.set_language_server(Some(server), cx); self } @@ -523,13 +527,16 @@ impl Buffer { })) } - pub fn set_language( + pub fn set_language(&mut self, language: Option>, cx: &mut ModelContext) { + self.language = language; + self.reparse(cx); + } + + pub fn set_language_server( &mut self, - language: Option>, language_server: Option>, cx: &mut ModelContext, ) { - self.language = language; self.language_server = if let Some(server) = language_server { let (latest_snapshot_tx, mut latest_snapshot_rx) = watch::channel(); Some(LanguageServerState { @@ -611,7 +618,6 @@ impl Buffer { None }; - self.reparse(cx); self.update_language_server(); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index e2ee035c86ac4d36a4cbeeebebe265ab6023a2a6..065ca28cec4d5aa3474e975e6290e3baac377bcf 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -145,9 +145,8 @@ async fn test_apply_diff(mut cx: gpui::TestAppContext) { #[gpui::test] async fn test_reparse(mut cx: gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = cx.add_model(|cx| { - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx) - }); + let buffer = + cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); // Wait for the initial text to parse buffer @@ -280,7 +279,7 @@ async fn test_reparse(mut cx: gpui::TestAppContext) { #[gpui::test] async fn test_outline(mut cx: gpui::TestAppContext) { - let language = Some(Arc::new( + let language = Arc::new( rust_lang() .with_outline_query( r#" @@ -308,7 +307,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) { "#, ) .unwrap(), - )); + ); let text = r#" struct Person { @@ -337,7 +336,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let outline = buffer .read_with(&cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -422,7 +421,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { } " .unindent(); - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx) + Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx) }); let buffer = buffer.read(cx); assert_eq!( @@ -452,8 +451,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { fn test_edit_with_autoindent(cx: &mut MutableAppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit_with_autoindent([8..8], "\n\n", cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); @@ -479,8 +477,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta " .unindent(); - let mut buffer = - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, // their indentation is not adjusted. @@ -529,8 +526,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte " .unindent(); - let mut buffer = - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit_with_autoindent([5..5], "\nb", cx); assert_eq!( @@ -575,7 +571,9 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .unindent(); let buffer = cx.add_model(|cx| { - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx) + Buffer::new(0, text, cx) + .with_language(Arc::new(rust_lang), cx) + .with_language_server(language_server, cx) }); let open_notification = fake @@ -849,7 +847,7 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { ); let mut buffer = Buffer::new(0, text, cx); - buffer.set_language(Some(Arc::new(rust_lang())), None, cx); + buffer.set_language(Some(Arc::new(rust_lang())), cx); buffer .update_diagnostics( None, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4a3046850439370798105912c1be368be791359d..93c91cdb22359d1cf6630f09a1018d2cd019270c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5,7 +5,7 @@ pub mod worktree; use anyhow::{anyhow, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; -use collections::HashMap; +use collections::{hash_map, HashMap, HashSet}; use futures::Future; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ @@ -27,7 +27,7 @@ pub struct Project { worktrees: Vec>, active_entry: Option, languages: Arc, - language_servers: HashMap<(Arc, String), Arc>, + language_servers: HashMap<(WorktreeId, String), Arc>, client: Arc, user_store: ModelHandle, fs: Arc, @@ -63,6 +63,7 @@ pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), DiskBasedDiagnosticsStarted, + DiskBasedDiagnosticsUpdated { worktree_id: WorktreeId }, DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), } @@ -223,7 +224,6 @@ impl Project { worktree, client.clone(), user_store.clone(), - languages.clone(), cx, ) .await?, @@ -463,39 +463,21 @@ impl Project { path: ProjectPath, cx: &mut ModelContext, ) -> Task>> { - if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) { - let language_server = worktree - .read(cx) - .as_local() - .map(|w| w.abs_path().clone()) - .and_then(|worktree_abs_path| { - let abs_path = worktree.abs_path().join(&path.path); - let language = self.languages.select_language(&abs_path).cloned(); - if let Some(language) = language { - if let Some(language_server) = - self.register_language_server(worktree.abs_path(), &language, cx) - { - worktree.register_language(&language, &language_server); - } - } - }); - worktree.update(cx, |worktree, cx| { - if let Some(worktree) = worktree.as_local_mut() { - let abs_path = worktree.abs_path().join(&path.path); - let language = self.languages.select_language(&abs_path).cloned(); - if let Some(language) = language { - if let Some(language_server) = - self.register_language_server(worktree.abs_path(), &language, cx) - { - worktree.register_language(&language, &language_server); - } - } - } - worktree.open_buffer(path.path, cx) - }) + let worktree = if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) { + worktree } else { - cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) }) - } + return cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) }); + }; + let buffer_task = worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx)); + cx.spawn(|this, mut cx| async move { + let (buffer, buffer_is_new) = buffer_task.await?; + if buffer_is_new { + this.update(&mut cx, |this, cx| { + this.buffer_added(worktree, buffer.clone(), cx) + }); + } + Ok(buffer) + }) } pub fn save_buffer_as( @@ -504,189 +486,213 @@ impl Project { abs_path: PathBuf, cx: &mut ModelContext, ) -> Task> { - let result = self.worktree_for_abs_path(&abs_path, cx); - let languages = self.languages.clone(); + let worktree_task = self.worktree_for_abs_path(&abs_path, cx); cx.spawn(|this, mut cx| async move { - let (worktree, path) = result.await?; + let (worktree, path) = worktree_task.await?; worktree .update(&mut cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - let language = languages.select_language(&abs_path); - if let Some(language) = language { - if let Some(language_server) = this.update(cx, |this, cx| { - this.register_language_server(worktree.abs_path(), language, cx) - }) { - worktree.register_language(&language, &language_server); - } - } - - worktree.save_buffer_as(buffer.clone(), path, cx) + worktree + .as_local_mut() + .unwrap() + .save_buffer_as(buffer.clone(), path, cx) }) .await?; + this.update(&mut cx, |this, cx| this.buffer_added(worktree, buffer, cx)); Ok(()) }) } - fn register_language_server( + fn buffer_added( &mut self, - worktree_abs_path: Arc, - language: &Arc, + worktree: ModelHandle, + buffer: ModelHandle, cx: &mut ModelContext, - ) -> Option> { - if let Some(server) = self + ) -> Option<()> { + // Set the buffer's language + let full_path = buffer.read(cx).file()?.full_path(); + let language = self.languages.select_language(&full_path)?.clone(); + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(language.clone()), cx); + }); + + // For local worktrees, start a language server if needed. + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let worktree_abs_path = worktree.as_local()?.abs_path().clone(); + let language_server = match self .language_servers - .get(&(worktree_abs_path.clone(), language.name().to_string())) + .entry((worktree_id, language.name().to_string())) { - return Some(server.clone()); - } + hash_map::Entry::Occupied(e) => Some(e.get().clone()), + hash_map::Entry::Vacant(e) => Self::start_language_server( + self.client.clone(), + language, + worktree_id, + &worktree_abs_path, + cx, + ) + .map(|server| e.insert(server).clone()), + }; + + buffer.update(cx, |buffer, cx| { + buffer.set_language_server(language_server, cx) + }); + + None + } - if let Some(language_server) = language - .start_server(&worktree_abs_path, cx) + fn start_language_server( + rpc: Arc, + language: Arc, + worktree_id: WorktreeId, + worktree_path: &Path, + cx: &mut ModelContext, + ) -> Option> { + let language_server = language + .start_server(worktree_path, cx) .log_err() - .flatten() - { - enum DiagnosticProgress { - Updating, - Updated, - } + .flatten()?; - let disk_based_sources = language - .disk_based_diagnostic_sources() - .cloned() - .unwrap_or_default(); - let disk_based_diagnostics_progress_token = - language.disk_based_diagnostics_progress_token().cloned(); - let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); - let (disk_based_diagnostics_done_tx, disk_based_diagnostics_done_rx) = - smol::channel::unbounded(); - language_server - .on_notification::(move |params| { - smol::block_on(diagnostics_tx.send(params)).ok(); - }) - .detach(); - cx.spawn_weak(|this, mut cx| { - let has_disk_based_diagnostic_progress_token = - disk_based_diagnostics_progress_token.is_some(); - let disk_based_diagnostics_done_tx = disk_based_diagnostics_done_tx.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| { - if !has_disk_based_diagnostic_progress_token { + enum DiagnosticProgress { + Updating, + Publish(lsp::PublishDiagnosticsParams), + Updated, + } + + let disk_based_sources = language + .disk_based_diagnostic_sources() + .cloned() + .unwrap_or_default(); + let disk_based_diagnostics_progress_token = + language.disk_based_diagnostics_progress_token().cloned(); + let has_disk_based_diagnostic_progress_token = + disk_based_diagnostics_progress_token.is_some(); + let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); + + language_server + .on_notification::({ + let diagnostics_tx = diagnostics_tx.clone(); + move |params| { + if !has_disk_based_diagnostic_progress_token { + smol::block_on(diagnostics_tx.send(DiagnosticProgress::Updating)).ok(); + } + smol::block_on(diagnostics_tx.send(DiagnosticProgress::Publish(params))).ok(); + if !has_disk_based_diagnostic_progress_token { + smol::block_on(diagnostics_tx.send(DiagnosticProgress::Updated)).ok(); + } + } + }) + .detach(); + + let mut pending_disk_based_diagnostics: i32 = 0; + language_server + .on_notification::(move |params| { + let token = match params.token { + lsp::NumberOrString::Number(_) => None, + lsp::NumberOrString::String(token) => Some(token), + }; + + if token == disk_based_diagnostics_progress_token { + match params.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + if pending_disk_based_diagnostics == 0 { smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updating), + diagnostics_tx.send(DiagnosticProgress::Updating), ) .ok(); } - this.update_diagnostics(diagnostics, &disk_based_sources, cx) - .log_err(); - if !has_disk_based_diagnostic_progress_token { + pending_disk_based_diagnostics += 1; + } + lsp::WorkDoneProgress::End(_) => { + pending_disk_based_diagnostics -= 1; + if pending_disk_based_diagnostics == 0 { smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updated), + diagnostics_tx.send(DiagnosticProgress::Updated), ) .ok(); } - }) - } else { - break; - } + } + _ => {} + }, } } }) .detach(); - let mut pending_disk_based_diagnostics: i32 = 0; - language_server - .on_notification::(move |params| { - let token = match params.token { - lsp::NumberOrString::Number(_) => None, - lsp::NumberOrString::String(token) => Some(token), - }; - - if token == disk_based_diagnostics_progress_token { - match params.value { - lsp::ProgressParamsValue::WorkDone(progress) => match progress { - lsp::WorkDoneProgress::Begin(_) => { - if pending_disk_based_diagnostics == 0 { - smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updating), - ) - .ok(); - } - pending_disk_based_diagnostics += 1; - } - lsp::WorkDoneProgress::End(_) => { - pending_disk_based_diagnostics -= 1; - if pending_disk_based_diagnostics == 0 { - smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updated), - ) - .ok(); - } - } - _ => {} - }, + cx.spawn_weak(|this, mut cx| async move { + while let Ok(message) = diagnostics_rx.recv().await { + let this = cx.read(|cx| this.upgrade(cx))?; + match message { + DiagnosticProgress::Updating => { + let project_id = this.update(&mut cx, |this, cx| { + cx.emit(Event::DiskBasedDiagnosticsStarted); + this.remote_id() + }); + if let Some(project_id) = project_id { + rpc.send(proto::DiskBasedDiagnosticsUpdating { + project_id, + worktree_id: worktree_id.to_proto(), + }) + .await + .log_err(); } } - }) - .detach(); - let rpc = self.client.clone(); - cx.spawn_weak(|this, mut cx| async move { - while let Ok(progress) = disk_based_diagnostics_done_rx.recv().await { - if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { - match progress { - DiagnosticProgress::Updating => { - let message = handle.update(&mut cx, |this, cx| { - cx.emit(Event::DiskBasedDiagnosticsUpdating); - let this = this.as_local().unwrap(); - this.share.as_ref().map(|share| { - proto::DiskBasedDiagnosticsUpdating { - project_id: share.project_id, - worktree_id: this.id().to_proto(), - } - }) - }); - - if let Some(message) = message { - rpc.send(message).await.log_err(); - } - } - DiagnosticProgress::Updated => { - let message = handle.update(&mut cx, |this, cx| { - cx.emit(Event::DiskBasedDiagnosticsUpdated); - let this = this.as_local().unwrap(); - this.share.as_ref().map(|share| { - proto::DiskBasedDiagnosticsUpdated { - project_id: share.project_id, - worktree_id: this.id().to_proto(), - } - }) - }); - - if let Some(message) = message { - rpc.send(message).await.log_err(); - } - } + DiagnosticProgress::Publish(params) => { + this.update(&mut cx, |this, cx| { + this.update_diagnostics(params, &disk_based_sources, cx) + .log_err(); + }); + } + DiagnosticProgress::Updated => { + let project_id = this.update(&mut cx, |this, cx| { + cx.emit(Event::DiskBasedDiagnosticsFinished); + this.remote_id() + }); + if let Some(project_id) = project_id { + rpc.send(proto::DiskBasedDiagnosticsUpdated { + project_id, + worktree_id: worktree_id.to_proto(), + }) + .await + .log_err(); } - } else { - break; } } - }) - .detach(); + } + Some(()) + }) + .detach(); - self.language_servers.insert( - (worktree_abs_path.clone(), language.name().to_string()), - language_server.clone(), - ); - Some(language_server) - } else { - None + Some(language_server) + } + + fn update_diagnostics( + &mut self, + diagnostics: lsp::PublishDiagnosticsParams, + disk_based_sources: &HashSet, + cx: &mut ModelContext, + ) -> Result<()> { + let path = diagnostics + .uri + .to_file_path() + .map_err(|_| anyhow!("URI is not a file"))?; + for tree in &self.worktrees { + let relative_path = tree.update(cx, |tree, _| { + path.strip_prefix(tree.as_local()?.abs_path()).ok() + }); + if let Some(relative_path) = relative_path { + return tree.update(cx, |tree, cx| { + tree.as_local_mut().unwrap().update_diagnostics( + relative_path.into(), + diagnostics, + disk_based_sources, + cx, + ) + }); + } } + todo!() } pub fn worktree_for_abs_path( @@ -726,12 +732,10 @@ impl Project { let fs = self.fs.clone(); let client = self.client.clone(); let user_store = self.user_store.clone(); - let languages = self.languages.clone(); let path = Arc::from(abs_path.as_ref()); cx.spawn(|project, mut cx| async move { let worktree = - Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx) - .await?; + Worktree::open_local(client.clone(), user_store, path, fs, &mut cx).await?; let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| { project.add_worktree(worktree.clone(), cx); @@ -936,13 +940,11 @@ impl Project { .worktree .ok_or_else(|| anyhow!("invalid worktree"))?; let user_store = self.user_store.clone(); - let languages = self.languages.clone(); cx.spawn(|this, mut cx| { async move { - let worktree = Worktree::remote( - remote_id, replica_id, worktree, client, user_store, languages, &mut cx, - ) - .await?; + let worktree = + Worktree::remote(remote_id, replica_id, worktree, client, user_store, &mut cx) + .await?; this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx)); Ok(()) } @@ -1248,6 +1250,25 @@ impl Entity for Project { } } } + + fn app_will_quit( + &mut self, + _: &mut MutableAppContext, + ) -> Option>>> { + use futures::FutureExt; + + let shutdown_futures = self + .language_servers + .drain() + .filter_map(|(_, server)| server.shutdown()) + .collect::>(); + Some( + async move { + futures::future::join_all(shutdown_futures).await; + } + .boxed(), + ) + } } impl Collaborator { @@ -1275,8 +1296,12 @@ mod tests { use super::*; use client::test::FakeHttpClient; use fs::RealFs; - use gpui::TestAppContext; - use language::LanguageRegistry; + use futures::StreamExt; + use gpui::{test::subscribe, TestAppContext}; + use language::{ + tree_sitter_rust, Diagnostic, LanguageConfig, LanguageRegistry, LanguageServerConfig, Point, + }; + use lsp::Url; use serde_json::json; use std::{os::unix, path::PathBuf}; use util::test::temp_tree; @@ -1344,6 +1369,114 @@ mod tests { ); } + #[gpui::test] + async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) { + let (language_server_config, mut fake_server) = + LanguageServerConfig::fake(cx.background()).await; + let progress_token = language_server_config + .disk_based_diagnostics_progress_token + .clone() + .unwrap(); + let mut languages = LanguageRegistry::new(); + languages.add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + let dir = temp_tree(json!({ + "a.rs": "fn a() { A }", + "b.rs": "const y: i32 = 1", + })); + + let http_client = FakeHttpClient::with_404_response(); + let client = Client::new(http_client.clone()); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + + let tree = Worktree::open_local( + client, + user_store, + dir.path(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + // Cause worktree to start the fake language server + let _buffer = tree + .update(&mut cx, |tree, cx| tree.open_buffer("b.rs", cx)) + .await + .unwrap(); + + let mut events = subscribe(&tree, &mut cx); + + fake_server.start_progress(&progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsUpdating + ); + + fake_server.start_progress(&progress_token).await; + fake_server.end_progress(&progress_token).await; + fake_server.start_progress(&progress_token).await; + + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + ..Default::default() + }], + }) + .await; + assert_eq!( + events.next().await.unwrap(), + Event::DiagnosticsUpdated(Arc::from(Path::new("a.rs"))) + ); + + fake_server.end_progress(&progress_token).await; + fake_server.end_progress(&progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsUpdated + ); + + let (buffer, _) = tree + .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) + .await + .unwrap(); + + buffer.read_with(&cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let diagnostics = snapshot + .diagnostics_in_range::<_, Point>(0..buffer.len()) + .collect::>(); + assert_eq!( + diagnostics, + &[DiagnosticEntry { + range: Point::new(0, 9)..Point::new(0, 10), + diagnostic: Diagnostic { + severity: lsp::DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + group_id: 0, + is_primary: true, + ..Default::default() + } + }] + ) + }); + } + #[gpui::test] async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) { let dir = temp_tree(json!({ diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 06b61b7175dbf5c372d5ef8509d743d7dd5757f3..ab26524473f8d1a5c70763b9ebbadf817bab1418 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4,7 +4,7 @@ use super::{ DiagnosticSummary, ProjectEntry, }; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; @@ -15,11 +15,10 @@ use gpui::{ Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ - range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Language, - LanguageRegistry, Operation, PointUtf16, Rope, + range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Operation, + PointUtf16, Rope, }; use lazy_static::lazy_static; -use lsp::LanguageServer; use parking_lot::Mutex; use postage::{ prelude::{Sink as _, Stream as _}, @@ -37,7 +36,7 @@ use std::{ ops::Deref, path::{Path, PathBuf}, sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, time::{Duration, SystemTime}, @@ -74,29 +73,6 @@ pub enum Event { impl Entity for Worktree { type Event = Event; - - fn app_will_quit( - &mut self, - _: &mut MutableAppContext, - ) -> Option>>> { - use futures::FutureExt; - - if let Self::Local(worktree) = self { - let shutdown_futures = worktree - .language_servers - .drain() - .filter_map(|(_, server)| server.shutdown()) - .collect::>(); - Some( - async move { - futures::future::join_all(shutdown_futures).await; - } - .boxed(), - ) - } else { - None - } - } } impl Worktree { @@ -105,11 +81,10 @@ impl Worktree { user_store: ModelHandle, path: impl Into>, fs: Arc, - languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { let (tree, scan_states_tx) = - LocalWorktree::new(client, user_store, path, fs.clone(), languages, cx).await?; + LocalWorktree::new(client, user_store, path, fs.clone(), cx).await?; tree.update(cx, |tree, cx| { let tree = tree.as_local_mut().unwrap(); let abs_path = tree.snapshot.abs_path.clone(); @@ -131,7 +106,6 @@ impl Worktree { worktree: proto::Worktree, client: Arc, user_store: ModelHandle, - languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { let remote_id = worktree.id; @@ -238,7 +212,6 @@ impl Worktree { loading_buffers: Default::default(), open_buffers: Default::default(), queued_operations: Default::default(), - languages, user_store, diagnostic_summaries, }) @@ -306,13 +279,6 @@ impl Worktree { } } - pub fn languages(&self) -> &Arc { - match self { - Worktree::Local(worktree) => &worktree.language_registry, - Worktree::Remote(worktree) => &worktree.languages, - } - } - pub fn user_store(&self) -> &ModelHandle { match self { Worktree::Local(worktree) => &worktree.user_store, @@ -379,7 +345,7 @@ impl Worktree { &mut self, path: impl AsRef, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task, bool)>> { let path = path.as_ref(); // If there is already a buffer for the given path, then return it. @@ -388,9 +354,10 @@ impl Worktree { Worktree::Remote(worktree) => worktree.get_open_buffer(path, cx), }; if let Some(existing_buffer) = existing_buffer { - return cx.spawn(move |_, _| async move { Ok(existing_buffer) }); + return cx.spawn(move |_, _| async move { Ok((existing_buffer, false)) }); } + let is_new = Arc::new(AtomicBool::new(true)); let path: Arc = Arc::from(path); let mut loading_watch = match self.loading_buffers().entry(path.clone()) { // If the given path is already being loaded, then wait for that existing @@ -412,7 +379,10 @@ impl Worktree { // After the buffer loads, record the fact that it is no longer // loading. this.update(&mut cx, |this, _| this.loading_buffers().remove(&path)); - *tx.borrow_mut() = Some(result.map_err(|e| Arc::new(e))); + *tx.borrow_mut() = Some(match result { + Ok(buffer) => Ok((buffer, is_new)), + Err(error) => Err(Arc::new(error)), + }); }) .detach(); rx @@ -422,7 +392,10 @@ impl Worktree { cx.spawn(|_, _| async move { loop { if let Some(result) = loading_watch.borrow().as_ref() { - return result.clone().map_err(|e| anyhow!("{}", e)); + return match result { + Ok((buf, is_new)) => Ok((buf.clone(), is_new.fetch_and(false, SeqCst))), + Err(error) => Err(anyhow!("{}", error)), + }; } loading_watch.recv().await; } @@ -731,179 +704,6 @@ impl Worktree { } } - pub fn update_diagnostics( - &mut self, - params: lsp::PublishDiagnosticsParams, - disk_based_sources: &HashSet, - cx: &mut ModelContext, - ) -> Result<()> { - let this = self.as_local_mut().ok_or_else(|| anyhow!("not local"))?; - let abs_path = params - .uri - .to_file_path() - .map_err(|_| anyhow!("URI is not a file"))?; - let worktree_path = Arc::from( - abs_path - .strip_prefix(&this.abs_path) - .context("path is not within worktree")?, - ); - - let mut next_group_id = 0; - let mut diagnostics = Vec::default(); - let mut primary_diagnostic_group_ids = HashMap::default(); - let mut sources_by_group_id = HashMap::default(); - let mut supporting_diagnostic_severities = HashMap::default(); - for diagnostic in ¶ms.diagnostics { - let source = diagnostic.source.as_ref(); - let code = diagnostic.code.as_ref().map(|code| match code { - lsp::NumberOrString::Number(code) => code.to_string(), - lsp::NumberOrString::String(code) => code.clone(), - }); - let range = range_from_lsp(diagnostic.range); - let is_supporting = diagnostic - .related_information - .as_ref() - .map_or(false, |infos| { - infos.iter().any(|info| { - primary_diagnostic_group_ids.contains_key(&( - source, - code.clone(), - range_from_lsp(info.location.range), - )) - }) - }); - - if is_supporting { - if let Some(severity) = diagnostic.severity { - supporting_diagnostic_severities - .insert((source, code.clone(), range), severity); - } - } else { - let group_id = post_inc(&mut next_group_id); - let is_disk_based = - source.map_or(false, |source| disk_based_sources.contains(source)); - - sources_by_group_id.insert(group_id, source); - primary_diagnostic_group_ids - .insert((source, code.clone(), range.clone()), group_id); - - diagnostics.push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - code: code.clone(), - severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), - message: diagnostic.message.clone(), - group_id, - is_primary: true, - is_valid: true, - is_disk_based, - }, - }); - if let Some(infos) = &diagnostic.related_information { - for info in infos { - if info.location.uri == params.uri { - let range = range_from_lsp(info.location.range); - diagnostics.push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - code: code.clone(), - severity: DiagnosticSeverity::INFORMATION, - message: info.message.clone(), - group_id, - is_primary: false, - is_valid: true, - is_disk_based, - }, - }); - } - } - } - } - } - - for entry in &mut diagnostics { - let diagnostic = &mut entry.diagnostic; - if !diagnostic.is_primary { - let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap(); - if let Some(&severity) = supporting_diagnostic_severities.get(&( - source, - diagnostic.code.clone(), - entry.range.clone(), - )) { - diagnostic.severity = severity; - } - } - } - - self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?; - Ok(()) - } - - pub fn update_diagnostic_entries( - &mut self, - worktree_path: Arc, - version: Option, - diagnostics: Vec>, - cx: &mut ModelContext, - ) -> Result<()> { - let this = self.as_local_mut().unwrap(); - for buffer in this.open_buffers.values() { - if let Some(buffer) = buffer.upgrade(cx) { - if buffer - .read(cx) - .file() - .map_or(false, |file| *file.path() == worktree_path) - { - let (remote_id, operation) = buffer.update(cx, |buffer, cx| { - ( - buffer.remote_id(), - buffer.update_diagnostics(version, diagnostics.clone(), cx), - ) - }); - self.send_buffer_update(remote_id, operation?, cx); - break; - } - } - } - - let this = self.as_local_mut().unwrap(); - let summary = DiagnosticSummary::new(&diagnostics); - this.diagnostic_summaries - .insert(PathKey(worktree_path.clone()), summary.clone()); - this.diagnostics.insert(worktree_path.clone(), diagnostics); - - cx.emit(Event::DiagnosticsUpdated(worktree_path.clone())); - - if let Some(share) = this.share.as_ref() { - cx.foreground() - .spawn({ - let client = this.client.clone(); - let project_id = share.project_id; - let worktree_id = this.id().to_proto(); - let path = worktree_path.to_string_lossy().to_string(); - async move { - client - .send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id, - summary: Some(proto::DiagnosticSummary { - path, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - info_count: summary.info_count as u32, - hint_count: summary.hint_count as u32, - }), - }) - .await - .log_err() - } - }) - .detach(); - } - - Ok(()) - } - fn send_buffer_update( &mut self, buffer_id: u64, @@ -998,11 +798,9 @@ pub struct LocalWorktree { diagnostics: HashMap, Vec>>, diagnostic_summaries: TreeMap, queued_operations: Vec<(u64, Operation)>, - language_registry: Arc, client: Arc, user_store: ModelHandle, fs: Arc, - language_servers: HashMap>, } struct ShareState { @@ -1020,7 +818,6 @@ pub struct RemoteWorktree { replica_id: ReplicaId, loading_buffers: LoadingBuffers, open_buffers: HashMap, - languages: Arc, user_store: ModelHandle, queued_operations: Vec<(u64, Operation)>, diagnostic_summaries: TreeMap, @@ -1028,7 +825,9 @@ pub struct RemoteWorktree { type LoadingBuffers = HashMap< Arc, - postage::watch::Receiver, Arc>>>, + postage::watch::Receiver< + Option, Arc), Arc>>, + >, >; #[derive(Default, Deserialize)] @@ -1042,7 +841,6 @@ impl LocalWorktree { user_store: ModelHandle, path: impl Into>, fs: Arc, - languages: Arc, cx: &mut AsyncAppContext, ) -> Result<(ModelHandle, Sender)> { let abs_path = path.into(); @@ -1105,11 +903,9 @@ impl LocalWorktree { diagnostics: Default::default(), diagnostic_summaries: Default::default(), queued_operations: Default::default(), - language_registry: languages, client, user_store, fs, - language_servers: Default::default(), }; cx.spawn_weak(|this, mut cx| async move { @@ -1149,19 +945,6 @@ impl LocalWorktree { self.config.collaborators.clone() } - pub fn language_registry(&self) -> &LanguageRegistry { - &self.language_registry - } - - pub fn register_language( - &mut self, - language: &Arc, - language_server: &Arc, - ) { - self.language_servers - .insert(language.name().to_string(), language_server.clone()); - } - fn get_open_buffer( &mut self, path: &Path, @@ -1195,23 +978,13 @@ impl LocalWorktree { .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) .await?; - let (diagnostics, language, language_server) = this.update(&mut cx, |this, _| { - let this = this.as_local_mut().unwrap(); - let diagnostics = this.diagnostics.get(&path).cloned(); - let language = this - .language_registry - .select_language(file.full_path()) - .cloned(); - let server = language - .as_ref() - .and_then(|language| this.language_servers.get(language.name()).cloned()); - (diagnostics, language, server) + let diagnostics = this.update(&mut cx, |this, _| { + this.as_local_mut().unwrap().diagnostics.get(&path).cloned() }); let mut buffer_operations = Vec::new(); let buffer = cx.add_model(|cx| { let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx); - buffer.set_language(language, language_server, cx); if let Some(diagnostics) = diagnostics { let op = buffer.update_diagnostics(None, diagnostics, cx).unwrap(); buffer_operations.push(op); @@ -1239,7 +1012,7 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { let peer_id = envelope.original_sender_id(); let path = Path::new(&envelope.payload.path); - let buffer = this + let (buffer, _) = this .update(&mut cx, |this, cx| this.open_buffer(path, cx)) .await?; this.update(&mut cx, |this, cx| { @@ -1285,6 +1058,201 @@ impl LocalWorktree { cx.notify(); } + pub fn update_diagnostics( + &mut self, + worktree_path: Arc, + params: lsp::PublishDiagnosticsParams, + disk_based_sources: &HashSet, + cx: &mut ModelContext, + ) -> Result<()> { + let mut next_group_id = 0; + let mut diagnostics = Vec::default(); + let mut primary_diagnostic_group_ids = HashMap::default(); + let mut sources_by_group_id = HashMap::default(); + let mut supporting_diagnostic_severities = HashMap::default(); + for diagnostic in ¶ms.diagnostics { + let source = diagnostic.source.as_ref(); + let code = diagnostic.code.as_ref().map(|code| match code { + lsp::NumberOrString::Number(code) => code.to_string(), + lsp::NumberOrString::String(code) => code.clone(), + }); + let range = range_from_lsp(diagnostic.range); + let is_supporting = diagnostic + .related_information + .as_ref() + .map_or(false, |infos| { + infos.iter().any(|info| { + primary_diagnostic_group_ids.contains_key(&( + source, + code.clone(), + range_from_lsp(info.location.range), + )) + }) + }); + + if is_supporting { + if let Some(severity) = diagnostic.severity { + supporting_diagnostic_severities + .insert((source, code.clone(), range), severity); + } + } else { + let group_id = post_inc(&mut next_group_id); + let is_disk_based = + source.map_or(false, |source| disk_based_sources.contains(source)); + + sources_by_group_id.insert(group_id, source); + primary_diagnostic_group_ids + .insert((source, code.clone(), range.clone()), group_id); + + diagnostics.push(DiagnosticEntry { + range, + diagnostic: Diagnostic { + code: code.clone(), + severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), + message: diagnostic.message.clone(), + group_id, + is_primary: true, + is_valid: true, + is_disk_based, + }, + }); + if let Some(infos) = &diagnostic.related_information { + for info in infos { + if info.location.uri == params.uri { + let range = range_from_lsp(info.location.range); + diagnostics.push(DiagnosticEntry { + range, + diagnostic: Diagnostic { + code: code.clone(), + severity: DiagnosticSeverity::INFORMATION, + message: info.message.clone(), + group_id, + is_primary: false, + is_valid: true, + is_disk_based, + }, + }); + } + } + } + } + } + + for entry in &mut diagnostics { + let diagnostic = &mut entry.diagnostic; + if !diagnostic.is_primary { + let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap(); + if let Some(&severity) = supporting_diagnostic_severities.get(&( + source, + diagnostic.code.clone(), + entry.range.clone(), + )) { + diagnostic.severity = severity; + } + } + } + + self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?; + Ok(()) + } + + pub fn update_diagnostic_entries( + &mut self, + worktree_path: Arc, + version: Option, + diagnostics: Vec>, + cx: &mut ModelContext, + ) -> Result<()> { + for buffer in self.open_buffers.values() { + if let Some(buffer) = buffer.upgrade(cx) { + if buffer + .read(cx) + .file() + .map_or(false, |file| *file.path() == worktree_path) + { + let (remote_id, operation) = buffer.update(cx, |buffer, cx| { + ( + buffer.remote_id(), + buffer.update_diagnostics(version, diagnostics.clone(), cx), + ) + }); + self.send_buffer_update(remote_id, operation?, cx); + break; + } + } + } + + let summary = DiagnosticSummary::new(&diagnostics); + self.diagnostic_summaries + .insert(PathKey(worktree_path.clone()), summary.clone()); + self.diagnostics.insert(worktree_path.clone(), diagnostics); + + cx.emit(Event::DiagnosticsUpdated(worktree_path.clone())); + + if let Some(share) = self.share.as_ref() { + cx.foreground() + .spawn({ + let client = self.client.clone(); + let project_id = share.project_id; + let worktree_id = self.id().to_proto(); + let path = worktree_path.to_string_lossy().to_string(); + async move { + client + .send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id, + summary: Some(proto::DiagnosticSummary { + path, + error_count: summary.error_count as u32, + warning_count: summary.warning_count as u32, + info_count: summary.info_count as u32, + hint_count: summary.hint_count as u32, + }), + }) + .await + .log_err() + } + }) + .detach(); + } + + Ok(()) + } + + fn send_buffer_update( + &mut self, + buffer_id: u64, + operation: Operation, + cx: &mut ModelContext, + ) -> Option<()> { + let share = self.share.as_ref()?; + let project_id = share.project_id; + let worktree_id = self.id(); + let rpc = self.client.clone(); + cx.spawn(|worktree, mut cx| async move { + if let Err(error) = rpc + .request(proto::UpdateBuffer { + project_id, + worktree_id: worktree_id.0 as u64, + buffer_id, + operations: vec![language::proto::serialize_operation(&operation)], + }) + .await + { + worktree.update(&mut cx, |worktree, _| { + log::error!("error sending buffer operation: {}", error); + worktree + .as_local_mut() + .unwrap() + .queued_operations + .push((buffer_id, operation)); + }); + } + }) + .detach(); + None + } + pub fn scan_complete(&self) -> impl Future { let mut scan_state_rx = self.last_scan_state_rx.clone(); async move { @@ -1375,21 +1343,8 @@ impl LocalWorktree { } }); - let (language, language_server) = this.update(&mut cx, |worktree, _| { - let worktree = worktree.as_local_mut().unwrap(); - let language = worktree - .language_registry() - .select_language(file.full_path()) - .cloned(); - let language_server = language - .as_ref() - .and_then(|language| worktree.language_servers.get(language.name()).cloned()); - (language, language_server.clone()) - }); - buffer_handle.update(&mut cx, |buffer, cx| { buffer.did_save(version, file.mtime, Some(Box::new(file)), cx); - buffer.set_language(language, language_server, cx); }); Ok(()) @@ -1578,16 +1533,10 @@ impl RemoteWorktree { mtime: entry.mtime, is_local: false, }; - let language = this.read_with(&cx, |this, _| { - use language::File; - this.languages().select_language(file.full_path()).cloned() - }); let remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?; let buffer_id = remote_buffer.id as usize; let buffer = cx.add_model(|cx| { - Buffer::from_proto(replica_id, remote_buffer, Some(Box::new(file)), cx) - .unwrap() - .with_language(language, None, cx) + Buffer::from_proto(replica_id, remote_buffer, Some(Box::new(file)), cx).unwrap() }); this.update(&mut cx, move |this, cx| { let this = this.as_remote_mut().unwrap(); @@ -3129,9 +3078,7 @@ mod tests { use anyhow::Result; use client::test::{FakeHttpClient, FakeServer}; use fs::RealFs; - use gpui::test::subscribe; - use language::{tree_sitter_rust, DiagnosticEntry, Language, LanguageServerConfig}; - use language::{Diagnostic, LanguageConfig}; + use language::{Diagnostic, DiagnosticEntry}; use lsp::Url; use rand::prelude::*; use serde_json::json; @@ -3169,7 +3116,6 @@ mod tests { user_store, Arc::from(Path::new("/root")), Arc::new(fs), - Default::default(), &mut cx.to_async(), ) .await @@ -3207,12 +3153,11 @@ mod tests { user_store, dir.path(), Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await .unwrap(); - let buffer = tree + let (buffer, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file1", cx)) .await .unwrap(); @@ -3242,7 +3187,6 @@ mod tests { user_store, file_path.clone(), Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3251,7 +3195,7 @@ mod tests { .await; cx.read(|cx| assert_eq!(tree.read(cx).file_count(), 1)); - let buffer = tree + let (buffer, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("", cx)) .await .unwrap(); @@ -3291,7 +3235,6 @@ mod tests { user_store.clone(), dir.path(), Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3299,7 +3242,7 @@ mod tests { let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { let buffer = tree.update(cx, |tree, cx| tree.open_buffer(path, cx)); - async move { buffer.await.unwrap() } + async move { buffer.await.unwrap().0 } }; let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { tree.read_with(cx, |tree, _| { @@ -3330,7 +3273,6 @@ mod tests { initial_snapshot.to_proto(&Default::default()), Client::new(http_client.clone()), user_store, - Default::default(), &mut cx.to_async(), ) .await @@ -3443,7 +3385,6 @@ mod tests { user_store, dir.path(), Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3496,7 +3437,6 @@ mod tests { user_store, "/the-dir".as_ref(), fs, - Default::default(), &mut cx.to_async(), ) .await @@ -3511,9 +3451,9 @@ mod tests { ) }); - let buffer_a_1 = buffer_a_1.await.unwrap(); - let buffer_a_2 = buffer_a_2.await.unwrap(); - let buffer_b = buffer_b.await.unwrap(); + let buffer_a_1 = buffer_a_1.await.unwrap().0; + let buffer_a_2 = buffer_a_2.await.unwrap().0; + let buffer_b = buffer_b.await.unwrap().0; assert_eq!(buffer_a_1.read_with(&cx, |b, _| b.text()), "a-contents"); assert_eq!(buffer_b.read_with(&cx, |b, _| b.text()), "b-contents"); @@ -3526,7 +3466,8 @@ mod tests { let buffer_a_3 = worktree .update(&mut cx, |worktree, cx| worktree.open_buffer("a.txt", cx)) .await - .unwrap(); + .unwrap() + .0; // There's still only one buffer per path. assert_eq!(buffer_a_3.id(), buffer_a_id); @@ -3550,7 +3491,6 @@ mod tests { user_store, dir.path(), Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3559,7 +3499,7 @@ mod tests { cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; - let buffer1 = tree + let (buffer1, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file1", cx)) .await .unwrap(); @@ -3626,7 +3566,7 @@ mod tests { // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); - let buffer2 = tree + let (buffer2, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file2", cx)) .await .unwrap(); @@ -3647,7 +3587,7 @@ mod tests { // When a file is already dirty when deleted, we don't emit a Dirtied event. let events = Rc::new(RefCell::new(Vec::new())); - let buffer3 = tree + let (buffer3, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file3", cx)) .await .unwrap(); @@ -3687,7 +3627,6 @@ mod tests { user_store, dir.path(), Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3696,7 +3635,7 @@ mod tests { .await; let abs_path = dir.path().join("the-file"); - let buffer = tree + let (buffer, _) = tree .update(&mut cx, |tree, cx| { tree.open_buffer(Path::new("the-file"), cx) }) @@ -3775,115 +3714,6 @@ mod tests { .await; } - #[gpui::test] - async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) { - let (language_server_config, mut fake_server) = - LanguageServerConfig::fake(cx.background()).await; - let progress_token = language_server_config - .disk_based_diagnostics_progress_token - .clone() - .unwrap(); - let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); - - let dir = temp_tree(json!({ - "a.rs": "fn a() { A }", - "b.rs": "const y: i32 = 1", - })); - - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - - let tree = Worktree::open_local( - client, - user_store, - dir.path(), - Arc::new(RealFs), - Arc::new(languages), - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - // Cause worktree to start the fake language server - let _buffer = tree - .update(&mut cx, |tree, cx| tree.open_buffer("b.rs", cx)) - .await - .unwrap(); - - let mut events = subscribe(&tree, &mut cx); - - fake_server.start_progress(&progress_token).await; - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsUpdating - ); - - fake_server.start_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; - fake_server.start_progress(&progress_token).await; - - fake_server - .notify::(lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - ..Default::default() - }], - }) - .await; - assert_eq!( - events.next().await.unwrap(), - Event::DiagnosticsUpdated(Arc::from(Path::new("a.rs"))) - ); - - fake_server.end_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsUpdated - ); - - let buffer = tree - .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) - .await - .unwrap(); - - buffer.read_with(&cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let diagnostics = snapshot - .diagnostics_in_range::<_, Point>(0..buffer.len()) - .collect::>(); - assert_eq!( - diagnostics, - &[DiagnosticEntry { - range: Point::new(0, 9)..Point::new(0, 10), - diagnostic: Diagnostic { - severity: lsp::DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - group_id: 0, - is_primary: true, - ..Default::default() - } - }] - ) - }); - } - #[gpui::test] async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) { let fs = Arc::new(FakeFs::new()); @@ -3911,13 +3741,12 @@ mod tests { user_store, "/the-dir".as_ref(), fs, - Default::default(), &mut cx.to_async(), ) .await .unwrap(); - let buffer = worktree + let (buffer, _) = worktree .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) .await .unwrap(); @@ -4024,7 +3853,12 @@ mod tests { worktree .update(&mut cx, |tree, cx| { - tree.update_diagnostics(message, &Default::default(), cx) + tree.as_local_mut().unwrap().update_diagnostics( + Arc::from("a.rs".as_ref()), + message, + &Default::default(), + cx, + ) }) .unwrap(); let buffer = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0c1f6a52873de6f96f765dc6befd92aea16741eb..29480fb4920054ef0800c850909173e0770b2090 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1213,7 +1213,8 @@ mod tests { let buffer_b = worktree_b .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx)) .await - .unwrap(); + .unwrap() + .0; let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx)); buffer_b.read_with(&cx_b, |buf, cx| { assert_eq!(buf.read(cx).text(), "b-contents") @@ -1222,7 +1223,8 @@ mod tests { let buffer_a = worktree_a .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx)) .await - .unwrap(); + .unwrap() + .0; let editor_b = cx_b.add_view(window_b, |cx| { Editor::for_buffer(buffer_b, Arc::new(|cx| EditorSettings::test(cx)), cx) @@ -1436,11 +1438,13 @@ mod tests { let buffer_b = worktree_b .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) .await - .unwrap(); + .unwrap() + .0; let buffer_c = worktree_c .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx)) .await - .unwrap(); + .unwrap() + .0; buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx)); @@ -1448,7 +1452,8 @@ mod tests { let buffer_a = worktree_a .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx)) .await - .unwrap(); + .unwrap() + .0; buffer_a .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") @@ -1574,7 +1579,8 @@ mod tests { let buffer_b = worktree_b .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) .await - .unwrap(); + .unwrap() + .0; let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime()); buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx)); @@ -1669,7 +1675,8 @@ mod tests { let buffer_a = worktree_a .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) .await - .unwrap(); + .unwrap() + .0; // Start opening the same buffer as client B let buffer_b = cx_b @@ -1681,7 +1688,7 @@ mod tests { buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); - let buffer_b = buffer_b.await.unwrap(); + let buffer_b = buffer_b.await.unwrap().0; buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; } @@ -2016,7 +2023,8 @@ mod tests { .background() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx))) .await - .unwrap(); + .unwrap() + .0; buffer_b.read_with(&cx_b, |buffer, _| { assert_eq!( @@ -2128,7 +2136,8 @@ mod tests { .background() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx))) .await - .unwrap(); + .unwrap() + .0; let format = buffer_b.update(&mut cx_b, |buffer, cx| buffer.format(cx)); let (request_id, _) = fake_language_server