WIP

Nathan Sobo , Antonio Scandurra , and Max Brunsfeld created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

crates/language/src/buffer.rs         | 16 ++--
crates/language/src/diagnostic_set.rs | 17 ++++
crates/language/src/language.rs       |  2 
crates/language/src/proto.rs          | 89 +++++++++++++++-------------
crates/project/src/worktree.rs        | 77 ++++++++++++++++++++++++
crates/rpc/proto/zed.proto            | 13 ++-
6 files changed, 156 insertions(+), 58 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -77,7 +77,7 @@ pub struct Buffer {
 pub struct BufferSnapshot {
     text: text::BufferSnapshot,
     tree: Option<Tree>,
-    diagnostics: DiagnosticSet,
+    diagnostics: HashMap<&'static str, DiagnosticSet>,
     remote_selections: TreeMap<ReplicaId, Arc<[Selection<Anchor>]>>,
     diagnostics_update_count: usize,
     is_parsing: bool,
@@ -115,7 +115,7 @@ struct LanguageServerSnapshot {
 pub enum Operation {
     Buffer(text::Operation),
     UpdateDiagnostics {
-        diagnostics: Arc<[DiagnosticEntry<Anchor>]>,
+        diagnostic_set: Arc<DiagnosticSet>,
         lamport_timestamp: clock::Lamport,
     },
     UpdateSelections {
@@ -298,10 +298,12 @@ impl Buffer {
                 proto::deserialize_selections(selection_set.selections),
             );
         }
-        this.apply_diagnostic_update(
-            Arc::from(proto::deserialize_diagnostics(message.diagnostics)),
-            cx,
-        );
+        for diagnostic_set in message.diagnostic_sets {
+            this.apply_diagnostic_update(
+                Arc::from(proto::deserialize_diagnostics(diagnostic_set)),
+                cx,
+            );
+        }
 
         Ok(this)
     }
@@ -323,7 +325,7 @@ impl Buffer {
                     selections: proto::serialize_selections(selections),
                 })
                 .collect(),
-            diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
+            diagnostics: proto::serialize_diagnostic_set(self.diagnostics.iter()),
         }
     }
 

crates/language/src/diagnostic_set.rs 🔗

@@ -8,8 +8,9 @@ use std::{
 use sum_tree::{self, Bias, SumTree};
 use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
 
-#[derive(Clone, Default)]
+#[derive(Clone, Debug, Default)]
 pub struct DiagnosticSet {
+    provider_name: String,
     diagnostics: SumTree<DiagnosticEntry<Anchor>>,
 }
 
@@ -34,22 +35,32 @@ pub struct Summary {
 }
 
 impl DiagnosticSet {
-    pub fn from_sorted_entries<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
+    pub fn provider_name(&self) -> &str {
+        &self.provider_name
+    }
+
+    pub fn from_sorted_entries<I>(
+        provider_name: String,
+        iter: I,
+        buffer: &text::BufferSnapshot,
+    ) -> Self
     where
         I: IntoIterator<Item = DiagnosticEntry<Anchor>>,
     {
         Self {
+            provider_name,
             diagnostics: SumTree::from_iter(iter, buffer),
         }
     }
 
-    pub fn new<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
+    pub fn new<I>(provider_name: &'static str, iter: I, buffer: &text::BufferSnapshot) -> Self
     where
         I: IntoIterator<Item = DiagnosticEntry<PointUtf16>>,
     {
         let mut entries = iter.into_iter().collect::<Vec<_>>();
         entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end)));
         Self {
+            provider_name,
             diagnostics: SumTree::from_iter(
                 entries.into_iter().map(|entry| DiagnosticEntry {
                     range: buffer.anchor_before(entry.range.start)

crates/language/src/language.rs 🔗

@@ -66,6 +66,8 @@ pub struct BracketPair {
 
 #[async_trait]
 pub trait DiagnosticSource: 'static + Send + Sync {
+    fn name(&self) -> &'static str;
+
     async fn diagnose(
         &self,
         path: Arc<Path>,

crates/language/src/proto.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{diagnostic_set::DiagnosticEntry, Diagnostic, Operation};
+use crate::{diagnostic_set::DiagnosticEntry, Diagnostic, DiagnosticSet, Operation};
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
 use lsp::DiagnosticSeverity;
@@ -57,12 +57,12 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
                 lamport_timestamp: lamport_timestamp.value,
             }),
             Operation::UpdateDiagnostics {
-                diagnostics,
+                diagnostic_set,
                 lamport_timestamp,
-            } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
+            } => proto::operation::Variant::UpdateDiagnosticSet(proto::UpdateDiagnosticSet {
                 replica_id: lamport_timestamp.replica_id as u32,
                 lamport_timestamp: lamport_timestamp.value,
-                diagnostics: serialize_diagnostics(diagnostics.iter()),
+                diagnostic_set: Some(serialize_diagnostic_set(&diagnostic_set)),
             }),
         }),
     }
@@ -99,29 +99,30 @@ pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto:
         .collect()
 }
 
