In a diagnostic group, mark the highest-severity diagnostic as primary

Antonio Scandurra and Nathan Sobo created

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

Change summary

crates/editor/src/lib.rs       |   2 
crates/language/src/lib.rs     | 117 ++++++++++++++++++++---------------
crates/language/src/proto.rs   |   2 
crates/language/src/tests.rs   |  36 ++++++++---
crates/project/src/worktree.rs |   1 
crates/rpc/proto/zed.proto     |   1 
crates/server/src/rpc.rs       |   4 
7 files changed, 100 insertions(+), 63 deletions(-)

Detailed changes

crates/editor/src/lib.rs 🔗

@@ -2205,7 +2205,7 @@ impl Editor {
     }
 
     pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext<Self>) {
-        let selection = self.selections::<usize>(cx).last().unwrap();
+        let selection = self.newest_selection(cx);
         let buffer = self.buffer.read(cx.as_ref());
         let diagnostic_group_id = dbg!(buffer
             .diagnostics_in_range::<_, usize>(selection.head()..buffer.len())

crates/language/src/lib.rs 🔗

@@ -86,6 +86,7 @@ pub struct Diagnostic {
     pub severity: DiagnosticSeverity,
     pub message: String,
     pub group_id: usize,
+    pub is_primary: bool,
 }
 
 struct LanguageServerState {
@@ -717,68 +718,82 @@ impl Buffer {
                 .peekable();
             let mut last_edit_old_end = PointUtf16::zero();
             let mut last_edit_new_end = PointUtf16::zero();
-            let mut groups = HashMap::new();
+            let mut group_ids_by_diagnostic_range = HashMap::new();
+            let mut diagnostics_by_group_id = HashMap::new();
             let mut next_group_id = 0;
-
-            content.anchor_range_multimap(
-                Bias::Left,
-                Bias::Right,
-                diagnostics.iter().filter_map(|diagnostic| {
-                    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| groups.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()) {
-                                groups.insert((source, code, range), group_id);
-                            }
-                            group_id
-                        });
-
-                    if diagnostic
-                        .source
-                        .as_ref()
-                        .map_or(false, |source| disk_based_sources.contains(source))
-                    {
-                        while let Some(edit) = edits_since_save.peek() {
-                            if edit.old.end <= start {
-                                last_edit_old_end = edit.old.end;
-                                last_edit_new_end = edit.new.end;
-                                edits_since_save.next();
-                            } else if edit.old.start <= end && edit.old.end >= start {
-                                return None;
-                            } else {
-                                break;
-                            }
+            '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
+                    .source
+                    .as_ref()
+                    .map_or(false, |source| disk_based_sources.contains(source))
+                {
+                    while let Some(edit) = edits_since_save.peek() {
+                        if edit.old.end <= start {
+                            last_edit_old_end = edit.old.end;
+                            last_edit_new_end = edit.new.end;
+                            edits_since_save.next();
+                        } else if edit.old.start <= end && edit.old.end >= start {
+                            continue 'outer;
+                        } else {
+                            break;
                         }
-
-                        start = last_edit_new_end + (start - last_edit_old_end);
-                        end = last_edit_new_end + (end - last_edit_old_end);
                     }
 
-                    let mut 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);
-                        }
+                    start = last_edit_new_end + (start - last_edit_old_end);
+                    end = last_edit_new_end + (end - last_edit_old_end);
+                }
+
+                let mut 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);
                     }
-                    Some((
+                }
+
+                diagnostics_by_group_id
+                    .entry(group_id)
+                    .or_insert(Vec::new())
+                    .push((
                         range,
                         Diagnostic {
                             severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
                             message: diagnostic.message.clone(),
                             group_id,
+                            is_primary: false,
                         },
-                    ))
-                }),
+                    ));
+            }
+
+            content.anchor_range_multimap(
+                Bias::Left,
+                Bias::Right,
+                diagnostics_by_group_id
+                    .into_values()
+                    .flat_map(|mut diagnostics| {
+                        let primary_diagnostic =
+                            diagnostics.iter_mut().min_by_key(|d| d.1.severity).unwrap();
+                        primary_diagnostic.1.is_primary = true;
+                        diagnostics
+                    }),
             )
         };
 

crates/language/src/proto.rs 🔗

@@ -142,6 +142,7 @@ pub fn serialize_diagnostics(map: &AnchorRangeMultimap<Diagnostic>) -> proto::Di
                     _ => proto::diagnostic::Severity::None,
                 } as i32,
                 group_id: diagnostic.group_id as u64,
+                is_primary: diagnostic.is_primary,
             })
             .collect(),
     }
@@ -310,6 +311,7 @@ pub fn deserialize_diagnostics(message: proto::DiagnosticSet) -> AnchorRangeMult
                     },
                     message: diagnostic.message,
                     group_id: diagnostic.group_id as usize,
