WIP

Antonio Scandurra and Nathan Sobo created

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

Change summary

Cargo.lock                            |  11 
crates/diagnostics/Cargo.toml         |  14 +
crates/diagnostics/src/diagnostics.rs |  45 +++
crates/editor/src/editor.rs           |  46 +++
crates/editor/src/items.rs            |  47 ---
crates/language/src/buffer.rs         | 119 ++-------
crates/language/src/proto.rs          |   4 
crates/language/src/tests.rs          | 346 ++++++----------------------
crates/project/src/project.rs         |  15 +
crates/project/src/worktree.rs        | 322 ++++++++++++++++++++++++++
crates/rpc/proto/zed.proto            |   2 
11 files changed, 554 insertions(+), 417 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1410,6 +1410,17 @@ dependencies = [
  "const-oid",
 ]
 
+[[package]]
+name = "diagnostics"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "gpui",
+ "postage",
+ "project",
+ "workspace",
+]
+
 [[package]]
 name = "digest"
 version = "0.8.1"

crates/diagnostics/Cargo.toml 🔗

@@ -0,0 +1,14 @@
+[package]
+name = "diagnostics"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/diagnostics.rs"
+
+[dependencies]
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+workspace = { path = "../workspace" }
+postage = { version = "0.4", features = ["futures-traits"] }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -0,0 +1,45 @@
+use editor::{Editor, MultiBuffer};
+use gpui::{elements::*, Entity, ModelHandle, RenderContext, View, ViewContext, ViewHandle};
+use postage::watch;
+use project::Project;
+
+struct ProjectDiagnostics {
+    editor: ViewHandle<Editor>,
+    project: ModelHandle<Project>,
+}
+
+impl ProjectDiagnostics {
+    fn new(
+        project: ModelHandle<Project>,
+        settings: watch::Receiver<workspace::Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let mut buffer = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id(cx)));
+        for (path, diagnostics) in project.read(cx).diagnostics(cx) {}
+
+        Self {
+            editor: cx.add_view(|cx| {
+                Editor::for_buffer(
+                    buffer.clone(),
+                    editor::settings_builder(buffer.downgrade(), settings),
+                    cx,
+                )
+            }),
+            project,
+        }
+    }
+}
+
+impl Entity for ProjectDiagnostics {
+    type Event = ();
+}
+
+impl View for ProjectDiagnostics {
+    fn ui_name() -> &'static str {
+        "ProjectDiagnostics"
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(self.editor.id()).boxed()
+    }
+}

crates/editor/src/editor.rs 🔗

@@ -15,10 +15,11 @@ pub use element::*;
 use gpui::{
     action,
     elements::Text,
+    fonts::TextStyle,
     geometry::vector::{vec2f, Vector2F},
     keymap::Binding,
     text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
-    MutableAppContext, RenderContext, View, ViewContext, WeakViewHandle,
+    MutableAppContext, RenderContext, View, ViewContext, WeakModelHandle, WeakViewHandle,
 };
 use items::BufferItemHandle;
 use language::{
@@ -29,6 +30,7 @@ pub use multi_buffer::MultiBuffer;
 use multi_buffer::{
     Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot, ToOffset, ToPoint,
 };
+use postage::watch;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use smol::Timer;
@@ -3787,6 +3789,48 @@ pub fn diagnostic_style(
     }
 }
 
