Include diagnostic info in HighlightedChunks iterator

Max Brunsfeld and Nathan Sobo created

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

Change summary

crates/editor/src/display_map.rs          |  10 
crates/editor/src/display_map/fold_map.rs |  31 ++--
crates/editor/src/display_map/tab_map.rs  |  40 +++--
crates/editor/src/display_map/wrap_map.rs |  34 ++--
crates/editor/src/element.rs              |  27 +++
crates/editor/src/lib.rs                  |   4 
crates/language/src/lib.rs                | 159 ++++++++++++++++++++----
crates/language/src/tests.rs              |   7 
crates/theme/src/lib.rs                   |  10 +
crates/zed/assets/themes/black.toml       |   4 
crates/zed/assets/themes/dark.toml        |   4 
crates/zed/assets/themes/light.toml       |   6 
12 files changed, 247 insertions(+), 89 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -972,16 +972,16 @@ mod tests {
     ) -> Vec<(String, Option<&'a str>)> {
         let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
-        for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) {
-            let style_name = style_id.name(theme);
+        for chunk in snapshot.highlighted_chunks_for_rows(rows) {
+            let style_name = chunk.highlight_id.name(theme);
             if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
                 if style_name == *last_style_name {
-                    last_chunk.push_str(chunk);
+                    last_chunk.push_str(chunk.text);
                 } else {
-                    chunks.push((chunk.to_string(), style_name));
+                    chunks.push((chunk.text.to_string(), style_name));
                 }
             } else {
-                chunks.push((chunk.to_string(), style_name));
+                chunks.push((chunk.text.to_string(), style_name));
             }
         }
         chunks

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

@@ -1,5 +1,7 @@
 use gpui::{AppContext, ModelHandle};
