Show diagnostics in scrollbar (#7175)

Bennet Bo Fenner created

This PR implements support for displaying diagnostics in the scrollbar,
similar to what is already done for search results, symbols, git diff,
...

For example, changing a field name (`text`) without changing the
references looks like this in `buffer.rs` (note the red lines in the
scrollbar):

![image](https://github.com/zed-industries/zed/assets/53836821/c46f0d55-32e3-4334-8ad7-66d1578d5725)

As you can see, the errors, warnings, ... are displayed in the scroll
bar, which helps to identify possible problems with the current file.

Relevant issues: #4866, #6819

Release Notes:

- Added diagnostic indicators to the scrollbar

Change summary

assets/settings/default.json            |  4 +
crates/editor/src/editor_settings.rs    |  5 ++
crates/editor/src/element.rs            | 62 +++++++++++++++++++++++++++
crates/language/src/buffer.rs           |  5 ++
crates/multi_buffer/src/multi_buffer.rs |  6 ++
5 files changed, 81 insertions(+), 1 deletion(-)

Detailed changes

assets/settings/default.json 🔗

@@ -127,7 +127,9 @@
     // Whether to show selections in the scrollbar.
     "selections": true,
     // Whether to show symbols selections in the scrollbar.
-    "symbols_selections": true
+    "symbols_selections": true,
+    // Whether to show diagnostic indicators in the scrollbar.
+    "diagnostics": true
   },
   "relative_line_numbers": false,
   // When to populate a new search's query based on the text under the cursor.

crates/editor/src/editor_settings.rs 🔗

@@ -34,6 +34,7 @@ pub struct Scrollbar {
     pub git_diff: bool,
     pub selections: bool,
     pub symbols_selections: bool,
+    pub diagnostics: bool,
 }
 
 /// When to show the scrollbar in the editor.
@@ -122,6 +123,10 @@ pub struct ScrollbarContent {
     ///
     /// Default: true
     pub symbols_selections: Option<bool>,
+    /// Whether to show diagnostic indicators in the scrollbar.
+    ///
+    /// Default: true
+    pub diagnostics: Option<bool>,
 }
 
 impl Settings for EditorSettings {

crates/editor/src/element.rs 🔗

@@ -35,6 +35,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
+use lsp::DiagnosticSeverity;
 use multi_buffer::Anchor;
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},
@@ -1477,6 +1478,64 @@ impl EditorElement {
                 }
             }
 
+            if layout.is_singleton && scrollbar_settings.diagnostics {
+                let max_point = layout
+                    .position_map
+                    .snapshot
+                    .display_snapshot
+                    .buffer_snapshot
+                    .max_point();
+
+                let diagnostics = layout
+                    .position_map
+                    .snapshot
+                    .buffer_snapshot
+                    .diagnostics_in_range::<_, Point>(Point::zero()..max_point, false)
+                    // We want to sort by severity, in order to paint the most severe diagnostics last.
+                    .sorted_by_key(|diagnostic| std::cmp::Reverse(diagnostic.diagnostic.severity));
+
+                for diagnostic in diagnostics {
+                    let start_display = diagnostic
+                        .range
+                        .start
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let end_display = diagnostic
+                        .range
+                        .end
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let start_y = y_for_row(start_display.row() as f32);
+                    let mut end_y = if diagnostic.range.start == diagnostic.range.end {
+                        y_for_row((end_display.row() + 1) as f32)
+                    } else {
+                        y_for_row((end_display.row()) as f32)
+                    };
+
+                    if end_y - start_y < px(1.) {
+                        end_y = start_y + px(1.);
+                    }
+                    let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
+
+                    let color = match diagnostic.diagnostic.severity {
+                        DiagnosticSeverity::ERROR => cx.theme().status().error,
+                        DiagnosticSeverity::WARNING => cx.theme().status().warning,
+                        DiagnosticSeverity::INFORMATION => cx.theme().status().info,
+                        _ => cx.theme().status().hint,
+                    };
+                    cx.paint_quad(quad(
+                        bounds,
+                        Corners::default(),
+                        color,
+                        Edges {
+                            top: Pixels::ZERO,
+                            right: px(1.),
+                            bottom: Pixels::ZERO,
+                            left: px(1.),
+                        },
+                        cx.theme().colors().scrollbar_thumb_border,
+                    ));
+                }
+            }
+
             cx.paint_quad(quad(
                 thumb_bounds,
                 Corners::default(),
@@ -2106,6 +2165,9 @@ impl EditorElement {
                     // Symbols Selections
                     (is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
                     ||
+                    // Diagnostics
+                    (is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
+                    ||
                     // Scrollmanager
                     editor.scroll_manager.scrollbars_visible()
                 }

crates/language/src/buffer.rs 🔗

@@ -2993,6 +2993,11 @@ impl BufferSnapshot {
         self.git_diff.hunks_intersecting_range_rev(range, self)
     }
 
+    /// Returns if the buffer contains any diagnostics.
+    pub fn has_diagnostics(&self) -> bool {
+        !self.diagnostics.is_empty()
+    }
+
     /// Returns all the diagnostics intersecting the given range.
     pub fn diagnostics_in_range<'a, T, O>(
         &'a self,

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -3052,6 +3052,12 @@ impl MultiBufferSnapshot {
         self.has_conflict
     }
 
+    pub fn has_diagnostics(&self) -> bool {
+        self.excerpts
+            .iter()
+            .any(|excerpt| excerpt.buffer.has_diagnostics())
+    }
+
     pub fn diagnostic_group<'a, O>(
         &'a self,
         group_id: usize,