Start work on preserving continuity of disk-based diagnostics

Max Brunsfeld created

Change summary

crates/language/src/buffer.rs  | 67 +++++++++++++++++++++++++++++++++--
crates/language/src/proto.rs   |  2 +
crates/project/src/worktree.rs |  1 
crates/rpc/proto/zed.proto     | 10 +++--
4 files changed, 72 insertions(+), 8 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -18,11 +18,11 @@ use smol::future::yield_now;
 use std::{
     any::Any,
     cell::RefCell,
-    cmp,
+    cmp::{self, Reverse},
     collections::{BTreeMap, HashMap, HashSet},
     ffi::OsString,
     future::Future,
-    iter::{Iterator, Peekable},
+    iter::{self, Iterator, Peekable},
     ops::{Deref, DerefMut, Range},
     path::{Path, PathBuf},
     str,
@@ -92,6 +92,7 @@ pub struct Diagnostic {
     pub severity: DiagnosticSeverity,
     pub message: String,
     pub group_id: usize,
+    pub is_valid: bool,
     pub is_primary: bool,
 }
 
@@ -725,7 +726,7 @@ impl Buffer {
         mut diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
         cx: &mut ModelContext<Self>,
     ) -> Result<Operation> {
-        diagnostics.sort_unstable_by_key(|d| (d.range.start, d.range.end));
+        diagnostics.sort_unstable_by_key(|d| (d.range.start, Reverse(d.range.end)));
 
         let version = version.map(|version| version as usize);
         let content = if let Some(version) = version {
@@ -754,6 +755,7 @@ impl Buffer {
             .peekable();
         let mut last_edit_old_end = PointUtf16::zero();
         let mut last_edit_new_end = PointUtf16::zero();
+        let mut has_disk_based_diagnostics = false;
         let mut ix = 0;
         'outer: while ix < diagnostics.len() {
             let entry = &mut diagnostics[ix];
@@ -769,6 +771,7 @@ impl Buffer {
                 .as_ref()
                 .map_or(false, |source| disk_based_sources.contains(source))
             {
+                has_disk_based_diagnostics = true;
                 while let Some(edit) = edits_since_save.peek() {
                     if edit.old.end <= start {
                         last_edit_old_end = edit.old.end;
@@ -802,7 +805,62 @@ impl Buffer {
         }
 
         drop(edits_since_save);
-        self.diagnostics = DiagnosticSet::new(diagnostics, content);
+
+        let diagnostics = diagnostics.into_iter().map(|entry| DiagnosticEntry {
+            range: content.anchor_before(entry.range.start)..content.anchor_after(entry.range.end),
+            diagnostic: entry.diagnostic,
+        });
+
+        // Some diagnostic sources are reported on a less frequent basis than others.
+        // If those sources are absent from this message, then preserve the previous
+        // diagnostics for those sources, but mark them as stale, and set a time to
+        // clear them out.
+        let mut merged_old_disk_based_diagnostics = false;
+        self.diagnostics = if has_disk_based_diagnostics {
+            DiagnosticSet::from_sorted_entries(diagnostics, content)
+        } else {
+            let mut new_diagnostics = diagnostics.peekable();
+            let mut old_diagnostics = self
+                .diagnostics
+                .iter()
+                .filter_map(|entry| {
+                    let is_disk_based = entry
+                        .diagnostic
+                        .source
+                        .as_ref()
+                        .map_or(false, |source| disk_based_sources.contains(source));
+                    if is_disk_based {
+                        merged_old_disk_based_diagnostics = true;
+                        let mut entry = entry.clone();
+                        entry.diagnostic.is_valid = false;
+                        Some(entry)
+                    } else {
+                        None
+                    }
+                })
+                .peekable();
+            let merged_diagnostics =
+                iter::from_fn(|| match (old_diagnostics.peek(), new_diagnostics.peek()) {
+                    (None, None) => None,
+                    (Some(_), None) => old_diagnostics.next(),
+                    (None, Some(_)) => new_diagnostics.next(),
+                    (Some(old), Some(new)) => {
+                        let ordering = old
+                            .range
+                            .start
+                            .cmp(&new.range.start, content)
+                            .unwrap()
+                            .then_with(|| new.range.end.cmp(&old.range.end, content).unwrap());
+                        if ordering.is_lt() {
+                            old_diagnostics.next()
+                        } else {
+                            new_diagnostics.next()
+                        }
+                    }
+                });
+            DiagnosticSet::from_sorted_entries(merged_diagnostics, content)
+        };
+
         self.diagnostics_update_count += 1;
         cx.notify();
         cx.emit(Event::DiagnosticsUpdated);
@@ -2009,6 +2067,7 @@ impl Default for Diagnostic {
             message: Default::default(),
             group_id: Default::default(),
             is_primary: Default::default(),
+            is_valid: Default::default(),
         }
     }
 }

crates/language/src/proto.rs 🔗

@@ -117,6 +117,7 @@ pub fn serialize_diagnostics<'a>(
             } 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(),
             source: entry.diagnostic.source.clone(),
         })
@@ -273,6 +274,7 @@ pub fn deserialize_diagnostics(
                     is_primary: diagnostic.is_primary,
                     code: diagnostic.code,
                     source: diagnostic.source,
+                    is_valid: diagnostic.is_valid,
                 },
             })
         })

crates/project/src/worktree.rs 🔗

@@ -721,6 +721,7 @@ impl Worktree {
                         message: mem::take(&mut diagnostic.message),
                         group_id,
                         is_primary: false,
+                        is_valid: true,
                     },
                 });
         }

crates/rpc/proto/zed.proto 🔗

@@ -303,10 +303,12 @@ message Diagnostic {
     Anchor end = 2;
     Severity severity = 3;
     string message = 4;
-    uint64 group_id = 5;
-    bool is_primary = 6;
-    optional string code = 7;
-    optional string source = 8;
+    optional string code = 5;
+    optional string source = 6;
+    uint64 group_id = 7;
+    bool is_primary = 8;
+    bool is_valid = 9;
+
     enum Severity {
         None = 0;
         Error = 1;