+pub fn settings_builder(
+    buffer: WeakModelHandle<MultiBuffer>,
+    settings: watch::Receiver<workspace::Settings>,
+) -> impl Fn(&AppContext) -> EditorSettings {
+    move |cx| {
+        let settings = settings.borrow();
+        let font_cache = cx.font_cache();
+        let font_family_id = settings.buffer_font_family;
+        let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
+        let font_properties = Default::default();
+        let font_id = font_cache
+            .select_font(font_family_id, &font_properties)
+            .unwrap();
+        let font_size = settings.buffer_font_size;
+
+        let mut theme = settings.theme.editor.clone();
+        theme.text = TextStyle {
+            color: theme.text.color,
+            font_family_name,
+            font_family_id,
+            font_id,
+            font_size,
+            font_properties,
+            underline: None,
+        };
+        let language = buffer.upgrade(cx).and_then(|buf| buf.read(cx).language(cx));
+        let soft_wrap = match settings.soft_wrap(language) {
+            workspace::settings::SoftWrap::None => SoftWrap::None,
+            workspace::settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
+            workspace::settings::SoftWrap::PreferredLineLength => {
+                SoftWrap::Column(settings.preferred_line_length(language).saturating_sub(1))
+            }
+        };
+
+        EditorSettings {
+            tab_size: settings.tab_size,
+            soft_wrap,
+            style: theme,
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/editor/src/items.rs 🔗

@@ -1,10 +1,9 @@
-use crate::{Editor, EditorSettings, Event};
+use crate::{Editor, Event};
 use crate::{MultiBuffer, ToPoint as _};
 use anyhow::Result;
 use gpui::{
-    elements::*, fonts::TextStyle, AppContext, Entity, ModelContext, ModelHandle,
-    MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
-    WeakModelHandle,
+    elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
+    Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
 };
 use language::{Diagnostic, File as _};
 use postage::watch;
@@ -13,8 +12,7 @@ use std::fmt::Write;
 use std::path::Path;
 use text::{Point, Selection};
 use workspace::{
-    settings, EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView,
-    WeakItemHandle,
+    EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle,
 };
 
 pub struct BufferOpener;
@@ -53,42 +51,7 @@ impl ItemHandle for BufferItemHandle {
         Box::new(cx.add_view(window_id, |cx| {
             Editor::for_buffer(
                 self.0.clone(),
-                move |cx| {
-                    let settings = settings.borrow();
-                    let font_cache = cx.font_cache();
-                    let font_family_id = settings.buffer_font_family;
-                    let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
-                    let font_properties = Default::default();
-                    let font_id = font_cache
-                        .select_font(font_family_id, &font_properties)
-                        .unwrap();
-                    let font_size = settings.buffer_font_size;
-
-                    let mut theme = settings.theme.editor.clone();
-                    theme.text = TextStyle {
-                        color: theme.text.color,
-                        font_family_name,
-                        font_family_id,
-                        font_id,
-                        font_size,
-                        font_properties,
-                        underline: None,
-                    };
-                    let language = buffer.upgrade(cx).and_then(|buf| buf.read(cx).language(cx));
-                    let soft_wrap = match settings.soft_wrap(language) {
-                        settings::SoftWrap::None => crate::SoftWrap::None,
-                        settings::SoftWrap::EditorWidth => crate::SoftWrap::EditorWidth,
-                        settings::SoftWrap::PreferredLineLength => crate::SoftWrap::Column(
-                            settings.preferred_line_length(language).saturating_sub(1),
-                        ),
-                    };
-
-                    EditorSettings {
-                        tab_size: settings.tab_size,
-                        soft_wrap,
-                        style: theme,
-                    }
-                },
+                crate::settings_builder(buffer, settings),
                 cx,
             )
         }))

crates/language/src/buffer.rs 🔗

@@ -87,6 +87,8 @@ pub struct BufferSnapshot {
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Diagnostic {
+    pub source: Option<String>,
+    pub code: Option<String>,
     pub severity: DiagnosticSeverity,
     pub message: String,
     pub group_id: usize,
@@ -720,7 +722,7 @@ impl Buffer {
     pub fn update_diagnostics(
         &mut self,
         version: Option<i32>,
-        mut diagnostics: Vec<lsp::Diagnostic>,
+        mut diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
         cx: &mut ModelContext<Self>,
     ) -> Result<Operation> {
         diagnostics.sort_unstable_by_key(|d| (d.range.start, d.range.end));
@@ -736,7 +738,6 @@ impl Buffer {
         } else {
             self.deref()
         };
-        let abs_path = self.file.as_ref().and_then(|f| f.abs_path());
 
         let empty_set = HashSet::new();
         let disk_based_sources = self
@@ -750,26 +751,11 @@ impl Buffer {
             .peekable();
         let mut last_edit_old_end = PointUtf16::zero();
         let mut last_edit_new_end = PointUtf16::zero();
-        let mut group_ids_by_diagnostic_range = HashMap::new();
-        let mut diagnostics_by_group_id = HashMap::new();
-        let mut next_group_id = 0;
-        'outer: for diagnostic in &diagnostics {
-            let mut start = diagnostic.range.start.to_point_utf16();
-            let mut end = diagnostic.range.end.to_point_utf16();
-            let source = diagnostic.source.as_ref();
-            let code = diagnostic.code.as_ref();
-            let group_id = diagnostic_ranges(&diagnostic, abs_path.as_deref())
-                .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.as_deref()) {
-                        group_ids_by_diagnostic_range.insert((source, code, range), group_id);
-                    }
-                    group_id
-                });
-
-            if diagnostic
+        'outer: for entry in &mut diagnostics {
+            let mut start = entry.range.start;
+            let mut end = entry.range.end;
+            if entry
+                .diagnostic
                 .source
                 .as_ref()
                 .map_or(false, |source| disk_based_sources.contains(source))
@@ -790,46 +776,20 @@ impl Buffer {
                 end = last_edit_new_end + (end - last_edit_old_end);
             }
 
-            let mut range = content.clip_point_utf16(start, Bias::Left)
+            entry.range = content.clip_point_utf16(start, Bias::Left)
                 ..content.clip_point_utf16(end, Bias::Right);
-            if range.start == range.end {
-                range.end.column += 1;
-                range.end = content.clip_point_utf16(range.end, Bias::Right);
-                if range.start == range.end && range.end.column > 0 {
-                    range.start.column -= 1;
-                    range.start = content.clip_point_utf16(range.start, Bias::Left);
+            if entry.range.start == entry.range.end {
+                entry.range.end.column += 1;
+                entry.range.end = content.clip_point_utf16(entry.range.end, Bias::Right);
+                if entry.range.start == entry.range.end && entry.range.end.column > 0 {
+                    entry.range.start.column -= 1;
+                    entry.range.start = content.clip_point_utf16(entry.range.start, Bias::Left);
                 }
             }
-
-            diagnostics_by_group_id
-                .entry(group_id)
-                .or_insert(Vec::new())
-                .push(DiagnosticEntry {
-                    range,
-                    diagnostic: Diagnostic {
-                        severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
-                        message: diagnostic.message.clone(),
-                        group_id,
-                        is_primary: false,
-                    },
-                });
         }
 
         drop(edits_since_save);
-        let new_diagnostics = DiagnosticSet::new(
-            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
-                }),
-            content,
-        );
-        self.diagnostics = new_diagnostics;
+        self.diagnostics = DiagnosticSet::new(diagnostics, content);
 
         if let Some(version) = version {
             let language_server = self.language_server.as_mut().unwrap();
@@ -1971,16 +1931,6 @@ impl ToTreeSitterPoint for Point {
     }
 }
 
-trait ToPointUtf16 {
-    fn to_point_utf16(self) -> PointUtf16;
-}
-
-impl ToPointUtf16 for lsp::Position {
-    fn to_point_utf16(self) -> PointUtf16 {
-        PointUtf16::new(self.line, self.character)
-    }
-}
-
 impl operation_queue::Operation for Operation {
     fn lamport_timestamp(&self) -> clock::Lamport {
         match self {
@@ -2000,32 +1950,17 @@ impl operation_queue::Operation for Operation {
     }
 }
 
-fn diagnostic_ranges<'a>(
-    diagnostic: &'a lsp::Diagnostic,
-    abs_path: Option<&'a Path>,
-) -> impl 'a + Iterator<Item = Range<PointUtf16>> {
-    diagnostic
-        .related_information
-        .iter()
-        .flatten()
-        .filter_map(move |info| {
-            if info.location.uri.to_file_path().ok()? == abs_path? {
-                let info_start = PointUtf16::new(
-                    info.location.range.start.line,
-                    info.location.range.start.character,
-                );
-                let info_end = PointUtf16::new(
-                    info.location.range.end.line,
-                    info.location.range.end.character,
-                );
-                Some(info_start..info_end)
-            } else {
-                None
-            }
-        })
-        .chain(Some(
-            diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(),
-        ))
+impl Default for Diagnostic {
+    fn default() -> Self {
+        Self {
+            source: Default::default(),
+            code: Default::default(),
+            severity: DiagnosticSeverity::ERROR,
+            message: Default::default(),
+            group_id: Default::default(),
+            is_primary: Default::default(),
+        }
+    }
 }
 
 pub fn contiguous_ranges(

crates/language/src/proto.rs 🔗

@@ -117,6 +117,8 @@ pub fn serialize_diagnostics<'a>(
             } as i32,
             group_id: entry.diagnostic.group_id as u64,
             is_primary: entry.diagnostic.is_primary,
+            code: entry.diagnostic.code.clone(),
+            source: entry.diagnostic.source.clone(),
         })
         .collect()
 }
@@ -269,6 +271,8 @@ pub fn deserialize_diagnostics(
                     message: diagnostic.message,
                     group_id: diagnostic.group_id as usize,
                     is_primary: diagnostic.is_primary,
+                    code: diagnostic.code,
+                    source: diagnostic.source,
                 },
             })
         })