-pub fn serialize_diagnostics<'a>(
-    diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<Anchor>>,
-) -> Vec<proto::Diagnostic> {
-    diagnostics
-        .into_iter()
-        .map(|entry| proto::Diagnostic {
-            start: Some(serialize_anchor(&entry.range.start)),
-            end: Some(serialize_anchor(&entry.range.end)),
-            message: entry.diagnostic.message.clone(),
-            severity: match entry.diagnostic.severity {
-                DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
-                DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
-                DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
-                DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
-                _ => proto::diagnostic::Severity::None,
-            } as i32,
-            group_id: entry.diagnostic.group_id as u64,
-            is_primary: entry.diagnostic.is_primary,
-            is_valid: entry.diagnostic.is_valid,
-            code: entry.diagnostic.code.clone(),
-            is_disk_based: entry.diagnostic.is_disk_based,
-        })
-        .collect()
+pub fn serialize_diagnostic_set(set: &DiagnosticSet) -> proto::DiagnosticSet {
+    proto::DiagnosticSet {
+        provider_name: set.provider_name().to_string(),
+        diagnostics: set
+            .iter()
+            .map(|entry| proto::Diagnostic {
+                start: Some(serialize_anchor(&entry.range.start)),
+                end: Some(serialize_anchor(&entry.range.end)),
+                message: entry.diagnostic.message.clone(),
+                severity: match entry.diagnostic.severity {
+                    DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
+                    DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
+                    DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
+                    DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
+                    _ => proto::diagnostic::Severity::None,
+                } as i32,
+                group_id: entry.diagnostic.group_id as u64,
+                is_primary: entry.diagnostic.is_primary,
+                is_valid: entry.diagnostic.is_valid,
+                code: entry.diagnostic.code.clone(),
+                is_disk_based: entry.diagnostic.is_disk_based,
+            })
+            .collect(),
+    }
 }
 
 fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
@@ -207,13 +208,15 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                     value: message.lamport_timestamp,
                 },
             },
-            proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics {
-                diagnostics: Arc::from(deserialize_diagnostics(message.diagnostics)),
-                lamport_timestamp: clock::Lamport {
-                    replica_id: message.replica_id as ReplicaId,
-                    value: message.lamport_timestamp,
-                },
-            },
+            proto::operation::Variant::UpdateDiagnosticSet(message) => {
+                Operation::UpdateDiagnostics {
+                    diagnostics: Arc::from(deserialize_diagnostic_set(message.diagnostic_set?)),
+                    lamport_timestamp: clock::Lamport {
+                        replica_id: message.replica_id as ReplicaId,
+                        value: message.lamport_timestamp,
+                    },
+                }
+            }
         },
     )
 }
@@ -253,12 +256,13 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
     )
 }
 
