Allow retrieving a buffer's diagnostics

Max Brunsfeld created

Change summary

crates/buffer/src/anchor.rs  | 17 ++++---
crates/buffer/src/lib.rs     |  1 
crates/language/Cargo.toml   |  3 
crates/language/src/lib.rs   | 82 +++++++++++++++++++++++++------------
crates/language/src/tests.rs | 72 +++++++++++++++++++++++++++++++++
5 files changed, 140 insertions(+), 35 deletions(-)

Detailed changes

crates/buffer/src/anchor.rs 🔗

@@ -175,10 +175,10 @@ impl<T: Clone> Default for AnchorRangeMultimap<T> {
 }
 
 impl<T: Clone> AnchorRangeMultimap<T> {
-    fn intersecting_point_ranges<'a, O>(
+    pub fn intersecting_point_ranges<'a, O>(
         &'a self,
         range: Range<O>,
-        content: &'a Content<'a>,
+        content: Content<'a>,
         inclusive: bool,
     ) -> impl Iterator<Item = (usize, Range<Point>, &T)> + 'a
     where
@@ -187,10 +187,11 @@ impl<T: Clone> AnchorRangeMultimap<T> {
         use super::ToPoint as _;
 
         let end_bias = if inclusive { Bias::Right } else { Bias::Left };
-        let range = range.start.to_full_offset(content, Bias::Left)
-            ..range.end.to_full_offset(content, end_bias);
+        let range = range.start.to_full_offset(&content, Bias::Left)
+            ..range.end.to_full_offset(&content, end_bias);
         let mut cursor = self.entries.filter::<_, usize>(
             {
+                let content = content.clone();
                 let mut endpoint = Anchor {
                     full_offset: 0,
                     bias: Bias::Right,
@@ -199,12 +200,12 @@ impl<T: Clone> AnchorRangeMultimap<T> {
                 move |summary: &AnchorRangeMultimapSummary| {
                     endpoint.full_offset = summary.max_end;
                     endpoint.bias = self.end_bias;
-                    let max_end = endpoint.to_full_offset(content, self.end_bias);
+                    let max_end = endpoint.to_full_offset(&content, self.end_bias);
                     let start_cmp = range.start.cmp(&max_end);
 
                     endpoint.full_offset = summary.min_start;
                     endpoint.bias = self.start_bias;
-                    let min_start = endpoint.to_full_offset(content, self.start_bias);
+                    let min_start = endpoint.to_full_offset(&content, self.start_bias);
                     let end_cmp = range.end.cmp(&min_start);
 
                     if inclusive {
@@ -228,10 +229,10 @@ impl<T: Clone> AnchorRangeMultimap<T> {
                     let ix = *cursor.start();
                     endpoint.full_offset = item.range.start;
                     endpoint.bias = self.start_bias;
-                    let start = endpoint.to_point(content);
+                    let start = endpoint.to_point(&content);
                     endpoint.full_offset = item.range.end;
                     endpoint.bias = self.end_bias;
-                    let end = endpoint.to_point(content);
+                    let end = endpoint.to_point(&content);
                     let value = &item.value;
                     cursor.next(&());
                     Some((ix, start..end, value))

crates/buffer/src/lib.rs 🔗

@@ -1592,6 +1592,7 @@ impl Snapshot {
     }
 }
 
+#[derive(Clone)]
 pub struct Content<'a> {
     visible_text: &'a Rope,
     deleted_text: &'a Rope,

crates/language/Cargo.toml 🔗

@@ -4,7 +4,7 @@ version = "0.1.0"
 edition = "2018"
 
 [features]
-test-support = ["rand", "buffer/test-support"]
+test-support = ["rand", "buffer/test-support", "lsp/test-support"]
 
 [dependencies]
 buffer = { path = "../buffer" }
@@ -29,6 +29,7 @@ tree-sitter = "0.19.5"
 [dev-dependencies]
 buffer = { path = "../buffer", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
 rand = "0.8.3"
 tree-sitter-rust = "0.19.0"
 unindent = "0.1.7"

crates/language/src/lib.rs 🔗

@@ -13,7 +13,7 @@ use clock::ReplicaId;
 use futures::FutureExt as _;
 use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
 use lazy_static::lazy_static;
-use lsp::LanguageServer;
+use lsp::{DiagnosticSeverity, LanguageServer};
 use parking_lot::Mutex;
 use postage::{prelude::Stream, sink::Sink, watch};
 use rpc::proto;
@@ -59,7 +59,7 @@ pub struct Buffer {
     syntax_tree: Mutex<Option<SyntaxTree>>,
     parsing_in_background: bool,
     parse_count: usize,
-    diagnostics: AnchorRangeMultimap<()>,
+    diagnostics: AnchorRangeMultimap<(DiagnosticSeverity, String)>,
     language_server: Option<LanguageServerState>,
     #[cfg(test)]
     operations: Vec<Operation>,
@@ -73,6 +73,13 @@ pub struct Snapshot {
     query_cursor: QueryCursorHandle,
 }
 
+#[derive(Debug, PartialEq, Eq)]
+pub struct Diagnostic {
+    pub range: Range<Point>,
+    pub severity: DiagnosticSeverity,
+    pub message: String,
+}
+
 struct LanguageServerState {
     server: Arc<LanguageServer>,
     latest_snapshot: watch::Sender<Option<LanguageServerSnapshot>>,
@@ -613,43 +620,67 @@ impl Buffer {
 
     pub fn update_diagnostics(
         &mut self,
-        params: lsp::PublishDiagnosticsParams,
+        version: Option<i32>,
+        diagnostics: Vec<lsp::Diagnostic>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
-        dbg!(&params);
-        let language_server = self.language_server.as_mut().unwrap();
-        let version = params.version.ok_or_else(|| anyhow!("missing version"))? as usize;
-        let snapshot = language_server
-            .pending_snapshots
-            .get(&version)
-            .ok_or_else(|| anyhow!("missing snapshot"))?;
-        self.diagnostics = snapshot.buffer_snapshot.content().anchor_range_multimap(
+        let version = version.map(|version| version as usize);
+        let content = if let Some(version) = version {
+            let language_server = self.language_server.as_mut().unwrap();
+            let snapshot = language_server
+                .pending_snapshots
+                .get(&version)
+                .ok_or_else(|| anyhow!("missing snapshot"))?;
+            snapshot.buffer_snapshot.content()
+        } else {
+            self.content()
+        };
+        self.diagnostics = content.anchor_range_multimap(
             Bias::Left,
             Bias::Right,
-            params.diagnostics.into_iter().map(|diagnostic| {
+            diagnostics.into_iter().map(|diagnostic| {
                 // TODO: Use UTF-16 positions.
                 let start = Point::new(
                     diagnostic.range.start.line,
                     diagnostic.range.start.character,
                 );
                 let end = Point::new(diagnostic.range.end.line, diagnostic.range.end.character);
-                (start..end, ())
+                let severity = diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR);
+                (start..end, (severity, diagnostic.message))
             }),
         );
 
-        let versions_to_delete = language_server
-            .pending_snapshots
-            .range(..version)
-            .map(|(v, _)| *v)
-            .collect::<Vec<_>>();
-        for version in versions_to_delete {
-            language_server.pending_snapshots.remove(&version);
+        if let Some(version) = version {
+            let language_server = self.language_server.as_mut().unwrap();
+            let versions_to_delete = language_server
+                .pending_snapshots
+                .range(..version)
+                .map(|(v, _)| *v)
+                .collect::<Vec<_>>();
+            for version in versions_to_delete {
+                language_server.pending_snapshots.remove(&version);
+            }
         }
 
         cx.notify();
         Ok(())
     }
 
+    pub fn diagnostics_in_range<'a, T: ToOffset>(
+        &'a self,
+        range: Range<T>,
+    ) -> impl Iterator<Item = Diagnostic> + 'a {
+        let content = self.content();
+        let range = range.start.to_offset(&content)..range.end.to_offset(&content);
+        self.diagnostics
+            .intersecting_point_ranges(range, content, true)
+            .map(move |(_, range, (severity, message))| Diagnostic {
+                range,
+                severity: *severity,
+                message: message.clone(),
+            })
+    }
+
     fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
         if let Some(indent_columns) = self.compute_autoindents() {
             let indent_columns = cx.background().spawn(indent_columns);
@@ -987,17 +1018,16 @@ impl Buffer {
         } else {
             return;
         };
-        let file = if let Some(file) = self.file.as_ref() {
-            file
-        } else {
-            return;
-        };
+        let abs_path = self
+            .file
+            .as_ref()
+            .map_or(PathBuf::new(), |file| file.abs_path(cx).unwrap());
 
         let version = post_inc(&mut language_server.next_version);
         let snapshot = LanguageServerSnapshot {
             buffer_snapshot: self.text.snapshot(),
             version,
-            path: Arc::from(file.abs_path(cx).unwrap()),
+            path: Arc::from(abs_path),
         };
         language_server
             .pending_snapshots

crates/language/src/tests.rs 🔗

@@ -407,6 +407,78 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
     });
 }
 
+#[gpui::test]
+async fn test_diagnostics(mut cx: gpui::TestAppContext) {
+    let (language_server, mut fake) = lsp::LanguageServer::fake(&cx.background()).await;
+
+    let text = "
+        fn a() { A }
+        fn b() { BB }
+        fn c() { CCC }
+    "
+    .unindent();
+
+    let buffer = cx.add_model(|cx| {
+        Buffer::new(0, text, cx).with_language(rust_lang(), Some(language_server), cx)
+    });
+
+    let open_notification = fake
+        .receive_notification::<lsp::notification::DidOpenTextDocument>()
+        .await;
+
+    buffer.update(&mut cx, |buffer, cx| {
+        // Edit the buffer, moving the content down
+        buffer.edit([0..0], "\n\n", cx);
+
+        // Receive diagnostics for an earlier version of the buffer.
+        buffer
+            .update_diagnostics(
+                Some(open_notification.text_document.version),
+                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()
+                    },
+                    lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
+                        message: "undefined variable 'BB'".to_string(),
+                        ..Default::default()
+                    },
+                    lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
+                        message: "undefined variable 'CCC'".to_string(),
+                        ..Default::default()
+                    },
+                ],
+                cx,
+            )
+            .unwrap();
+
+        // The diagnostics have moved down since they were created.
+        assert_eq!(
+            buffer
+                .diagnostics_in_range(Point::new(3, 0)..Point::new(5, 0))
+                .collect::<Vec<_>>(),
+            &[
+                Diagnostic {
+                    range: Point::new(3, 9)..Point::new(3, 11),
+                    severity: DiagnosticSeverity::ERROR,
+                    message: "undefined variable 'BB'".to_string()
+                },
+                Diagnostic {
+                    range: Point::new(4, 9)..Point::new(4, 12),
+                    severity: DiagnosticSeverity::ERROR,
+                    message: "undefined variable 'CCC'".to_string()
+                }
+            ]
+        )
+    });
+}
+
 #[test]
 fn test_contiguous_ranges() {
     assert_eq!(