crates/language/src/tests.rs 🔗

@@ -516,23 +516,29 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
             .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()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
+                        diagnostic: Diagnostic {
+                            severity: 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()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
+                        diagnostic: Diagnostic {
+                            severity: 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()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "undefined variable 'CCC'".to_string(),
+                            ..Default::default()
+                        },
                     },
                 ],
                 cx,
@@ -553,6 +559,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         message: "undefined variable 'BB'".to_string(),
                         group_id: 1,
                         is_primary: true,
+                        ..Default::default()
                     },
                 },
                 DiagnosticEntry {
@@ -562,6 +569,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         message: "undefined variable 'CCC'".to_string(),
                         group_id: 2,
                         is_primary: true,
+                        ..Default::default()
                     }
                 }
             ]
@@ -592,17 +600,21 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
             .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()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "undefined variable 'A'".to_string(),
+                            ..Default::default()
+                        },
                     },
-                    lsp::Diagnostic {
-                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
-                        severity: Some(lsp::DiagnosticSeverity::WARNING),
-                        message: "unreachable statement".to_string(),
-                        ..Default::default()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::WARNING,
+                            message: "unreachable statement".to_string(),
+                            ..Default::default()
+                        },
                     },
                 ],
                 cx,
@@ -621,6 +633,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         message: "unreachable statement".to_string(),
                         group_id: 1,
                         is_primary: true,