-use language::{Anchor, AnchorRangeExt, Buffer, HighlightId, Point, TextSummary, ToOffset};
+use language::{
+    Anchor, AnchorRangeExt, Buffer, HighlightId, HighlightedChunk, Point, TextSummary, ToOffset,
+};
 use parking_lot::Mutex;
 use std::{
     cmp::{self, Ordering},
@@ -995,12 +997,12 @@ impl<'a> Iterator for Chunks<'a> {
 pub struct HighlightedChunks<'a> {
     transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
     buffer_chunks: language::HighlightedChunks<'a>,
-    buffer_chunk: Option<(usize, &'a str, HighlightId)>,
+    buffer_chunk: Option<(usize, HighlightedChunk<'a>)>,
     buffer_offset: usize,
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, HighlightId);
+    type Item = HighlightedChunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         let transform = if let Some(item) = self.transform_cursor.item() {
@@ -1022,34 +1024,35 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                 self.transform_cursor.next(&());
             }
 
-            return Some((output_text, HighlightId::default()));
+            return Some(HighlightedChunk {
+                text: output_text,
+                highlight_id: HighlightId::default(),
+                diagnostic: None,
+            });
         }
 
         // Retrieve a chunk from the current location in the buffer.
         if self.buffer_chunk.is_none() {
             let chunk_offset = self.buffer_chunks.offset();
-            self.buffer_chunk = self
-                .buffer_chunks
-                .next()
-                .map(|(chunk, capture_ix)| (chunk_offset, chunk, capture_ix));
+            self.buffer_chunk = self.buffer_chunks.next().map(|chunk| (chunk_offset, chunk));
         }
 
         // Otherwise, take a chunk from the buffer's text.
-        if let Some((chunk_offset, mut chunk, capture_ix)) = self.buffer_chunk {
+        if let Some((chunk_offset, mut chunk)) = self.buffer_chunk {
             let offset_in_chunk = self.buffer_offset - chunk_offset;
-            chunk = &chunk[offset_in_chunk..];
+            chunk.text = &chunk.text[offset_in_chunk..];
 
             // Truncate the chunk so that it ends at the next fold.
             let region_end = self.transform_cursor.end(&()).1 - self.buffer_offset;
-            if chunk.len() >= region_end {
-                chunk = &chunk[0..region_end];
+            if chunk.text.len() >= region_end {
+                chunk.text = &chunk.text[0..region_end];
                 self.transform_cursor.next(&());
             } else {
                 self.buffer_chunk.take();
             }
 
-            self.buffer_offset += chunk.len();
-            return Some((chunk, capture_ix));
+            self.buffer_offset += chunk.text.len();
+            return Some(chunk);
         }
 
         None

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

@@ -1,5 +1,5 @@
 use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
-use language::{rope, HighlightId};
+use language::{rope, HighlightedChunk};
 use parking_lot::Mutex;
 use std::{mem, ops::Range};
 use sum_tree::Bias;
@@ -173,9 +173,11 @@ impl Snapshot {
                 .highlighted_chunks(input_start..input_end),
             column: expanded_char_column,
             tab_size: self.tab_size,
-            chunk: &SPACES[0..to_next_stop],
+            chunk: HighlightedChunk {
+                text: &SPACES[0..to_next_stop],
+                ..Default::default()
+            },
             skip_leading_tab: to_next_stop > 0,
-            style_id: Default::default(),
         }
     }
 
@@ -415,23 +417,21 @@ impl<'a> Iterator for Chunks<'a> {
 
 pub struct HighlightedChunks<'a> {
     fold_chunks: fold_map::HighlightedChunks<'a>,
-    chunk: &'a str,
-    style_id: HighlightId,
+    chunk: HighlightedChunk<'a>,
     column: usize,
     tab_size: usize,
     skip_leading_tab: bool,
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, HighlightId);
+    type Item = HighlightedChunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        if self.chunk.is_empty() {
-            if let Some((chunk, style_id)) = self.fold_chunks.next() {
+        if self.chunk.text.is_empty() {
+            if let Some(chunk) = self.fold_chunks.next() {
                 self.chunk = chunk;
-                self.style_id = style_id;
                 if self.skip_leading_tab {
-                    self.chunk = &self.chunk[1..];
+                    self.chunk.text = &self.chunk.text[1..];
                     self.skip_leading_tab = false;
                 }
             } else {
@@ -439,18 +439,24 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             }
         }
 
-        for (ix, c) in self.chunk.char_indices() {
+        for (ix, c) in self.chunk.text.char_indices() {
             match c {
                 '\t' => {
                     if ix > 0 {
-                        let (prefix, suffix) = self.chunk.split_at(ix);
-                        self.chunk = suffix;
-                        return Some((prefix, self.style_id));
+                        let (prefix, suffix) = self.chunk.text.split_at(ix);
+                        self.chunk.text = suffix;
+                        return Some(HighlightedChunk {
+                            text: prefix,
+                            ..self.chunk
+                        });
                     } else {
-                        self.chunk = &self.chunk[1..];
+                        self.chunk.text = &self.chunk.text[1..];
                         let len = self.tab_size - self.column % self.tab_size;
                         self.column += len;
-                        return Some((&SPACES[0..len], self.style_id));
+                        return Some(HighlightedChunk {
+                            text: &SPACES[0..len],
+                            ..self.chunk
+                        });
                     }
                 }
                 '\n' => self.column = 0,
@@ -458,7 +464,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             }
         }
 
-        Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
+        Some(mem::take(&mut self.chunk))
     }
 }
 

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

@@ -3,7 +3,7 @@ use super::{
     tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
 };
 use gpui::{fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, Task};
-use language::{HighlightId, Point};
+use language::{HighlightedChunk, Point};
 use lazy_static::lazy_static;
 use smol::future::yield_now;
 use std::{collections::VecDeque, ops::Range, time::Duration};
@@ -52,8 +52,7 @@ pub struct Chunks<'a> {
 
 pub struct HighlightedChunks<'a> {
     input_chunks: tab_map::HighlightedChunks<'a>,
-    input_chunk: &'a str,
-    style_id: HighlightId,
+    input_chunk: HighlightedChunk<'a>,
     output_position: WrapPoint,
     max_output_row: u32,
     transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
@@ -490,8 +489,7 @@ impl Snapshot {
             .min(self.tab_snapshot.max_point());
         HighlightedChunks {
             input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end),
-            input_chunk: "",
-            style_id: HighlightId::default(),
+            input_chunk: Default::default(),
             output_position: output_start,
             max_output_row: rows.end,
             transforms,
@@ -674,7 +672,7 @@ impl<'a> Iterator for Chunks<'a> {
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, HighlightId);
+    type Item = HighlightedChunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.output_position.row() >= self.max_output_row {
@@ -699,18 +697,19 @@ impl<'a> Iterator for HighlightedChunks<'a> {
 
             self.output_position.0 += summary;
             self.transforms.next(&());
-            return Some((&display_text[start_ix..end_ix], self.style_id));
+            return Some(HighlightedChunk {
+                text: &display_text[start_ix..end_ix],
+                ..self.input_chunk
+            });
         }
 
-        if self.input_chunk.is_empty() {
-            let (chunk, style_id) = self.input_chunks.next().unwrap();
-            self.input_chunk = chunk;
-            self.style_id = style_id;
+        if self.input_chunk.text.is_empty() {
+            self.input_chunk = self.input_chunks.next().unwrap();
         }
 
         let mut input_len = 0;
         let transform_end = self.transforms.end(&()).0;
-        for c in self.input_chunk.chars() {
+        for c in self.input_chunk.text.chars() {
             let char_len = c.len_utf8();
             input_len += char_len;
             if c == '\n' {
@@ -726,9 +725,12 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             }
         }
 
-        let (prefix, suffix) = self.input_chunk.split_at(input_len);
-        self.input_chunk = suffix;
-        Some((prefix, self.style_id))
+        let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
+        self.input_chunk.text = suffix;
+        Some(HighlightedChunk {
+            text: prefix,
+            ..self.input_chunk
+        })
     }
 }
 
@@ -1090,7 +1092,7 @@ mod tests {
 
                 let actual_text = self
                     .highlighted_chunks_for_rows(start_row..end_row)
-                    .map(|c| c.0)
+                    .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(
                     expected_text,

crates/editor/src/element.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
 use json::json;
-use language::HighlightId;
+use language::{DiagnosticSeverity, HighlightedChunk};
 use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
@@ -495,8 +495,12 @@ impl EditorElement {
         let mut line_exceeded_max_len = false;
         let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
 
-        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
-            for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
+        let newline_chunk = HighlightedChunk {
+            text: "\n",
+            ..Default::default()
+        };
+        'outer: for chunk in chunks.chain([newline_chunk]) {
+            for (ix, mut line_chunk) in chunk.text.split('\n').enumerate() {
                 if ix > 0 {
                     layouts.push(cx.text_layout_cache.layout_str(
                         &line,
@@ -513,7 +517,8 @@ impl EditorElement {
                 }
 
                 if !line_chunk.is_empty() && !line_exceeded_max_len {
-                    let highlight_style = style_ix
+                    let highlight_style = chunk
+                        .highlight_id
                         .style(&style.syntax)
                         .unwrap_or(style.text.clone().into());
                     // Avoid a lookup if the font properties match the previous ones.
@@ -537,13 +542,25 @@ impl EditorElement {
                         line_exceeded_max_len = true;
                     }
 
+                    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,
+                        }
+                    } else {
+                        highlight_style.underline
+                    };
+
                     line.push_str(line_chunk);
                     styles.push((
                         line_chunk.len(),
                         RunStyle {
                             font_id,
                             color: highlight_style.color,
-                            underline: highlight_style.underline,
+                            underline,
                         },
                     ));
                     prev_font_id = font_id;

crates/editor/src/lib.rs 🔗

@@ -2774,6 +2774,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(),
                 }
             },
         }

crates/language/src/lib.rs 🔗

@@ -13,7 +13,7 @@ use clock::ReplicaId;
 use futures::FutureExt as _;
 use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
 use lazy_static::lazy_static;
-use lsp::{DiagnosticSeverity, LanguageServer};
+use lsp::LanguageServer;
 use parking_lot::Mutex;
 use postage::{prelude::Stream, sink::Sink, watch};
 use rpc::proto;
@@ -26,16 +26,19 @@ use std::{
     collections::{BTreeMap, HashMap, HashSet},
     ffi::OsString,
     future::Future,
-    iter::Iterator,
+    iter::{Iterator, Peekable},
     ops::{Deref, DerefMut, Range},
     path::{Path, PathBuf},
     str,
     sync::Arc,
     time::{Duration, Instant, SystemTime, UNIX_EPOCH},
+    vec,
 };
 use tree_sitter::{InputEdit, Parser, QueryCursor, Tree};
 use util::{post_inc, TryFutureExt as _};
 
+pub use lsp::DiagnosticSeverity;
+
 thread_local! {
     static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
 }
@@ -68,6 +71,7 @@ pub struct Buffer {
 pub struct Snapshot {
     text: buffer::Snapshot,
     tree: Option<Tree>,
+    diagnostics: AnchorRangeMultimap<(DiagnosticSeverity, String)>,
     is_parsing: bool,
     language: Option<Arc<Language>>,
     query_cursor: QueryCursorHandle,
@@ -182,15 +186,34 @@ struct Highlights<'a> {
 pub struct HighlightedChunks<'a> {
     range: Range<usize>,
     chunks: Chunks<'a>,
+    diagnostic_endpoints: Peekable<vec::IntoIter<DiagnosticEndpoint>>,
+    error_depth: usize,
+    warning_depth: usize,
+    information_depth: usize,
+    hint_depth: usize,
     highlights: Option<Highlights<'a>>,
 }
 
+#[derive(Clone, Copy, Debug, Default)]
+pub struct HighlightedChunk<'a> {
+    pub text: &'a str,
+    pub highlight_id: HighlightId,
+    pub diagnostic: Option<DiagnosticSeverity>,
+}
+
 struct Diff {
     base_version: clock::Global,
     new_text: Arc<str>,
     changes: Vec<(ChangeTag, usize)>,
 }
 
+#[derive(Clone, Copy)]
+struct DiagnosticEndpoint {
+    offset: usize,
+    is_start: bool,
+    severity: DiagnosticSeverity,
+}
+
 impl Buffer {
     pub fn new<T: Into<Arc<str>>>(
         replica_id: ReplicaId,
@@ -275,6 +298,7 @@ impl Buffer {
         Snapshot {
             text: self.text.snapshot(),
             tree: self.syntax_tree(),
+            diagnostics: self.diagnostics.clone(),
             is_parsing: self.parsing_in_background,
             language: self.language.clone(),
             query_cursor: QueryCursorHandle::new(),
@@ -673,7 +697,7 @@ impl Buffer {
         let content = self.content();
         let range = range.start.to_offset(&content)..range.end.to_offset(&content);
         self.diagnostics
-            .intersecting_point_ranges(range, content, true)
+            .intersecting_ranges(range, content, true)
             .map(move |(_, range, (severity, message))| Diagnostic {
                 range,
                 severity: *severity,
@@ -1021,7 +1045,9 @@ impl Buffer {
         let abs_path = self
             .file
             .as_ref()
-            .map_or(PathBuf::new(), |file| file.abs_path(cx).unwrap());
+            .map_or(Path::new("/").to_path_buf(), |file| {
+                file.abs_path(cx).unwrap()
+            });
 
         let version = post_inc(&mut language_server.next_version);
         let snapshot = LanguageServerSnapshot {
@@ -1462,30 +1488,54 @@ impl Snapshot {
         range: Range<T>,
     ) -> HighlightedChunks {
         let range = range.start.to_offset(&*self)..range.end.to_offset(&*self);
+
+        let mut diagnostic_endpoints = Vec::<DiagnosticEndpoint>::new();
+        for (_, range, (severity, _)) in
+            self.diagnostics
+                .intersecting_ranges(range.clone(), self.content(), true)
+        {
+            diagnostic_endpoints.push(DiagnosticEndpoint {
+                offset: range.start,
+                is_start: true,
+                severity: *severity,
+            });
+            diagnostic_endpoints.push(DiagnosticEndpoint {
+                offset: range.end,
+                is_start: false,
+                severity: *severity,
+            });
+        }
+        diagnostic_endpoints.sort_unstable_by_key(|endpoint| endpoint.offset);
+        let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable();
+
         let chunks = self.text.as_rope().chunks_in_range(range.clone());
-        if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
-            let captures = self.query_cursor.set_byte_range(range.clone()).captures(
-                &language.highlights_query,
-                tree.root_node(),
-                TextProvider(self.text.as_rope()),
-            );
+        let highlights =
+            if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
+                let captures = self.query_cursor.set_byte_range(range.clone()).captures(
+                    &language.highlights_query,
+                    tree.root_node(),
+                    TextProvider(self.text.as_rope()),
+                );
 
-            HighlightedChunks {
-                range,
-                chunks,
-                highlights: Some(Highlights {
+                Some(Highlights {
                     captures,
                     next_capture: None,
                     stack: Default::default(),
                     highlight_map: language.highlight_map(),
-                }),
-            }
-        } else {
-            HighlightedChunks {
-                range,
-                chunks,
-                highlights: None,
-            }
+                })
+            } else {
+                None
+            };
+
+        HighlightedChunks {
+            range,
+            chunks,
+            diagnostic_endpoints,
+            error_depth: 0,
+            warning_depth: 0,
+            information_depth: 0,
+            hint_depth: 0,
+            highlights,
         }
     }
 }
@@ -1495,6 +1545,7 @@ impl Clone for Snapshot {
         Self {
             text: self.text.clone(),
             tree: self.tree.clone(),
+            diagnostics: self.diagnostics.clone(),
             is_parsing: self.is_parsing,
             language: self.language.clone(),
             query_cursor: QueryCursorHandle::new(),
@@ -1556,13 +1607,43 @@ impl<'a> HighlightedChunks<'a> {
     pub fn offset(&self) -> usize {
         self.range.start
     }
+
+    fn update_diagnostic_depths(&mut self, endpoint: DiagnosticEndpoint) {
+        let depth = match endpoint.severity {
+            DiagnosticSeverity::ERROR => &mut self.error_depth,
+            DiagnosticSeverity::WARNING => &mut self.warning_depth,
+            DiagnosticSeverity::INFORMATION => &mut self.information_depth,
+            DiagnosticSeverity::HINT => &mut self.hint_depth,
+            _ => return,
+        };
+        if endpoint.is_start {
+            *depth += 1;
+        } else {
+            *depth -= 1;
+        }
+    }
+
+    fn current_diagnostic_severity(&mut self) -> Option<DiagnosticSeverity> {
+        if self.error_depth > 0 {
+            Some(DiagnosticSeverity::ERROR)
+        } else if self.warning_depth > 0 {
+            Some(DiagnosticSeverity::WARNING)
+        } else if self.information_depth > 0 {
+            Some(DiagnosticSeverity::INFORMATION)
+        } else if self.hint_depth > 0 {
+            Some(DiagnosticSeverity::HINT)
+        } else {
+            None
+        }
+    }
 }
 
 impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, HighlightId);
+    type Item = HighlightedChunk<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         let mut next_capture_start = usize::MAX;
+        let mut next_diagnostic_endpoint = usize::MAX;
 
         if let Some(highlights) = self.highlights.as_mut() {
             while let Some((parent_capture_end, _)) = highlights.stack.last() {
@@ -1583,22 +1664,36 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                     next_capture_start = capture.node.start_byte();
                     break;
                 } else {
-                    let style_id = highlights.highlight_map.get(capture.index);
-                    highlights.stack.push((capture.node.end_byte(), style_id));
+                    let highlight_id = highlights.highlight_map.get(capture.index);
+                    highlights
+                        .stack
+                        .push((capture.node.end_byte(), highlight_id));
                     highlights.next_capture = highlights.captures.next();
                 }
             }
         }
 
+        while let Some(endpoint) = self.diagnostic_endpoints.peek().copied() {
+            if endpoint.offset <= self.range.start {
+                self.update_diagnostic_depths(endpoint);
+                self.diagnostic_endpoints.next();
+            } else {
+                next_diagnostic_endpoint = endpoint.offset;
+                break;
+            }
+        }
+
         if let Some(chunk) = self.chunks.peek() {
             let chunk_start = self.range.start;
-            let mut chunk_end = (self.chunks.offset() + chunk.len()).min(next_capture_start);
-            let mut style_id = HighlightId::default();
-            if let Some((parent_capture_end, parent_style_id)) =
+            let mut chunk_end = (self.chunks.offset() + chunk.len())
+                .min(next_capture_start)
+                .min(next_diagnostic_endpoint);
+            let mut highlight_id = HighlightId::default();
+            if let Some((parent_capture_end, parent_highlight_id)) =
                 self.highlights.as_ref().and_then(|h| h.stack.last())
             {
                 chunk_end = chunk_end.min(*parent_capture_end);
-                style_id = *parent_style_id;
+                highlight_id = *parent_highlight_id;
             }
 
             let slice =
@@ -1608,7 +1703,11 @@ impl<'a> Iterator for HighlightedChunks<'a> {
                 self.chunks.next().unwrap();
             }
 
-            Some((slice, style_id))
+            Some(HighlightedChunk {
+                text: slice,
+                highlight_id,
+                diagnostic: self.current_diagnostic_severity(),
+            })
         } else {
             None
         }

crates/language/src/tests.rs 🔗

@@ -475,7 +475,12 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
                     message: "undefined variable 'CCC'".to_string()
                 }
             ]
-        )
+        );
+
+        dbg!(buffer
+            .snapshot()
+            .highlighted_text_for_range(0..buffer.len())
+            .collect::<Vec<_>>());
     });
 }
 

crates/theme/src/lib.rs 🔗

@@ -214,6 +214,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,
+    #[serde(default)]
+    pub information_underline: Color,
+    #[serde(default)]
+    pub hint_underline: Color,
 }
 
 #[derive(Clone, Copy, Default, Deserialize)]
@@ -254,6 +260,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(),
         }
     }
 }

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

@@ -39,6 +39,10 @@ bad = "#b7372e"
 active_line = "#00000033"
 hover = "#00000033"
 
+[editor]
+error_underline = "#ff0000"
+warning_underline = "#00ffff"
+
 [editor.syntax]
 keyword = { color = "#0086c0", weight = "bold" }
 function = "#dcdcaa"

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

@@ -39,6 +39,10 @@ bad = "#b7372e"
 active_line = "#00000022"
 hover = "#00000033"
 
+[editor]
+error_underline = "#ff0000"
+warning_underline = "#00ffff"
+
 [editor.syntax]
 keyword = { color = "#0086c0", weight = "bold" }
 function = "#dcdcaa"

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

@@ -26,7 +26,7 @@ guests = [
   { selection = "#EE823133", cursor = "#EE8231" },
   { selection = "#5A2B9233", cursor = "#5A2B92" },
   { selection = "#FDF35133", cursor = "#FDF351" },
-  { selection = "#4EACAD33", cursor = "#4EACAD" }
+  { selection = "#4EACAD33", cursor = "#4EACAD" },
 ]
 
 [status]
@@ -39,6 +39,10 @@ bad = "#b7372e"
 active_line = "#00000008"
 hover = "#0000000D"
 
+[editor]
+error_underline = "#ff0000"
+warning_underline = "#00ffff"
+
 [editor.syntax]
 keyword = { color = "#0000fa", weight = "bold" }
 function = "#795e26"