Style block decorations

Antonio Scandurra and Nathan Sobo created

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

Change summary

crates/diagnostics/src/diagnostics.rs | 27 +--------
crates/editor/src/editor.rs           | 85 +++++++++++++++++++++-------
crates/editor/src/element.rs          |  7 ++
crates/theme/src/theme.rs             | 24 ++++---
crates/zed/assets/themes/_base.toml   | 40 ++++++++++--
5 files changed, 117 insertions(+), 66 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -5,6 +5,7 @@ use collections::{BTreeSet, HashMap, HashSet};
 use editor::{
     diagnostic_block_renderer,
     display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
+    highlight_diagnostic_message,
     items::BufferItemHandle,
     Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
 };
@@ -703,10 +704,10 @@ fn diagnostic_header_renderer(
         let style = &settings.style.diagnostic_header;
         let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
             Svg::new("icons/diagnostic-error-10.svg")
-                .with_color(settings.style.error_diagnostic.text)
+                .with_color(settings.style.error_diagnostic.message.text.color)
         } else {
             Svg::new("icons/diagnostic-warning-10.svg")
-                .with_color(settings.style.warning_diagnostic.text)
+                .with_color(settings.style.warning_diagnostic.message.text.color)
         };
 
         Flex::row()
@@ -741,28 +742,6 @@ fn diagnostic_header_renderer(
     })
 }
 
-fn highlight_diagnostic_message(message: &str) -> (String, Vec<usize>) {
-    let mut message_without_backticks = String::new();
-    let mut prev_offset = 0;
-    let mut inside_block = false;
-    let mut highlights = Vec::new();
-    for (match_ix, (offset, _)) in message
-        .match_indices('`')
-        .chain([(message.len(), "")])
-        .enumerate()
-    {
-        message_without_backticks.push_str(&message[prev_offset..offset]);
-        if inside_block {
-            highlights.extend(prev_offset - match_ix..offset - match_ix);
-        }
-
-        inside_block = !inside_block;
-        prev_offset = offset + 1;
-    }
-
-    (message_without_backticks, highlights)
-}
-
 fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
     Arc::new(move |cx| {
         let settings = build_settings(cx);

crates/editor/src/editor.rs 🔗

@@ -3825,6 +3825,10 @@ impl EditorSettings {
                     font_properties,
                     underline: None,
                 };
+                let default_diagnostic_style = DiagnosticStyle {
+                    message: text.clone().into(),
+                    header: Default::default(),
+                };
                 EditorStyle {
                     text: text.clone(),
                     placeholder_text: None,
@@ -3860,14 +3864,14 @@ impl EditorSettings {
                         },
                         icon: Default::default(),
                     },
-                    error_diagnostic: Default::default(),
-                    invalid_error_diagnostic: Default::default(),
-                    warning_diagnostic: Default::default(),
-                    invalid_warning_diagnostic: Default::default(),
-                    information_diagnostic: Default::default(),
-                    invalid_information_diagnostic: Default::default(),
-                    hint_diagnostic: Default::default(),
-                    invalid_hint_diagnostic: Default::default(),
+                    error_diagnostic: default_diagnostic_style.clone(),
+                    invalid_error_diagnostic: default_diagnostic_style.clone(),
+                    warning_diagnostic: default_diagnostic_style.clone(),
+                    invalid_warning_diagnostic: default_diagnostic_style.clone(),
+                    information_diagnostic: default_diagnostic_style.clone(),
+                    invalid_information_diagnostic: default_diagnostic_style.clone(),
+                    hint_diagnostic: default_diagnostic_style.clone(),
+                    invalid_hint_diagnostic: default_diagnostic_style.clone(),
                 }
             },
         }