+                        ..Default::default()
                     }
                 },
                 DiagnosticEntry {
@@ -630,6 +643,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         message: "undefined variable 'A'".to_string(),
                         group_id: 0,
                         is_primary: true,
+                        ..Default::default()
                     },
                 }
             ]
@@ -670,19 +684,23 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
             .update_diagnostics(
                 Some(change_notification_2.text_document.version),
                 vec![
-                    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(),
-                        source: Some("disk".to_string()),
-                        ..Default::default()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "undefined variable 'BB'".to_string(),
+                            source: Some("disk".to_string()),
+                            ..Default::default()
+                        },
                     },
-                    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(),
-                        source: Some("disk".to_string()),
-                        ..Default::default()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "undefined variable 'A'".to_string(),
+                            source: Some("disk".to_string()),
+                            ..Default::default()
+                        },
                     },
                 ],
                 cx,
@@ -701,6 +719,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         message: "undefined variable 'A'".to_string(),
                         group_id: 0,
                         is_primary: true,
+                        ..Default::default()
                     }
                 },
                 DiagnosticEntry {
@@ -710,6 +729,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         message: "undefined variable 'BB'".to_string(),
                         group_id: 1,
                         is_primary: true,
+                        ..Default::default()
                     },
                 }
             ]
@@ -732,23 +752,21 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
             .update_diagnostics(
                 None,
                 vec![
-                    lsp::Diagnostic {
-                        range: lsp::Range::new(
-                            lsp::Position::new(0, 10),
-                            lsp::Position::new(0, 10),
-                        ),
-                        severity: Some(lsp::DiagnosticSeverity::ERROR),
-                        message: "syntax error 1".to_string(),
-                        ..Default::default()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "syntax error 1".to_string(),
+                            ..Default::default()
+                        },
                     },
-                    lsp::Diagnostic {
-                        range: lsp::Range::new(
-                            lsp::Position::new(1, 10),
-                            lsp::Position::new(1, 10),
-                        ),
-                        severity: Some(lsp::DiagnosticSeverity::ERROR),
-                        message: "syntax error 2".to_string(),
-                        ..Default::default()
+                    DiagnosticEntry {
+                        range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "syntax error 2".to_string(),
+                            ..Default::default()
+                        },
                     },
                 ],
                 cx,
@@ -766,9 +784,9 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
                 .collect::<Vec<_>>(),
             &[
                 ("let one = ", None),
-                (";", Some(lsp::DiagnosticSeverity::ERROR)),
+                (";", Some(DiagnosticSeverity::ERROR)),
                 ("\nlet two =", None),
-                (" ", Some(lsp::DiagnosticSeverity::ERROR)),
+                (" ", Some(DiagnosticSeverity::ERROR)),
                 ("\nlet three = 3;\n", None)
             ]
         );
