Add fine-grained control for scrollbar diagnostics (#22364)

Aaron Feickert created

This PR updates the scrollbar diagnostic setting to provide fine-grained
control over which indicators to show, based on severity level. This
allows the user to hide lower-severity diagnostics that can otherwise
clutter the scrollbar (for example, unused or disabled code).

The options are set such that the existing boolean setting has the same
effect: when `true` all diagnostics are shown, and when `false` no
diagnostics are shown.

Closes #22296.

Release Notes:

- Added fine-grained control of scrollbar diagnostic indicators.

Change summary

assets/settings/default.json         |  9 ++
crates/editor/src/editor_settings.rs | 80 ++++++++++++++++++++++++++++-
crates/editor/src/element.rs         | 31 ++++++++++-
3 files changed, 111 insertions(+), 9 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -256,8 +256,13 @@
     "search_results": true,
     // Whether to show selected symbol occurrences in the scrollbar.
     "selected_symbol": true,
-    // Whether to show diagnostic indicators in the scrollbar.
-    "diagnostics": true,
+    // Which diagnostic indicator levels to show in the scrollbar:
+    //  - "none" or false: do not show diagnostics
+    //  - "error": show only errors
+    //  - "warning": show only errors and warnings
+    //  - "information": show only errors, warnings, and information
+    //  - "all" or true: show all diagnostics
+    "diagnostics": "all",
     /// Forcefully enable or disable the scrollbar for each axis
     "axes": {
       /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.

crates/editor/src/editor_settings.rs 🔗

@@ -105,7 +105,7 @@ pub struct Scrollbar {
     pub git_diff: bool,
     pub selected_symbol: bool,
     pub search_results: bool,
-    pub diagnostics: bool,
+    pub diagnostics: ScrollbarDiagnostics,
     pub cursors: bool,
     pub axes: ScrollbarAxes,
 }
@@ -150,6 +150,73 @@ pub struct ScrollbarAxes {
     pub vertical: bool,
 }
 
+/// Which diagnostic indicator levels to show in the scrollbar.
+///
+/// Default: all
+#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+pub enum ScrollbarDiagnostics {
+    /// Show all diagnostic levels: hint, information, warnings, error.
+    All,
+    /// Show only the following diagnostic levels: information, warning, error.
+    Information,
+    /// Show only the following diagnostic levels: warning, error.
+    Warning,
+    /// Show only the following diagnostic level: error.
+    Error,
+    /// Do not show diagnostics.
+    None,
+}
+
+impl<'de> Deserialize<'de> for ScrollbarDiagnostics {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        struct Visitor;
+
+        impl<'de> serde::de::Visitor<'de> for Visitor {
+            type Value = ScrollbarDiagnostics;
+
+            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+                write!(
+                    f,
+                    r#"a boolean or one of "all", "information", "warning", "error", "none""#
+                )
+            }
+
+            fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                match b {
+                    false => Ok(ScrollbarDiagnostics::None),
+                    true => Ok(ScrollbarDiagnostics::All),
+                }
+            }
+
+            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                match s {
+                    "all" => Ok(ScrollbarDiagnostics::All),
+                    "information" => Ok(ScrollbarDiagnostics::Information),
+                    "warning" => Ok(ScrollbarDiagnostics::Warning),
+                    "error" => Ok(ScrollbarDiagnostics::Error),
+                    "none" => Ok(ScrollbarDiagnostics::None),
+                    _ => Err(E::unknown_variant(
+                        s,
+                        &["all", "information", "warning", "error", "none"],
+                    )),
+                }
+            }
+        }
+
+        deserializer.deserialize_any(Visitor)
+    }
+}
+
 /// The key to use for adding multiple cursors
 ///
 /// Default: alt
@@ -348,10 +415,15 @@ pub struct ScrollbarContent {
     ///
     /// Default: true
     pub selected_symbol: Option<bool>,
-    /// Whether to show diagnostic indicators in the scrollbar.
+    /// Which diagnostic indicator levels to show in the scrollbar:
+    ///  - "none" or false: do not show diagnostics
+    ///  - "error": show only errors
+    ///  - "warning": show only errors and warnings
+    ///  - "information": show only errors, warnings, and information
+    ///  - "all" or true: show all diagnostics
     ///
-    /// Default: true
-    pub diagnostics: Option<bool>,
+    /// Default: all
+    pub diagnostics: Option<ScrollbarDiagnostics>,
     /// Whether to show cursor positions in the scrollbar.
     ///
     /// Default: true

crates/editor/src/element.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
     },
     editor_settings::{
         CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
-        ShowScrollbar,
+        ScrollbarDiagnostics, ShowScrollbar,
     },
     git::blame::{CommitDetails, GitBlame},
     hover_popover::{
@@ -1228,7 +1228,7 @@ impl EditorElement {
                     (is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
                     ||
                     // Diagnostics
-                    (is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
+                    (is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
                     ||
                     // Cursors out of sight
                     non_visible_cursors
@@ -4726,13 +4726,38 @@ impl EditorElement {
                                 }
                             }
 
-                            if scrollbar_settings.diagnostics {
+                            if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
                                 let diagnostics = snapshot
                                     .buffer_snapshot
                                     .diagnostics_in_range::<_, Point>(
                                         Point::zero()..max_point,
                                         false,
                                     )
+                                    // Don't show diagnostics the user doesn't care about
+                                    .filter(|diagnostic| {
+                                        match (
+                                            scrollbar_settings.diagnostics,
+                                            diagnostic.diagnostic.severity,
+                                        ) {
+                                            (ScrollbarDiagnostics::All, _) => true,
+                                            (
+                                                ScrollbarDiagnostics::Error,
+                                                DiagnosticSeverity::ERROR,
+                                            ) => true,
+                                            (
+                                                ScrollbarDiagnostics::Warning,
+                                                DiagnosticSeverity::ERROR
+                                                | DiagnosticSeverity::WARNING,
+                                            ) => true,
+                                            (
+                                                ScrollbarDiagnostics::Information,
+                                                DiagnosticSeverity::ERROR
+                                                | DiagnosticSeverity::WARNING
+                                                | DiagnosticSeverity::INFORMATION,
+                                            ) => true,
+                                            (_, _) => 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)