+                    is_primary: diagnostic.is_primary,
                 },
             ))
         }),

crates/language/src/tests.rs 🔗

@@ -484,6 +484,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         severity: DiagnosticSeverity::ERROR,
                         message: "undefined variable 'BB'".to_string(),
                         group_id: 1,
+                        is_primary: true,
                     },
                 ),
                 (
@@ -492,6 +493,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         severity: DiagnosticSeverity::ERROR,
                         message: "undefined variable 'CCC'".to_string(),
                         group_id: 2,
+                        is_primary: true,
                     }
                 )
             ]
@@ -549,6 +551,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         severity: DiagnosticSeverity::WARNING,
                         message: "unreachable statement".to_string(),
                         group_id: 1,
+                        is_primary: true,
                     }
                 ),
                 (
@@ -557,6 +560,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         severity: DiagnosticSeverity::ERROR,
                         message: "undefined variable 'A'".to_string(),
                         group_id: 0,
+                        is_primary: true,
                     },
                 )
             ]
@@ -626,6 +630,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         severity: DiagnosticSeverity::ERROR,
                         message: "undefined variable 'A'".to_string(),
                         group_id: 0,
+                        is_primary: true,
                     }
                 ),
                 (
@@ -634,6 +639,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                         severity: DiagnosticSeverity::ERROR,
                         message: "undefined variable 'BB'".to_string(),
                         group_id: 1,
+                        is_primary: true,
                     },
                 )
             ]
@@ -808,7 +814,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::WARNING,
                         message: "error 1".to_string(),
-                        group_id: 0
+                        group_id: 0,
+                        is_primary: true,
                     }
                 ),
                 (
@@ -816,7 +823,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::HINT,
                         message: "error 1 hint 1".to_string(),
-                        group_id: 0
+                        group_id: 0,
+                        is_primary: false,
                     }
                 ),
                 (
@@ -824,7 +832,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::HINT,
                         message: "error 2 hint 1".to_string(),
-                        group_id: 1
+                        group_id: 1,
+                        is_primary: false,
                     }
                 ),
                 (
@@ -832,7 +841,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::HINT,
                         message: "error 2 hint 2".to_string(),
-                        group_id: 1
+                        group_id: 1,
+                        is_primary: false,
                     }
                 ),
                 (
@@ -840,7 +850,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::ERROR,
                         message: "error 2".to_string(),
-                        group_id: 1
+                        group_id: 1,
+                        is_primary: true,
                     }
                 )
             ]
@@ -854,7 +865,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::WARNING,
                         message: "error 1".to_string(),
-                        group_id: 0
+                        group_id: 0,
+                        is_primary: true,
                     }
                 ),
                 (
@@ -862,7 +874,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::HINT,
                         message: "error 1 hint 1".to_string(),
-                        group_id: 0
+                        group_id: 0,
+                        is_primary: false,
                     }
                 ),
             ]
@@ -875,7 +888,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::HINT,
                         message: "error 2 hint 1".to_string(),
-                        group_id: 1
+                        group_id: 1,
+                        is_primary: false,
                     }
                 ),
                 (
@@ -883,7 +897,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::HINT,
                         message: "error 2 hint 2".to_string(),
-                        group_id: 1
+                        group_id: 1,
+                        is_primary: false,
                     }
                 ),
                 (
@@ -891,7 +906,8 @@ async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
                     &Diagnostic {
                         severity: DiagnosticSeverity::ERROR,
                         message: "error 2".to_string(),
-                        group_id: 1
+                        group_id: 1,
+                        is_primary: true,
                     }
                 )
             ]

crates/project/src/worktree.rs 🔗

@@ -3635,6 +3635,7 @@ mod tests {
                         severity: lsp::DiagnosticSeverity::ERROR,
                         message: "undefined variable 'A'".to_string(),
                         group_id: 0,
+                        is_primary: true
                     }
                 )]
             )

crates/rpc/proto/zed.proto 🔗

@@ -257,6 +257,7 @@ message Diagnostic {
     Severity severity = 3;
     string message = 4;
     uint64 group_id = 5;
+    bool is_primary = 6;
     enum Severity {
         None = 0;
         Error = 1;

crates/server/src/rpc.rs 🔗

@@ -1716,6 +1716,7 @@ mod tests {
                             group_id: 0,
                             message: "message 1".to_string(),
                             severity: lsp::DiagnosticSeverity::ERROR,
+                            is_primary: true
                         }
                     ),
                     (
@@ -1723,7 +1724,8 @@ mod tests {
                         &Diagnostic {
                             group_id: 1,
                             severity: lsp::DiagnosticSeverity::WARNING,
-                            message: "message 2".to_string()
+                            message: "message 2".to_string(),
+                            is_primary: true
                         }
                     )
                 ]