Color diagnostic messages based on their severity

Antonio Scandurra and Nathan Sobo created

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

Change summary

crates/editor/src/display_map.rs           |  9 +-
crates/editor/src/display_map/block_map.rs | 67 +++++++++++++++++------
crates/editor/src/element.rs               | 12 +---
crates/editor/src/lib.rs                   | 47 ++++++++++++----
crates/theme/src/lib.rs                    | 16 ++--
crates/zed/assets/themes/_base.toml        | 10 +-
6 files changed, 103 insertions(+), 58 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -8,7 +8,7 @@ pub use block_map::{BlockDisposition, BlockId, BlockProperties, BufferRows, Chun
 use block_map::{BlockMap, BlockPoint};
 use buffer::Rope;
 use fold_map::{FoldMap, ToFoldPoint as _};
-use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
+use gpui::{fonts::FontId, AppContext, Entity, ModelContext, ModelHandle};
 use language::{Anchor, Buffer, Point, ToOffset, ToPoint};
 use std::{collections::HashSet, ops::Range};
 use sum_tree::Bias;
@@ -230,7 +230,7 @@ impl DisplayMapSnapshot {
 
     pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         self.blocks_snapshot
-            .chunks(display_row..self.max_point().row() + 1, None)
+            .chunks(display_row..self.max_point().row() + 1, None, None)
             .map(|h| h.text)
     }
 
@@ -238,8 +238,9 @@ impl DisplayMapSnapshot {
         &'a self,
         display_rows: Range<u32>,
         theme: Option<&'a SyntaxTheme>,
+        cx: &'a AppContext,
     ) -> block_map::Chunks<'a> {
-        self.blocks_snapshot.chunks(display_rows, theme)
+        self.blocks_snapshot.chunks(display_rows, theme, Some(cx))
     }
 
     pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