-pub fn deserialize_diagnostics(
-    diagnostics: Vec<proto::Diagnostic>,
-) -> Vec<DiagnosticEntry<Anchor>> {
-    diagnostics
-        .into_iter()
-        .filter_map(|diagnostic| {
+pub fn deserialize_diagnostic_set(
+    message: proto::DiagnosticSet,
+    buffer: &BufferSnapshot,
+) -> DiagnosticSet {
+    DiagnosticSet::from_sorted_entries(
+        message.provider_name,
+        message.diagnostics.into_iter().filter_map(|diagnostic| {
             Some(DiagnosticEntry {
                 range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?,
                 diagnostic: Diagnostic {
@@ -277,8 +281,9 @@ pub fn deserialize_diagnostics(
                     is_disk_based: diagnostic.is_disk_based,
                 },
             })
-        })
-        .collect()
+        }),
+        buffer,
+    )
 }
 
 fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {

crates/project/src/worktree.rs 🔗

@@ -672,6 +672,79 @@ impl Worktree {
         }
     }
 
+    pub fn update_lsp_diagnostics(
+        &mut self,
+        mut params: lsp::PublishDiagnosticsParams,
+        disk_based_sources: &HashSet<String>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> 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 group_ids_by_diagnostic_range = HashMap::default();
+        let mut diagnostics_by_group_id = HashMap::default();
+        let mut next_group_id = 0;
+        for diagnostic in &mut params.diagnostics {
+            let source = diagnostic.source.as_ref();
+            let code = diagnostic.code.as_ref();
+            let group_id = diagnostic_ranges(&diagnostic, &abs_path)
+                .find_map(|range| group_ids_by_diagnostic_range.get(&(source, code, range)))
+                .copied()
+                .unwrap_or_else(|| {
+                    let group_id = post_inc(&mut next_group_id);
+                    for range in diagnostic_ranges(&diagnostic, &abs_path) {
+                        group_ids_by_diagnostic_range.insert((source, code, range), group_id);
+                    }
+                    group_id
+                });
+
+            diagnostics_by_group_id
+                .entry(group_id)
+                .or_insert(Vec::new())
+                .push(DiagnosticEntry {
+                    range: diagnostic.range.start.to_point_utf16()
+                        ..diagnostic.range.end.to_point_utf16(),
+                    diagnostic: Diagnostic {
+                        code: diagnostic.code.clone().map(|code| match code {
+                            lsp::NumberOrString::Number(code) => code.to_string(),
+                            lsp::NumberOrString::String(code) => code,
+                        }),
+                        severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
+                        message: mem::take(&mut diagnostic.message),
+                        group_id,
+                        is_primary: false,
+                        is_valid: true,
+                        is_disk_based: diagnostic
+                            .source
+                            .as_ref()
+                            .map_or(false, |source| disk_based_sources.contains(source)),
+                    },
+                });
+        }
+
+        let diagnostics = diagnostics_by_group_id
+            .into_values()
+            .flat_map(|mut diagnostics| {
+                let primary = diagnostics
+                    .iter_mut()
+                    .min_by_key(|entry| entry.diagnostic.severity)
+                    .unwrap();
+                primary.diagnostic.is_primary = true;
+                diagnostics
+            })
+            .collect::<Vec<_>>();
+
+        self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)
+    }
+
     pub fn update_diagnostics(
         &mut self,
         mut params: lsp::PublishDiagnosticsParams,
@@ -1046,7 +1119,7 @@ impl LocalWorktree {
                 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_diagnostics(diagnostics, &disk_based_sources, cx)
+                            this.update_lsp_diagnostics(diagnostics, &disk_based_sources, cx)
                                 .log_err();
                         });
                     } else {
@@ -3835,7 +3908,7 @@ mod tests {
 
         worktree
             .update(&mut cx, |tree, cx| {
-                tree.update_diagnostics(message, &Default::default(), cx)
+                tree.update_lsp_diagnostics(message, &Default::default(), cx)
             })
             .unwrap();
         let buffer = buffer.read_with(&cx, |buffer, _| buffer.snapshot());

crates/rpc/proto/zed.proto 🔗

@@ -265,7 +265,7 @@ message Buffer {
     string content = 2;
     repeated Operation.Edit history = 3;
     repeated SelectionSet selections = 4;
-    repeated Diagnostic diagnostics = 5;
+    repeated DiagnosticSet diagnostic_sets = 5;
 }
 
 message SelectionSet {
@@ -292,10 +292,15 @@ enum Bias {
     Right = 1;
 }
 
-message UpdateDiagnostics {
+message UpdateDiagnosticSet {
     uint32 replica_id = 1;
     uint32 lamport_timestamp = 2;
-    repeated Diagnostic diagnostics = 3;
+    DiagnosticSet diagnostic_set = 3;
+}
+
+message DiagnosticSet {
+    string provider_name = 1;
+    repeated Diagnostic diagnostics = 2;
 }
 
 message Diagnostic {
@@ -324,7 +329,7 @@ message Operation {
         Undo undo = 2;
         UpdateSelections update_selections = 3;
         RemoveSelections remove_selections = 4;
-        UpdateDiagnostics update_diagnostics = 5;
+        UpdateDiagnosticSet update_diagnostic_set = 5;
     }
 
     message Edit {