@@ -776,224 +794,6 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
     });
 }
 
-#[gpui::test]
-async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
-    cx.add_model(|cx| {
-        let text = "
-            fn foo(mut v: Vec<usize>) {
-                for x in &v {
-                    v.push(1);
-                }
-            }
-        "
-        .unindent();
-
-        let file = FakeFile::new("/example.rs");
-        let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
-        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
-        let diagnostics = vec![
-            lsp::Diagnostic {
-                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
-                severity: Some(DiagnosticSeverity::WARNING),
-                message: "error 1".to_string(),
-                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
-                    location: lsp::Location {
-                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
-                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
-                    },
-                    message: "error 1 hint 1".to_string(),
-                }]),
-                ..Default::default()
-            },
-            lsp::Diagnostic {
-                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
-                severity: Some(DiagnosticSeverity::HINT),
-                message: "error 1 hint 1".to_string(),
-                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
-                    location: lsp::Location {
-                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
-                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
-                    },
-                    message: "original diagnostic".to_string(),
-                }]),
-                ..Default::default()
-            },
-            lsp::Diagnostic {
-                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
-                severity: Some(DiagnosticSeverity::ERROR),
-                message: "error 2".to_string(),
-                related_information: Some(vec![
-                    lsp::DiagnosticRelatedInformation {
-                        location: lsp::Location {
-                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
-                            range: lsp::Range::new(
-                                lsp::Position::new(1, 13),
-                                lsp::Position::new(1, 15),
-                            ),
-                        },
-                        message: "error 2 hint 1".to_string(),
-                    },
-                    lsp::DiagnosticRelatedInformation {
-                        location: lsp::Location {
-                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
-                            range: lsp::Range::new(
-                                lsp::Position::new(1, 13),
-                                lsp::Position::new(1, 15),
-                            ),
-                        },
-                        message: "error 2 hint 2".to_string(),
-                    },
-                ]),
-                ..Default::default()
-            },
-            lsp::Diagnostic {
-                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
-                severity: Some(DiagnosticSeverity::HINT),
-                message: "error 2 hint 1".to_string(),
-                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
-                    location: lsp::Location {
-                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
-                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
-                    },
-                    message: "original diagnostic".to_string(),
-                }]),
-                ..Default::default()
-            },
-            lsp::Diagnostic {
-                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
-                severity: Some(DiagnosticSeverity::HINT),
-                message: "error 2 hint 2".to_string(),
-                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
-                    location: lsp::Location {
-                        uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
-                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
-                    },
-                    message: "original diagnostic".to_string(),
-                }]),
-                ..Default::default()
-            },
-        ];
-        buffer.update_diagnostics(None, diagnostics, cx).unwrap();
-        assert_eq!(
-            buffer
-                .snapshot()
-                .diagnostics_in_range::<_, Point>(0..buffer.len())
-                .collect::<Vec<_>>(),
-            &[
-                DiagnosticEntry {
-                    range: Point::new(1, 8)..Point::new(1, 9),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::WARNING,
-                        message: "error 1".to_string(),
-                        group_id: 0,
-                        is_primary: true,
-                    }
-                },
-                DiagnosticEntry {
-                    range: Point::new(1, 8)..Point::new(1, 9),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: "error 1 hint 1".to_string(),
-                        group_id: 0,
-                        is_primary: false,
-                    }
-                },
-                DiagnosticEntry {
-                    range: Point::new(1, 13)..Point::new(1, 15),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: "error 2 hint 1".to_string(),
-                        group_id: 1,
-                        is_primary: false,
-                    }
-                },
-                DiagnosticEntry {
-                    range: Point::new(1, 13)..Point::new(1, 15),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: "error 2 hint 2".to_string(),
-                        group_id: 1,
-                        is_primary: false,
-                    }
-                },
-                DiagnosticEntry {
-                    range: Point::new(2, 8)..Point::new(2, 17),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::ERROR,
-                        message: "error 2".to_string(),
-                        group_id: 1,
-                        is_primary: true,
-                    }
-                }
-            ]
-        );
-
-        assert_eq!(
-            buffer
-                .snapshot()
-                .diagnostic_group::<Point>(0)
-                .collect::<Vec<_>>(),
-            &[
-                DiagnosticEntry {
-                    range: Point::new(1, 8)..Point::new(1, 9),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::WARNING,
-                        message: "error 1".to_string(),
-                        group_id: 0,
-                        is_primary: true,
-                    }
-                },
-                DiagnosticEntry {
-                    range: Point::new(1, 8)..Point::new(1, 9),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: "error 1 hint 1".to_string(),
-                        group_id: 0,
-                        is_primary: false,
-                    }
-                },
-            ]
-        );
-        assert_eq!(
-            buffer
-                .snapshot()
-                .diagnostic_group::<Point>(1)
-                .collect::<Vec<_>>(),
-            &[
-                DiagnosticEntry {
-                    range: Point::new(1, 13)..Point::new(1, 15),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: "error 2 hint 1".to_string(),
-                        group_id: 1,
-                        is_primary: false,
-                    }
-                },
-                DiagnosticEntry {
-                    range: Point::new(1, 13)..Point::new(1, 15),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::HINT,
-                        message: "error 2 hint 2".to_string(),
-                        group_id: 1,
-                        is_primary: false,
-                    }
-                },
-                DiagnosticEntry {
-                    range: Point::new(2, 8)..Point::new(2, 17),
-                    diagnostic: Diagnostic {
-                        severity: DiagnosticSeverity::ERROR,
-                        message: "error 2".to_string(),
-                        group_id: 1,
-                        is_primary: true,
-                    }
-                }
-            ]
-        );
-
-        buffer
-    });
-}
-
 fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
     buffer: &Buffer,
     range: Range<T>,

