WIP: Use `cargo check` for on-disk diagnostics

Antonio Scandurra , Nathan Sobo , and Max Brunsfeld created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>

Change summary

Cargo.lock                      |  1 +
crates/editor/src/items.rs      |  4 ++--
crates/language/Cargo.toml      |  1 +
crates/language/src/language.rs | 26 +++++++++++++++++++++++++-
crates/lsp/src/lsp.rs           |  6 +++++-
crates/project/src/project.rs   | 29 +++++++++++++++++++++++++++--
crates/project/src/worktree.rs  | 33 +++++++++++++++++++++++----------
7 files changed, 84 insertions(+), 16 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2605,6 +2605,7 @@ name = "language"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-trait",
  "clock",
  "collections",
  "ctor",

crates/editor/src/items.rs 🔗

@@ -162,12 +162,12 @@ impl ItemView for Editor {
                     let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
                         let worktree = worktree.as_local_mut().unwrap();
                         let language = worktree
-                            .languages()
+                            .language_registry()
                             .select_language(new_file.full_path())
                             .cloned();
                         let language_server = language
                             .as_ref()
-                            .and_then(|language| worktree.ensure_language_server(language, cx));
+                            .and_then(|language| worktree.register_language(language, cx));
                         (language, language_server.clone())
                     });
 

crates/language/Cargo.toml 🔗

@@ -27,6 +27,7 @@ text = { path = "../text" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
+async-trait = "0.1"
 futures = "0.3"
 lazy_static = "1.4"
 log = "0.4"

crates/language/src/language.rs 🔗

@@ -6,6 +6,7 @@ pub mod proto;
 mod tests;
 
 use anyhow::{anyhow, Result};
+use async_trait::async_trait;
 pub use buffer::Operation;
 pub use buffer::*;
 use collections::HashSet;
@@ -15,7 +16,11 @@ use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Deserialize;
-use std::{path::Path, str, sync::Arc};
+use std::{
+    path::{Path, PathBuf},
+    str,
+    sync::Arc,
+};
 use theme::SyntaxTheme;
 use tree_sitter::{self, Query};
 pub use tree_sitter::{Parser, Tree};
@@ -59,9 +64,18 @@ pub struct BracketPair {
     pub newline: bool,
 }
 
+#[async_trait]
+pub trait DiagnosticSource: 'static + Send + Sync {
+    async fn diagnose(
+        &self,
+        path: Arc<Path>,
+    ) -> Result<Vec<(PathBuf, Vec<DiagnosticEntry<Point>>)>>;
+}
+
 pub struct Language {
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
+    pub(crate) diagnostic_source: Option<Arc<dyn DiagnosticSource>>,
 }
 
 pub struct Grammar {
@@ -126,6 +140,7 @@ impl Language {
                     highlight_map: Default::default(),
                 })
             }),
+            diagnostic_source: None,
         }
     }
 
@@ -159,6 +174,11 @@ impl Language {
         Ok(self)
     }
 
+    pub fn with_diagnostic_source(mut self, source: impl DiagnosticSource) -> Self {
+        self.diagnostic_source = Some(Arc::new(source));
+        self
+    }
+
     pub fn name(&self) -> &str {
         self.config.name.as_str()
     }
@@ -192,6 +212,10 @@ impl Language {
         }
     }
 
+    pub fn diagnostic_source(&self) -> Option<&Arc<dyn DiagnosticSource>> {
+        self.diagnostic_source.as_ref()
+    }
+
     pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
         self.config
             .language_server

crates/lsp/src/lsp.rs 🔗