@@ -1025,7 +1026,7 @@ mod tests {
     ) -> Vec<(String, Option<Color>)> {
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         let mut chunks: Vec<(String, Option<Color>)> = Vec::new();
-        for chunk in snapshot.chunks(rows, Some(theme)) {
+        for chunk in snapshot.chunks(rows, Some(theme), cx) {
             let color = chunk.highlight_style.map(|s| s.color);
             if let Some((last_chunk, last_color)) = chunks.last_mut() {
                 if color == *last_color {

crates/editor/src/display_map/block_map.rs 🔗

@@ -6,13 +6,14 @@ use parking_lot::Mutex;
 use std::{
     cmp::{self, Ordering},
     collections::HashSet,
+    fmt::Debug,
     iter,
     ops::Range,
-    slice,
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
     },
+    vec,
 };
 use sum_tree::SumTree;
 use theme::SyntaxTheme;
@@ -44,12 +45,11 @@ struct BlockRow(u32);
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 struct WrapRow(u32);
 
-#[derive(Debug)]
 struct Block {
     id: BlockId,
     position: Anchor,
     text: Rope,
-    runs: Vec<(usize, HighlightStyle)>,
+    build_runs: Option<Arc<dyn Fn(&AppContext) -> Vec<(usize, HighlightStyle)>>>,
     disposition: BlockDisposition,
 }
 
@@ -61,7 +61,7 @@ where
 {
     pub position: P,
     pub text: T,
-    pub runs: Vec<(usize, HighlightStyle)>,
+    pub build_runs: Option<Arc<dyn Fn(&AppContext) -> Vec<(usize, HighlightStyle)>>>,
     pub disposition: BlockDisposition,
 }
 
@@ -92,11 +92,12 @@ pub struct Chunks<'a> {
     block_chunks: Option<BlockChunks<'a>>,
     output_row: u32,
     max_output_row: u32,
+    cx: Option<&'a AppContext>,
 }
 
 struct BlockChunks<'a> {
     chunks: rope::Chunks<'a>,
-    runs: iter::Peekable<slice::Iter<'a, (usize, HighlightStyle)>>,
+    runs: iter::Peekable<vec::IntoIter<(usize, HighlightStyle)>>,
     chunk: Option<&'a str>,
     run_start: usize,
     offset: usize,
@@ -403,7 +404,7 @@ impl<'a> BlockMapWriter<'a> {
                     id,
                     position,
                     text: block.text.into(),
-                    runs: block.runs,
+                    build_runs: block.build_runs,
                     disposition: block.disposition,
                 }),
             );
@@ -460,12 +461,17 @@ impl<'a> BlockMapWriter<'a> {
 impl BlockSnapshot {
     #[cfg(test)]
     fn text(&mut self) -> String {
-        self.chunks(0..self.transforms.summary().output_rows, None)
+        self.chunks(0..self.transforms.summary().output_rows, None, None)
             .map(|chunk| chunk.text)
             .collect()
     }
 
-    pub fn chunks<'a>(&'a self, rows: Range<u32>, theme: Option<&'a SyntaxTheme>) -> Chunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        theme: Option<&'a SyntaxTheme>,
+        cx: Option<&'a AppContext>,
+    ) -> Chunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
         let input_end = {
@@ -499,6 +505,7 @@ impl BlockSnapshot {
             transforms: cursor,
             output_row: rows.start,
             max_output_row,
+            cx,
         }
     }
 
@@ -709,7 +716,11 @@ impl<'a> Iterator for Chunks<'a> {
             let start_in_block = self.output_row - block_start;
             let end_in_block = cmp::min(self.max_output_row, block_end) - block_start;
             self.transforms.next(&());
-            self.block_chunks = Some(BlockChunks::new(block, start_in_block..end_in_block));
+            self.block_chunks = Some(BlockChunks::new(
+                block,
+                start_in_block..end_in_block,
+                self.cx,
+            ));
             return self.next();
         }
 
@@ -748,11 +759,18 @@ impl<'a> Iterator for Chunks<'a> {
 }
 
 impl<'a> BlockChunks<'a> {
-    fn new(block: &'a Block, rows: Range<u32>) -> Self {
+    fn new(block: &'a Block, rows: Range<u32>, cx: Option<&'a AppContext>) -> Self {
         let offset_range = block.text.point_to_offset(Point::new(rows.start, 0))
             ..block.text.point_to_offset(Point::new(rows.end, 0));
 
-        let mut runs = block.runs.iter().peekable();
+        let mut runs = block
+            .build_runs
+            .as_ref()
+            .zip(cx)
+            .map(|(build_runs, cx)| build_runs(cx))
+            .unwrap_or_default()
+            .into_iter()
+            .peekable();
         let mut run_start = 0;
         while let Some((run_len, _)) = runs.peek() {
             let run_end = run_start + run_len;
@@ -874,6 +892,17 @@ impl BlockDisposition {
     }
 }
 
+impl Debug for Block {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Block")
+            .field("id", &self.id)
+            .field("position", &self.position)
+            .field("text", &self.text)
+            .field("disposition", &self.disposition)
+            .finish()
+    }
+}
+
 // Count the number of bytes prior to a target point. If the string doesn't contain the target
 // point, return its total extent. Otherwise return the target point itself.
 fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
@@ -938,19 +967,19 @@ mod tests {
                     position: Point::new(1, 0),
                     text: "BLOCK 1",
                     disposition: BlockDisposition::Above,
-                    runs: vec![],
+                    build_runs: None,
                 },
                 BlockProperties {
                     position: Point::new(1, 2),
                     text: "BLOCK 2",
                     disposition: BlockDisposition::Above,
-                    runs: vec![],
+                    build_runs: None,
                 },
                 BlockProperties {
                     position: Point::new(3, 2),
                     text: "BLOCK 3",
                     disposition: BlockDisposition::Below,
-                    runs: vec![],
+                    build_runs: None,
                 },
             ],
             cx,
@@ -1078,13 +1107,13 @@ mod tests {
                     position: Point::new(1, 12),
                     text: "<BLOCK 1",
                     disposition: BlockDisposition::Above,
-                    runs: vec![],
+                    build_runs: None,
                 },
                 BlockProperties {
                     position: Point::new(1, 1),
                     text: ">BLOCK 2",
                     disposition: BlockDisposition::Below,
-                    runs: vec![],
+                    build_runs: None,
                 },
             ],
             cx,
@@ -1177,7 +1206,7 @@ mod tests {
                             BlockProperties {
                                 position,
                                 text,
-                                runs: Vec::<(usize, HighlightStyle)>::new(),
+                                build_runs: None,
                                 disposition,
                             }
                         })
@@ -1252,7 +1281,7 @@ mod tests {
                         BlockProperties {
                             position: row,
                             text: block.text,
-                            runs: block.runs,
+                            build_runs: block.build_runs.clone(),
                             disposition: block.disposition,
                         },
                     )
@@ -1313,7 +1342,7 @@ mod tests {
             for start_row in 0..expected_row_count {
                 let expected_text = expected_lines[start_row..].join("\n");
                 let actual_text = blocks_snapshot
-                    .chunks(start_row as u32..expected_row_count as u32, None)
+                    .chunks(start_row as u32..expected_row_count as u32, None, None)
                     .map(|chunk| chunk.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/element.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
 use json::json;
-use language::{Chunk, DiagnosticSeverity};
+use language::Chunk;
 use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
@@ -493,7 +493,7 @@ impl EditorElement {
         let mut styles = Vec::new();
         let mut row = rows.start;
         let mut line_exceeded_max_len = false;
-        let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax));
+        let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax), cx);
 
         let newline_chunk = Chunk {
             text: "\n",
@@ -541,13 +541,7 @@ impl EditorElement {
                     }
 
                     let underline = if let Some(severity) = chunk.diagnostic {
-                        match severity {
-                            DiagnosticSeverity::ERROR => Some(style.error_underline),
-                            DiagnosticSeverity::WARNING => Some(style.warning_underline),
-                            DiagnosticSeverity::INFORMATION => Some(style.information_underline),
-                            DiagnosticSeverity::HINT => Some(style.hint_underline),
-                            _ => highlight_style.underline,
-                        }
+                        Some(super::diagnostic_color(severity, style))
                     } else {
                         highlight_style.underline
                     };

crates/editor/src/lib.rs 🔗

@@ -12,6 +12,7 @@ use display_map::*;
 pub use element::*;
 use gpui::{
     action,
+    color::Color,
     geometry::vector::{vec2f, Vector2F},
     keymap::Binding,
     text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
@@ -2250,21 +2251,30 @@ impl Editor {
                     let buffer = self.buffer.read(cx);
                     let diagnostic_group = buffer
                         .diagnostic_group::<Point>(group_id)
-                        .map(|(range, diagnostic)| (range, diagnostic.message.clone()))
+                        .map(|(range, diagnostic)| (range, diagnostic.clone()))
                         .collect::<Vec<_>>();
                     let primary_range = buffer.anchor_after(primary_range.start)
                         ..buffer.anchor_before(primary_range.end);
 
                     let block_ids = display_map
                         .insert_blocks(
-                            diagnostic_group
-                                .iter()
-                                .map(|(range, message)| BlockProperties {
+                            diagnostic_group.iter().map(|(range, diagnostic)| {
+                                let build_settings = self.build_settings.clone();
+                                let message_len = diagnostic.message.len();
+                                let severity = diagnostic.severity;
+                                BlockProperties {
                                     position: range.start,
-                                    text: message.as_str(),
-                                    runs: vec![],
-                                    disposition: BlockDisposition::Above,
-                                }),
+                                    text: diagnostic.message.as_str(),
+                                    build_runs: Some(Arc::new(move |cx| {
+                                        let settings = build_settings.borrow()(cx);
+                                        vec![(
+                                            message_len,
+                                            diagnostic_color(severity, &settings.style).into(),
+                                        )]
+                                    })),
+                                    disposition: BlockDisposition::Below,
+                                }
+                            }),
                             cx,
                         )
                         .into_iter()
@@ -2813,8 +2823,9 @@ impl Snapshot {
         &'a self,
         display_rows: Range<u32>,
         theme: Option<&'a SyntaxTheme>,
+        cx: &'a AppContext,
     ) -> display_map::Chunks<'a> {
-        self.display_snapshot.chunks(display_rows, theme)
+        self.display_snapshot.chunks(display_rows, theme, cx)
     }
 
     pub fn scroll_position(&self) -> Vector2F {
@@ -2882,10 +2893,10 @@ impl EditorSettings {
                     selection: Default::default(),
                     guest_selections: Default::default(),
                     syntax: Default::default(),
-                    error_underline: Default::default(),
-                    warning_underline: Default::default(),
-                    information_underline: Default::default(),
-                    hint_underline: Default::default(),
+                    error_color: Default::default(),
+                    warning_color: Default::default(),
+                    information_color: Default::default(),
+                    hint_color: Default::default(),
                 }
             },
         }
@@ -3009,6 +3020,16 @@ impl SelectionExt for Selection<Point> {
     }
 }
 
+pub fn diagnostic_color(severity: DiagnosticSeverity, style: &EditorStyle) -> Color {
+    match severity {
+        DiagnosticSeverity::ERROR => style.error_color,
+        DiagnosticSeverity::WARNING => style.warning_color,
+        DiagnosticSeverity::INFORMATION => style.information_color,
+        DiagnosticSeverity::HINT => style.hint_color,
+        _ => style.text.color,
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/theme/src/lib.rs 🔗

@@ -227,12 +227,12 @@ pub struct EditorStyle {
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
     pub syntax: Arc<SyntaxTheme>,
-    pub error_underline: Color,
-    pub warning_underline: Color,
+    pub error_color: Color,
+    pub warning_color: Color,
     #[serde(default)]
-    pub information_underline: Color,
+    pub information_color: Color,
     #[serde(default)]
-    pub hint_underline: Color,
+    pub hint_color: Color,
 }
 
 #[derive(Clone, Copy, Default, Deserialize)]
@@ -273,10 +273,10 @@ impl InputEditorStyle {
             line_number_active: Default::default(),
             guest_selections: Default::default(),
             syntax: Default::default(),
-            error_underline: Default::default(),
-            warning_underline: Default::default(),
-            information_underline: Default::default(),
-            hint_underline: Default::default(),
+            error_color: Default::default(),
+            warning_color: Default::default(),
+            information_color: Default::default(),
+            hint_color: Default::default(),
         }
     }
 }

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

@@ -173,7 +173,7 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
+padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"
@@ -235,7 +235,7 @@ line_number = "$text.2.color"
 line_number_active = "$text.0.color"
 selection = "$selection.host"
 guest_selections = "$selection.guests"
-error_underline = "$status.bad"
-warning_underline = "$status.warn"
-info_underline = "$status.info"
-hint_underline = "$status.info"
+error_color = "$status.bad"
+warning_color = "$status.warn"
+info_color = "$status.info"
+hint_color = "$status.info"