Start on a `DiagnosticProvider` implementation for Rust

Antonio Scandurra and Nathan Sobo created

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

Change summary

Cargo.lock                        |   1 
crates/language/src/buffer.rs     |   7 +
crates/language/src/language.rs   |  10 --
crates/workspace/src/workspace.rs |  14 +++
crates/zed/Cargo.toml             |   1 
crates/zed/src/language.rs        | 115 +++++++++++++++++++++++++++++++++
6 files changed, 138 insertions(+), 10 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5702,6 +5702,7 @@ dependencies = [
  "chat_panel",
  "client",
  "clock",
+ "collections",
  "contacts_panel",
  "crossbeam-channel",
  "ctor",

crates/language/src/buffer.rs 🔗

@@ -805,10 +805,13 @@ impl Buffer {
                     }
                 }
 
+                let start_overshoot = start - last_edit_old_end;
                 start = last_edit_new_end;
-                start.add_assign(&(start - last_edit_old_end));
+                start.add_assign(&start_overshoot);
+
+                let end_overshoot = end - last_edit_old_end;
                 end = last_edit_new_end;
-                end.add_assign(&(end - last_edit_old_end));
+                end.add_assign(&end_overshoot);
             }
 
             let range = start.clip(Bias::Left, content)..end.clip(Bias::Right, content);

crates/language/src/language.rs 🔗

@@ -9,18 +9,14 @@ use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 pub use buffer::Operation;
 pub use buffer::*;
-use collections::HashSet;
+use collections::{HashMap, HashSet};
 pub use diagnostic_set::DiagnosticEntry;
 use gpui::AppContext;
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Deserialize;
-use std::{
-    path::{Path, PathBuf},
-    str,
-    sync::Arc,
-};
+use std::{path::Path, str, sync::Arc};
 use theme::SyntaxTheme;
 use tree_sitter::{self, Query};
 pub use tree_sitter::{Parser, Tree};
@@ -69,7 +65,7 @@ pub trait DiagnosticProvider: 'static + Send + Sync {
     async fn diagnose(
         &self,
         path: Arc<Path>,
-    ) -> Result<Vec<(PathBuf, Vec<DiagnosticEntry<usize>>)>>;
+    ) -> Result<HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>>;
 }
 
 pub struct Language {

crates/workspace/src/workspace.rs 🔗

@@ -791,16 +791,24 @@ impl Workspace {
                                     {
                                         error!("failed to save item: {:?}, ", error);
                                     }
+
+                                    handle.update(&mut cx, |this, cx| {
+                                        this.project.update(cx, |project, cx| project.diagnose(cx))
+                                    });
                                 })
                                 .detach();
                             }
                         },
                     );
                 } else {
-                    cx.spawn(|_, mut cx| async move {
+                    cx.spawn(|this, mut cx| async move {
                         if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
                             error!("failed to save item: {:?}, ", error);
                         }
+
+                        this.update(&mut cx, |this, cx| {
+                            this.project.update(cx, |project, cx| project.diagnose(cx))
+                        });
                     })
                     .detach();
                 }
@@ -832,6 +840,10 @@ impl Workspace {
                             if let Err(error) = result {
                                 error!("failed to save item: {:?}, ", error);
                             }
+
+                            handle.update(&mut cx, |this, cx| {
+                                this.project.update(cx, |project, cx| project.diagnose(cx))
+                            });
                         })
                         .detach()
                     }

crates/zed/Cargo.toml 🔗