@@ -226,7 +226,11 @@ impl LanguageServer {
             process_id: Default::default(),
             root_path: Default::default(),
             root_uri: Some(root_uri),
-            initialization_options: Default::default(),
+            initialization_options: Some(json!({
+                "checkOnSave": {
+                    "enable": false
+                },
+            })),
             capabilities: lsp_types::ClientCapabilities {
                 experimental: Some(json!({
                     "serverStatusNotification": true,

crates/project/src/project.rs 🔗

@@ -11,14 +11,14 @@ use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
 };
-use language::{Buffer, DiagnosticEntry, LanguageRegistry};
+use language::{Buffer, DiagnosticEntry, Language, LanguageRegistry};
 use lsp::DiagnosticSeverity;
 use postage::{prelude::Stream, watch};
 use std::{
     path::Path,
     sync::{atomic::AtomicBool, Arc},
 };
-use util::TryFutureExt as _;
+use util::{ResultExt, TryFutureExt as _};
 
 pub use fs::*;
 pub use worktree::*;
@@ -503,6 +503,31 @@ impl Project {
         }
     }
 
+    pub fn diagnose(&self, cx: &mut ModelContext<Self>) {
+        for worktree_handle in &self.worktrees {
+            if let Some(worktree) = worktree_handle.read(cx).as_local() {
+                for language in worktree.languages() {
+                    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 {
+                            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 {}
+                                    })
+                                }
+                            }
+                        })
+                        .detach();
+                    }
+                }
+            }
+        }
+    }
+
     pub fn diagnostic_summaries<'a>(
         &'a self,
         cx: &'a AppContext,

crates/project/src/worktree.rs 🔗

@@ -291,7 +291,7 @@ impl Worktree {
 
     pub fn languages(&self) -> &Arc<LanguageRegistry> {
         match self {
-            Worktree::Local(worktree) => &worktree.languages,
+            Worktree::Local(worktree) => &worktree.language_registry,
             Worktree::Remote(worktree) => &worktree.languages,
         }
     }
@@ -853,10 +853,11 @@ pub struct LocalWorktree {
     diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
     diagnostic_summaries: BTreeMap<Arc<Path>, DiagnosticSummary>,
     queued_operations: Vec<(u64, Operation)>,
-    languages: Arc<LanguageRegistry>,
+    language_registry: Arc<LanguageRegistry>,
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
+    languages: Vec<Arc<Language>>,
     language_servers: HashMap<String, Arc<LanguageServer>>,
 }
 
@@ -960,10 +961,11 @@ impl LocalWorktree {
                 diagnostics: Default::default(),
                 diagnostic_summaries: Default::default(),
                 queued_operations: Default::default(),
-                languages,
+                language_registry: languages,
                 client,
                 user_store,
                 fs,
+                languages: Default::default(),
                 language_servers: Default::default(),
             };
 
@@ -1004,15 +1006,23 @@ impl LocalWorktree {
         self.config.collaborators.clone()
     }
 
-    pub fn languages(&self) -> &LanguageRegistry {
+    pub fn language_registry(&self) -> &LanguageRegistry {
+        &self.language_registry
+    }
+
+    pub fn languages(&self) -> &[Arc<Language>] {
         &self.languages
     }
 
-    pub fn ensure_language_server(
+    pub fn register_language(
         &mut self,
-        language: &Language,
+        language: &Arc<Language>,
         cx: &mut ModelContext<Worktree>,
     ) -> Option<Arc<LanguageServer>> {
+        if !self.languages.iter().any(|l| Arc::ptr_eq(l, language)) {
+            self.languages.push(language.clone());
+        }
+
         if let Some(server) = self.language_servers.get(language.name()) {
             return Some(server.clone());
         }
@@ -1090,10 +1100,13 @@ impl LocalWorktree {
             let (diagnostics, language, language_server) = this.update(&mut cx, |this, cx| {
                 let this = this.as_local_mut().unwrap();
                 let diagnostics = this.diagnostics.remove(&path);
-                let language = this.languages.select_language(file.full_path()).cloned();
+                let language = this
+                    .language_registry
+                    .select_language(file.full_path())
+                    .cloned();
                 let server = language
                     .as_ref()
-                    .and_then(|language| this.ensure_language_server(language, cx));
+                    .and_then(|language| this.register_language(language, cx));
                 (diagnostics, language, server)
             });
 
@@ -1191,8 +1204,8 @@ impl LocalWorktree {
         self.snapshot.clone()
     }
 
-    pub fn abs_path(&self) -> &Path {
-        self.snapshot.abs_path.as_ref()
+    pub fn abs_path(&self) -> &Arc<Path> {
+        &self.snapshot.abs_path
     }
 
     pub fn contains_abs_path(&self, path: &Path) -> bool {