@@ -4008,33 +4012,68 @@ pub fn diagnostic_block_renderer(
     is_valid: bool,
     build_settings: BuildSettings,
 ) -> RenderBlock {
+    let mut highlighted_lines = Vec::new();
+    for line in diagnostic.message.lines() {
+        highlighted_lines.push(highlight_diagnostic_message(line));
+    }
+
     Arc::new(move |cx: &BlockContext| {
         let settings = build_settings(cx);
-        let mut text_style = settings.style.text.clone();
-        text_style.color = diagnostic_style(diagnostic.severity, is_valid, &settings.style).text;
-        Text::new(diagnostic.message.clone(), text_style)
-            .with_soft_wrap(false)
-            .contained()
-            .with_margin_left(cx.anchor_x)
+        let style = diagnostic_style(diagnostic.severity, is_valid, &settings.style).message;
+        Flex::column()
+            .with_children(highlighted_lines.iter().map(|(line, highlights)| {
+                Label::new(line.clone(), style.clone())
+                    .with_highlights(highlights.clone())
+                    .contained()
+                    .with_margin_left(cx.anchor_x)
+                    .boxed()
+            }))
+            .aligned()
+            .left()
             .boxed()
     })
 }
 
+pub fn highlight_diagnostic_message(message: &str) -> (String, Vec<usize>) {
+    let mut message_without_backticks = String::new();
+    let mut prev_offset = 0;
+    let mut inside_block = false;
+    let mut highlights = Vec::new();
+    for (match_ix, (offset, _)) in message
+        .match_indices('`')
+        .chain([(message.len(), "")])
+        .enumerate()
+    {
+        message_without_backticks.push_str(&message[prev_offset..offset]);
+        if inside_block {
+            highlights.extend(prev_offset - match_ix..offset - match_ix);
+        }
+
+        inside_block = !inside_block;
+        prev_offset = offset + 1;
+    }
+
+    (message_without_backticks, highlights)
+}
+
 pub fn diagnostic_style(
     severity: DiagnosticSeverity,
     valid: bool,
     style: &EditorStyle,
 ) -> DiagnosticStyle {
     match (severity, valid) {
-        (DiagnosticSeverity::ERROR, true) => style.error_diagnostic,
-        (DiagnosticSeverity::ERROR, false) => style.invalid_error_diagnostic,
-        (DiagnosticSeverity::WARNING, true) => style.warning_diagnostic,
-        (DiagnosticSeverity::WARNING, false) => style.invalid_warning_diagnostic,
-        (DiagnosticSeverity::INFORMATION, true) => style.information_diagnostic,
-        (DiagnosticSeverity::INFORMATION, false) => style.invalid_information_diagnostic,
-        (DiagnosticSeverity::HINT, true) => style.hint_diagnostic,
-        (DiagnosticSeverity::HINT, false) => style.invalid_hint_diagnostic,
-        _ => Default::default(),
+        (DiagnosticSeverity::ERROR, true) => style.error_diagnostic.clone(),
+        (DiagnosticSeverity::ERROR, false) => style.invalid_error_diagnostic.clone(),
+        (DiagnosticSeverity::WARNING, true) => style.warning_diagnostic.clone(),
+        (DiagnosticSeverity::WARNING, false) => style.invalid_warning_diagnostic.clone(),
+        (DiagnosticSeverity::INFORMATION, true) => style.information_diagnostic.clone(),
+        (DiagnosticSeverity::INFORMATION, false) => style.invalid_information_diagnostic.clone(),
+        (DiagnosticSeverity::HINT, true) => style.hint_diagnostic.clone(),
+        (DiagnosticSeverity::HINT, false) => style.invalid_hint_diagnostic.clone(),
+        _ => DiagnosticStyle {
+            message: style.text.clone().into(),
+            header: Default::default(),
+        },
     }
 }
 

crates/editor/src/element.rs 🔗