@@ -29,6 +29,7 @@ test-support = [
 
 [dependencies]
 chat_panel = { path = "../chat_panel" }
+collections = { path = "../collections" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 contacts_panel = { path = "../contacts_panel" }

crates/zed/src/language.rs 🔗

@@ -7,6 +7,120 @@ use std::{str, sync::Arc};
 #[folder = "languages"]
 struct LanguageDir;
 
+mod rust {
+    use anyhow::Result;
+    use async_trait::async_trait;
+    use collections::{HashMap, HashSet};
+    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity};
+    use parking_lot::Mutex;
+    use serde::Deserialize;
+    use serde_json::Deserializer;
+    use smol::process::Command;
+    use std::path::{Path, PathBuf};
+    use std::sync::Arc;
+
+    #[derive(Default)]
+    pub struct DiagnosticProvider {
+        reported_paths: Mutex<HashSet<Arc<Path>>>,
+    }
+
+    #[derive(Debug, Deserialize)]
+    struct Check {
+        message: CompilerMessage,
+    }
+
+    #[derive(Debug, Deserialize)]
+    struct CompilerMessage {
+        code: ErrorCode,
+        spans: Vec<Span>,
+        message: String,
+        level: ErrorLevel,
+    }
+
+    #[derive(Debug, Deserialize)]
+    enum ErrorLevel {
+        #[serde(rename = "warning")]
+        Warning,
+        #[serde(rename = "error")]
+        Error,
+        #[serde(rename = "note")]
+        Note,
+    }
+
+    #[derive(Debug, Deserialize)]
+    struct ErrorCode {
+        code: String,
+    }
+
+    #[derive(Debug, Deserialize)]
+    struct Span {
+        is_primary: bool,
+        file_name: PathBuf,
+        byte_start: usize,
+        byte_end: usize,
+    }
+
+    #[async_trait]
+    impl language::DiagnosticProvider for DiagnosticProvider {
+        async fn diagnose(
+            &self,
+            path: Arc<Path>,
+        ) -> Result<HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>> {
+            let output = Command::new("cargo")
+                .arg("check")
+                .args(["--message-format", "json"])
+                .current_dir(&path)
+                .output()
+                .await?;
+
+            let mut group_id = 0;
+            let mut diagnostics_by_path = HashMap::default();
+            let mut new_reported_paths = HashSet::default();
+            for value in
+                Deserializer::from_slice(&output.stdout).into_iter::<&serde_json::value::RawValue>()
+            {
+                if let Ok(check) = serde_json::from_str::<Check>(value?.get()) {
+                    let severity = match check.message.level {
+                        ErrorLevel::Warning => DiagnosticSeverity::WARNING,
+                        ErrorLevel::Error => DiagnosticSeverity::ERROR,
+                        ErrorLevel::Note => DiagnosticSeverity::INFORMATION,
+                    };
+                    for span in check.message.spans {
+                        let span_path: Arc<Path> = span.file_name.into();
+                        new_reported_paths.insert(span_path.clone());
+                        diagnostics_by_path
+                            .entry(span_path)
+                            .or_insert(Vec::new())
+                            .push(DiagnosticEntry {
+                                range: span.byte_start..span.byte_end,
+                                diagnostic: Diagnostic {
+                                    code: Some(check.message.code.code.clone()),
+                                    severity,
+                                    message: check.message.message.clone(),
+                                    group_id,
+                                    is_valid: true,
+                                    is_primary: span.is_primary,
+                                    is_disk_based: true,
+                                },
+                            });
+                    }
+                    group_id += 1;
+                }
+            }
+
+            let reported_paths = &mut *self.reported_paths.lock();
+            for old_reported_path in reported_paths.iter() {
+                if !diagnostics_by_path.contains_key(old_reported_path) {
+                    diagnostics_by_path.insert(old_reported_path.clone(), Default::default());
+                }
+            }
+            *reported_paths = new_reported_paths;
+
+            Ok(diagnostics_by_path)
+        }
+    }
+}
+
 pub fn build_language_registry() -> LanguageRegistry {
     let mut languages = LanguageRegistry::default();
     languages.add(Arc::new(rust()));
@@ -24,6 +138,7 @@ fn rust() -> Language {
         .unwrap()
         .with_indents_query(load_query("rust/indents.scm").as_ref())
         .unwrap()
+        .with_diagnostic_provider(rust::DiagnosticProvider::default())
 }
 
 fn markdown() -> Language {