crates/project/src/project.rs 🔗

@@ -4,10 +4,11 @@ mod worktree;
 
 use anyhow::Result;
 use client::{Client, UserStore};
+use clock::ReplicaId;
 use futures::Future;
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
-use language::LanguageRegistry;
+use language::{DiagnosticEntry, LanguageRegistry, PointUtf16};
 use std::{
     path::Path,
     sync::{atomic::AtomicBool, Arc},
@@ -62,6 +63,11 @@ impl Project {
         }
     }
 
+    pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
+        // TODO
+        self.worktrees.first().unwrap().read(cx).replica_id()
+    }
+
     pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
         &self.worktrees
     }
@@ -159,6 +165,13 @@ impl Project {
         }
     }
 
+    pub fn diagnostics<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl Iterator<Item = (&'a Path, &'a [DiagnosticEntry<PointUtf16>])> {
+        std::iter::empty()
+    }
+
     pub fn active_entry(&self) -> Option<ProjectEntry> {
         self.active_entry
     }

crates/project/src/worktree.rs 🔗

@@ -12,7 +12,10 @@ use gpui::{
     executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
     Task, UpgradeModelHandle, WeakModelHandle,
 };
-use language::{Buffer, Language, LanguageRegistry, Operation, Rope};
+use language::{
+    Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, Operation,
+    PointUtf16, Rope,
+};
 use lazy_static::lazy_static;
 use lsp::LanguageServer;
 use parking_lot::Mutex;