@@ -549,7 +549,12 @@ impl EditorElement {
                 .chunks(rows.clone(), Some(&style.syntax))
                 .map(|chunk| {
                     let highlight = if let Some(severity) = chunk.diagnostic {
-                        let underline = Some(super::diagnostic_style(severity, true, style).text);
+                        let underline = Some(
+                            super::diagnostic_style(severity, true, style)
+                                .message
+                                .text
+                                .color,
+                        );
                         if let Some(mut highlight) = chunk.highlight_style {
                             highlight.underline = underline;
                             Some(highlight)

crates/theme/src/theme.rs 🔗

@@ -287,9 +287,9 @@ pub struct DiagnosticHeaderIcon {
     pub width: f32,
 }
 
-#[derive(Copy, Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default)]
 pub struct DiagnosticStyle {
-    pub text: Color,
+    pub message: LabelStyle,
     #[serde(default)]
     pub header: ContainerStyle,
 }
@@ -327,6 +327,10 @@ impl EditorStyle {
 
 impl InputEditorStyle {
     pub fn as_editor(&self) -> EditorStyle {
+        let default_diagnostic_style = DiagnosticStyle {
+            message: self.text.clone().into(),
+            header: Default::default(),
+        };
         EditorStyle {
             text: self.text.clone(),
             placeholder_text: self.placeholder_text.clone(),
@@ -365,14 +369,14 @@ impl InputEditorStyle {
                 },
                 icon: Default::default(),
             },
-            error_diagnostic: Default::default(),
-            invalid_error_diagnostic: Default::default(),
-            warning_diagnostic: Default::default(),
-            invalid_warning_diagnostic: Default::default(),
-            information_diagnostic: Default::default(),
-            invalid_information_diagnostic: Default::default(),
-            hint_diagnostic: Default::default(),
-            invalid_hint_diagnostic: Default::default(),
+            error_diagnostic: default_diagnostic_style.clone(),
+            invalid_error_diagnostic: default_diagnostic_style.clone(),
+            warning_diagnostic: default_diagnostic_style.clone(),
+            invalid_warning_diagnostic: default_diagnostic_style.clone(),
+            information_diagnostic: default_diagnostic_style.clone(),
+            invalid_information_diagnostic: default_diagnostic_style.clone(),
+            hint_diagnostic: default_diagnostic_style.clone(),
+            invalid_hint_diagnostic: default_diagnostic_style.clone(),
         }
     }
 }

crates/zed/assets/themes/_base.toml 🔗

@@ -251,10 +251,6 @@ line_number_active = "$text.0.color"
 selection = "$selection.host"
 guest_selections = "$selection.guests"
 error_color = "$status.bad"
-invalid_error_diagnostic = { text = "$text.3.color" }
-invalid_warning_diagnostic = { text = "$text.3.color" }
-invalid_information_diagnostic = { text = "$text.3.color" }
-invalid_hint_diagnostic = { text = "$text.3.color" }
 
 [editor.diagnostic_path_header]
 filename = { extends = "$text.0", size = 14 }
@@ -271,21 +267,49 @@ text = { extends = "$text.1", size = 14 }
 highlight_text = { extends = "$text.0", size = 14, weight = "bold" }
 
 [editor.error_diagnostic]
-text = "$status.bad"
 header.border = { width = 1, top = true, color = "$border.0" }
 
+[editor.error_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$status.bad" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$status.bad", weight = "bold" }
+
 [editor.warning_diagnostic]
-text = "$status.warn"
 header.border = { width = 1, top = true, color = "$border.0" }
 
+[editor.warning_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$status.warn" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$status.warn", weight = "bold" }
+
 [editor.information_diagnostic]
-text = "$status.info"
 border = { width = 1, top = true, color = "$border.0" }
 
+[editor.information_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$status.info" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$status.info", weight = "bold" }
+
 [editor.hint_diagnostic]
-text = "$status.info"
 border = { width = 1, top = true, color = "$border.0" }
 
+[editor.hint_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$status.info" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$status.info", weight = "bold" }
+
+[editor.invalid_error_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$text.3.color" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$text.3.color", weight = "bold" }
+
+[editor.invalid_warning_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$text.3.color" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$text.3.color", weight = "bold" }
+
+[editor.invalid_information_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$text.3.color" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$text.3.color", weight = "bold" }
+
+[editor.invalid_hint_diagnostic.message]
+text = { extends = "$editor.text", size = 14, color = "$text.3.color" }
+highlight_text = { extends = "$editor.text", size = 14, color = "$text.3.color", weight = "bold" }
+
 [project_diagnostics]
 background = "$surface.1"
 empty_message = "$text.0"