@@ -30,7 +33,7 @@ use std::{
     ffi::{OsStr, OsString},
     fmt,
     future::Future,
-    ops::Deref,
+    ops::{Deref, Range},
     path::{Path, PathBuf},
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
@@ -40,7 +43,7 @@ use std::{
 };
 use sum_tree::Bias;
 use sum_tree::{Edit, SeekTarget, SumTree};
-use util::{ResultExt, TryFutureExt};
+use util::{post_inc, ResultExt, TryFutureExt};
 
 lazy_static! {
     static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
@@ -747,20 +750,67 @@ impl Worktree {
         cx: &mut ModelContext<Worktree>,
     ) -> Result<()> {
         let this = self.as_local_mut().ok_or_else(|| anyhow!("not local"))?;
-        let file_path = params
+        let abs_path = params
             .uri
             .to_file_path()
-            .map_err(|_| anyhow!("URI is not a file"))?
+            .map_err(|_| anyhow!("URI is not a file"))?;
+        let worktree_path = abs_path
             .strip_prefix(&this.abs_path)
             .context("path is not within worktree")?
             .to_owned();
 
+        let mut group_ids_by_diagnostic_range = HashMap::new();
+        let mut diagnostics_by_group_id = HashMap::new();
+        let mut next_group_id = 0;
+        for diagnostic in &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 {
+                        source: diagnostic.source.clone(),
+                        code: diagnostic.code.clone(),
+                        severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
+                        message: diagnostic.message.clone(),
+                        group_id,
+                        is_primary: false,
+                    },
+                });
+        }
+
+        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<_>>();
+
         for buffer in this.open_buffers.values() {
             if let Some(buffer) = buffer.upgrade(cx) {
                 if buffer
                     .read(cx)
                     .file()
-                    .map_or(false, |file| file.path().as_ref() == file_path)
+                    .map_or(false, |file| file.path().as_ref() == worktree_path)
                 {
                     let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
                         (
@@ -774,7 +824,7 @@ impl Worktree {
             }
         }
 
-        this.diagnostics.insert(file_path, params.diagnostics);
+        this.diagnostics.insert(worktree_path, diagnostics);
         Ok(())
     }
 
@@ -838,7 +888,7 @@ pub struct LocalWorktree {
     share: Option<ShareState>,
     open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
     shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
-    diagnostics: HashMap<PathBuf, Vec<lsp::Diagnostic>>,
+    diagnostics: HashMap<PathBuf, Vec<DiagnosticEntry<PointUtf16>>>,
     collaborators: HashMap<PeerId, Collaborator>,
     queued_operations: Vec<(u64, Operation)>,
     languages: Arc<LanguageRegistry>,
@@ -2998,6 +3048,44 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
     }
 }
 
+trait ToPointUtf16 {
+    fn to_point_utf16(self) -> PointUtf16;
+}
+
+impl ToPointUtf16 for lsp::Position {
+    fn to_point_utf16(self) -> PointUtf16 {
+        PointUtf16::new(self.line, self.character)
+    }
+}
+
+fn diagnostic_ranges<'a>(
+    diagnostic: &'a lsp::Diagnostic,
+    abs_path: &'a Path,
+) -> impl 'a + Iterator<Item = Range<PointUtf16>> {
+    diagnostic
+        .related_information
+        .iter()
+        .flatten()
+        .filter_map(move |info| {
+            if info.location.uri.to_file_path().ok()? == abs_path {
+                let info_start = PointUtf16::new(
+                    info.location.range.start.line,
+                    info.location.range.start.character,
+                );
+                let info_end = PointUtf16::new(
+                    info.location.range.end.line,
+                    info.location.range.end.character,
+                );
+                Some(info_start..info_end)
+            } else {
+                None
+            }
+        })
+        .chain(Some(
+            diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(),
+        ))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -3740,6 +3828,224 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
+        cx.add_model(|cx| {
+            let text = "
+            fn foo(mut v: Vec<usize>) {
+                for x in &v {
+                    v.push(1);
+                }
+            }
+        "
+            .unindent();
+
+            let file = FakeFile::new("/example.rs");
+            let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
+            buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
+            let diagnostics = vec![
+                DiagnosticEntry {
+                    range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
+                    diagnostic: Diagnostic {
+                        severity: Some(DiagnosticSeverity::WARNING),
+                        message: "error 1".to_string(),
+                        related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                            location: lsp::Location {
+                                uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                                range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
+                            },
+                            message: "error 1 hint 1".to_string(),
+                        }]),
+                        ..Default::default()
+                    },
+                },
+                DiagnosticEntry {
+                    range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
+                    diagnostic: Diagnostic {},
+                    severity: Some(DiagnosticSeverity::HINT),
+                    message: "error 1 hint 1".to_string(),
+                    related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                        location: lsp::Location {
+                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
+                        },
+                        message: "original diagnostic".to_string(),
+                    }]),
+                    ..Default::default()
+                },
+                DiagnosticEntry {
+                    range: PointUtf16::new(2, 8)..PointUtf16::new(2, 17),
+                    diagnostic: Diagnostic {},
+                    severity: Some(DiagnosticSeverity::ERROR),
+                    message: "error 2".to_string(),
+                    related_information: Some(vec![
+                        lsp::DiagnosticRelatedInformation {
+                            location: lsp::Location {
+                                uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                                range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15),
+                            },
+                            message: "error 2 hint 1".to_string(),
+                        },
+                        lsp::DiagnosticRelatedInformation {
+                            location: lsp::Location {
+                                uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                                range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15),
+                            },
+                            message: "error 2 hint 2".to_string(),
+                        },
+                    ]),
+                    ..Default::default()
+                },
+                DiagnosticEntry {
+                    range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15),
+                    diagnostic: Diagnostic {},
+                    severity: Some(DiagnosticSeverity::HINT),
+                    message: "error 2 hint 1".to_string(),
+                    related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                        location: lsp::Location {
+                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 17),
+                        },
+                        message: "original diagnostic".to_string(),
+                    }]),
+                    ..Default::default()
+                },
+                DiagnosticEntry {
+                    range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15),
+                    diagnostic: Diagnostic {},
+                    severity: Some(DiagnosticSeverity::HINT),
+                    message: "error 2 hint 2".to_string(),
+                    related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                        location: lsp::Location {
+                            uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
+                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 17),
+                        },
+                        message: "original diagnostic".to_string(),
+                    }]),
+                    ..Default::default()
+                },
+            ];
+            buffer.update_diagnostics(None, diagnostics, cx).unwrap();
+            assert_eq!(
+                buffer
+                    .snapshot()
+                    .diagnostics_in_range::<_, Point>(0..buffer.len())
+                    .collect::<Vec<_>>(),
+                &[
+                    DiagnosticEntry {
+                        range: Point::new(1, 8)..Point::new(1, 9),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::WARNING,
+                            message: "error 1".to_string(),
+                            group_id: 0,
+                            is_primary: true,
+                        }
+                    },
+                    DiagnosticEntry {
+                        range: Point::new(1, 8)..Point::new(1, 9),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::HINT,
+                            message: "error 1 hint 1".to_string(),
+                            group_id: 0,
+                            is_primary: false,
+                        }
+                    },
+                    DiagnosticEntry {
+                        range: Point::new(1, 13)..Point::new(1, 15),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::HINT,
+                            message: "error 2 hint 1".to_string(),
+                            group_id: 1,
+                            is_primary: false,
+                        }
+                    },
+                    DiagnosticEntry {
+                        range: Point::new(1, 13)..Point::new(1, 15),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::HINT,
+                            message: "error 2 hint 2".to_string(),
+                            group_id: 1,
+                            is_primary: false,
+                        }
+                    },
+                    DiagnosticEntry {
+                        range: Point::new(2, 8)..Point::new(2, 17),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "error 2".to_string(),
+                            group_id: 1,
+                            is_primary: true,
+                        }
+                    }
+                ]
+            );
+
+            assert_eq!(
+                buffer
+                    .snapshot()
+                    .diagnostic_group::<Point>(0)
+                    .collect::<Vec<_>>(),
+                &[
+                    DiagnosticEntry {
+                        range: Point::new(1, 8)..Point::new(1, 9),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::WARNING,
+                            message: "error 1".to_string(),
+                            group_id: 0,
+                            is_primary: true,
+                        }
+                    },
+                    DiagnosticEntry {
+                        range: Point::new(1, 8)..Point::new(1, 9),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::HINT,
+                            message: "error 1 hint 1".to_string(),
+                            group_id: 0,
+                            is_primary: false,
+                        }
+                    },
+                ]
+            );
+            assert_eq!(
+                buffer
+                    .snapshot()
+                    .diagnostic_group::<Point>(1)
+                    .collect::<Vec<_>>(),
+                &[
+                    DiagnosticEntry {
+                        range: Point::new(1, 13)..Point::new(1, 15),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::HINT,
+                            message: "error 2 hint 1".to_string(),
+                            group_id: 1,
+                            is_primary: false,
+                        }
+                    },
+                    DiagnosticEntry {
+                        range: Point::new(1, 13)..Point::new(1, 15),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::HINT,
+                            message: "error 2 hint 2".to_string(),
+                            group_id: 1,
+                            is_primary: false,
+                        }
+                    },
+                    DiagnosticEntry {
+                        range: Point::new(2, 8)..Point::new(2, 17),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "error 2".to_string(),
+                            group_id: 1,
+                            is_primary: true,
+                        }
+                    }
+                ]
+            );
+
+            buffer
+        });
+    }
+
     #[gpui::test(iterations = 100)]
     fn test_random(mut rng: StdRng) {
         let operations = env::var("OPERATIONS")

crates/rpc/proto/zed.proto 🔗

@@ -269,6 +269,8 @@ message Diagnostic {
     string message = 4;
     uint64 group_id = 5;
     bool is_primary = 6;
+    optional string code = 7;
+    optional string source = 8;
     enum Severity {
         None = 0;
         Error = 1;