debugger: Process ANSI color escape codes in console (#32817)

Cole Miller created

- [x] foreground highlights
- [x] background highlights
- [x] advertise support in DAP capabilities

Closes #31372

Release Notes:

- Debugger Beta: added basic support for highlighting in the console
based on ANSI escape codes.

Change summary

Cargo.lock                                         |   1 
crates/agent/src/inline_assistant.rs               |  17 
crates/collab_ui/src/chat_panel/message_editor.rs  |  21 
crates/debugger_ui/Cargo.toml                      |   1 
crates/debugger_ui/src/session/running/console.rs  | 264 +++++++++++++++
crates/debugger_ui/src/tests/console.rs            |   6 
crates/editor/src/display_map.rs                   |  48 +-
crates/editor/src/display_map/custom_highlights.rs |  31 -
crates/editor/src/display_map/inlay_map.rs         |  20 
crates/editor/src/editor.rs                        | 257 +++++++++------
crates/editor/src/editor_tests.rs                  |  14 
crates/editor/src/element.rs                       |  32 +
crates/editor/src/highlight_matching_bracket.rs    |   2 
crates/editor/src/hover_links.rs                   |   5 
crates/editor/src/hover_popover.rs                 |   2 
crates/editor/src/items.rs                         |  29 +
crates/editor/src/test/editor_test_context.rs      |  14 
crates/language_tools/src/syntax_tree_view.rs      |   2 
crates/project/src/debugger/dap_command.rs         |   2 
crates/search/src/project_search.rs                |   2 
crates/vim/src/normal/yank.rs                      |   2 
crates/vim/src/replace.rs                          |   6 
crates/vim/src/test.rs                             |  14 
23 files changed, 558 insertions(+), 234 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -4268,6 +4268,7 @@ dependencies = [
 name = "debugger_ui"
 version = "0.1.0"
 dependencies = [
+ "alacritty_terminal",
  "anyhow",
  "client",
  "collections",

crates/agent/src/inline_assistant.rs πŸ”—

@@ -1350,11 +1350,18 @@ impl InlineAssistant {
                 editor.clear_highlights::<InlineAssist>(cx);
             } else {
                 editor.highlight_text::<InlineAssist>(
-                    foreground_ranges,
-                    HighlightStyle {
-                        fade_out: Some(0.6),
-                        ..Default::default()
-                    },
+                    foreground_ranges
+                        .into_iter()
+                        .map(|range| {
+                            (
+                                range,
+                                HighlightStyle {
+                                    fade_out: Some(0.6),
+                                    ..Default::default()
+                                },
+                            )
+                        })
+                        .collect(),
                     cx,
                 );
             }

crates/collab_ui/src/chat_panel/message_editor.rs πŸ”—

@@ -193,10 +193,10 @@ impl MessageEditor {
             let highlights = editor.text_highlights::<Self>(cx);
             let text = editor.text(cx);
             let snapshot = editor.buffer().read(cx).snapshot(cx);
-            let mentions = if let Some((_, ranges)) = highlights {
+            let mentions = if let Some(ranges) = highlights {
                 ranges
                     .iter()
-                    .map(|range| range.to_offset(&snapshot))
+                    .map(|(range, _)| range.to_offset(&snapshot))
                     .zip(self.mentions.iter().copied())
                     .collect()
             } else {
@@ -483,20 +483,19 @@ impl MessageEditor {
                             let end = multi_buffer.anchor_after(range.end);
 
                             mentioned_user_ids.push(user.id);
-                            anchor_ranges.push(start..end);
+                            anchor_ranges.push((
+                                start..end,
+                                HighlightStyle {
+                                    font_weight: Some(FontWeight::BOLD),
+                                    ..Default::default()
+                                },
+                            ));
                         }
                     }
                 }
 
                 editor.clear_highlights::<Self>(cx);
-                editor.highlight_text::<Self>(
-                    anchor_ranges,
-                    HighlightStyle {
-                        font_weight: Some(FontWeight::BOLD),
-                        ..Default::default()
-                    },
-                    cx,
-                )
+                editor.highlight_text::<Self>(anchor_ranges, cx)
             });
 
             this.mentions = mentioned_user_ids;

crates/debugger_ui/Cargo.toml πŸ”—

@@ -26,6 +26,7 @@ test-support = [
 ]
 
 [dependencies]
+alacritty_terminal.workspace = true
 anyhow.workspace = true
 client.workspace = true
 collections.workspace = true

crates/debugger_ui/src/session/running/console.rs πŸ”—

@@ -2,13 +2,17 @@ use super::{
     stack_frame_list::{StackFrameList, StackFrameListEvent},
     variable_list::VariableList,
 };
+use alacritty_terminal::vte::ansi;
 use anyhow::Result;
 use collections::HashMap;
 use dap::OutputEvent;
-use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
+use editor::{
+    BackgroundHighlight, Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId,
+};
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    Context, Entity, FocusHandle, Focusable, Render, Subscription, Task, TextStyle, WeakEntity,
+    Context, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task,
+    TextStyle, WeakEntity,
 };
 use language::{Buffer, CodeLabel, ToOffset};
 use menu::Confirm;
@@ -17,8 +21,8 @@ use project::{
     debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
 };
 use settings::Settings;
-use std::{cell::RefCell, rc::Rc, usize};
-use theme::ThemeSettings;
+use std::{cell::RefCell, ops::Range, rc::Rc, usize};
+use theme::{Theme, ThemeSettings};
 use ui::{Divider, prelude::*};
 
 pub struct Console {
@@ -30,6 +34,8 @@ pub struct Console {
     stack_frame_list: Entity<StackFrameList>,
     last_token: OutputToken,
     update_output_task: Task<()>,
+    ansi_handler: ConsoleHandler,
+    ansi_processor: ansi::Processor<ansi::StdSyncHandler>,
     focus_handle: FocusHandle,
 }
 
@@ -100,6 +106,8 @@ impl Console {
             stack_frame_list,
             update_output_task: Task::ready(()),
             last_token: OutputToken(0),
+            ansi_handler: Default::default(),
+            ansi_processor: Default::default(),
             focus_handle,
         }
     }
@@ -135,17 +143,185 @@ impl Console {
         window: &mut Window,
         cx: &mut App,
     ) {
-        self.console.update(cx, |console, cx| {
-            let mut to_insert = String::default();
-            for event in events {
-                use std::fmt::Write;
+        let mut to_insert = String::default();
+        for event in events {
+            use std::fmt::Write;
 
-                _ = write!(to_insert, "{}\n", event.output.trim_end());
-            }
+            _ = write!(to_insert, "{}\n", event.output.trim_end());
+        }
+
+        let len = self.ansi_handler.pos;
+        self.ansi_processor
+            .advance(&mut self.ansi_handler, to_insert.as_bytes());
+        let output = std::mem::take(&mut self.ansi_handler.output);
+        let mut spans = std::mem::take(&mut self.ansi_handler.spans);
+        let mut background_spans = std::mem::take(&mut self.ansi_handler.background_spans);
+        if self.ansi_handler.current_range_start < len + output.len() {
+            spans.push((
+                self.ansi_handler.current_range_start..len + output.len(),
+                self.ansi_handler.current_color,
+            ));
+            self.ansi_handler.current_range_start = len + output.len();
+        }
+        if self.ansi_handler.current_background_range_start < len + output.len() {
+            background_spans.push((
+                self.ansi_handler.current_background_range_start..len + output.len(),
+                self.ansi_handler.current_background_color,
+            ));
+            self.ansi_handler.current_background_range_start = len + output.len();
+        }
+
+        self.console.update(cx, |console, cx| {
+            struct ConsoleAnsiHighlight;
 
             console.set_read_only(false);
             console.move_to_end(&editor::actions::MoveToEnd, window, cx);
-            console.insert(&to_insert, window, cx);
+            console.insert(&output, window, cx);
+            let buffer = console.buffer().read(cx).snapshot(cx);
+
+            let mut highlights = console
+                .remove_text_highlights::<ConsoleAnsiHighlight>(cx)
+                .unwrap_or_default();
+            for (range, color) in spans {
+                let Some(color) = color else { continue };
+                let start = range.start + len;
+                let range = start..range.end + len;
+                let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
+                let style = HighlightStyle {
+                    color: Some(terminal_view::terminal_element::convert_color(
+                        &color,
+                        cx.theme(),
+                    )),
+                    ..Default::default()
+                };
+                highlights.push((range, style));
+            }
+            console.highlight_text::<ConsoleAnsiHighlight>(highlights, cx);
+
+            let mut background_highlights = console
+                .clear_background_highlights::<ConsoleAnsiHighlight>(cx)
+                .unwrap_or_default();
+            for (range, color) in background_spans {
+                let Some(color) = color else { continue };
+                let start = range.start + len;
+                let range = start..range.end + len;
+                let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
+
+                let color_fetcher: fn(&Theme) -> Hsla = match color {
+                    // Named and theme defined colors
+                    ansi::Color::Named(n) => match n {
+                        ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
+                        ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
+                        ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
+                        ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
+                        ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
+                        ansi::NamedColor::Magenta => |theme| theme.colors().terminal_ansi_magenta,
+                        ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
+                        ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
+                        ansi::NamedColor::BrightBlack => {
+                            |theme| theme.colors().terminal_ansi_bright_black
+                        }
+                        ansi::NamedColor::BrightRed => {
+                            |theme| theme.colors().terminal_ansi_bright_red
+                        }
+                        ansi::NamedColor::BrightGreen => {
+                            |theme| theme.colors().terminal_ansi_bright_green
+                        }
+                        ansi::NamedColor::BrightYellow => {
+                            |theme| theme.colors().terminal_ansi_bright_yellow
+                        }
+                        ansi::NamedColor::BrightBlue => {
+                            |theme| theme.colors().terminal_ansi_bright_blue
+                        }
+                        ansi::NamedColor::BrightMagenta => {
+                            |theme| theme.colors().terminal_ansi_bright_magenta
+                        }
+                        ansi::NamedColor::BrightCyan => {
+                            |theme| theme.colors().terminal_ansi_bright_cyan
+                        }
+                        ansi::NamedColor::BrightWhite => {
+                            |theme| theme.colors().terminal_ansi_bright_white
+                        }
+                        ansi::NamedColor::Foreground => |theme| theme.colors().terminal_foreground,
+                        ansi::NamedColor::Background => |theme| theme.colors().terminal_background,
+                        ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
+                        ansi::NamedColor::DimBlack => {
+                            |theme| theme.colors().terminal_ansi_dim_black
+                        }
+                        ansi::NamedColor::DimRed => |theme| theme.colors().terminal_ansi_dim_red,
+                        ansi::NamedColor::DimGreen => {
+                            |theme| theme.colors().terminal_ansi_dim_green
+                        }
+                        ansi::NamedColor::DimYellow => {
+                            |theme| theme.colors().terminal_ansi_dim_yellow
+                        }
+                        ansi::NamedColor::DimBlue => |theme| theme.colors().terminal_ansi_dim_blue,
+                        ansi::NamedColor::DimMagenta => {
+                            |theme| theme.colors().terminal_ansi_dim_magenta
+                        }
+                        ansi::NamedColor::DimCyan => |theme| theme.colors().terminal_ansi_dim_cyan,
+                        ansi::NamedColor::DimWhite => {
+                            |theme| theme.colors().terminal_ansi_dim_white
+                        }
+                        ansi::NamedColor::BrightForeground => {
+                            |theme| theme.colors().terminal_bright_foreground
+                        }
+                        ansi::NamedColor::DimForeground => {
+                            |theme| theme.colors().terminal_dim_foreground
+                        }
+                    },
+                    // 'True' colors
+                    ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
+                    // 8 bit, indexed colors
+                    ansi::Color::Indexed(i) => {
+                        match i {
+                            // 0-15 are the same as the named colors above
+                            0 => |theme| theme.colors().terminal_ansi_black,
+                            1 => |theme| theme.colors().terminal_ansi_red,
+                            2 => |theme| theme.colors().terminal_ansi_green,
+                            3 => |theme| theme.colors().terminal_ansi_yellow,
+                            4 => |theme| theme.colors().terminal_ansi_blue,
+                            5 => |theme| theme.colors().terminal_ansi_magenta,
+                            6 => |theme| theme.colors().terminal_ansi_cyan,
+                            7 => |theme| theme.colors().terminal_ansi_white,
+                            8 => |theme| theme.colors().terminal_ansi_bright_black,
+                            9 => |theme| theme.colors().terminal_ansi_bright_red,
+                            10 => |theme| theme.colors().terminal_ansi_bright_green,
+                            11 => |theme| theme.colors().terminal_ansi_bright_yellow,
+                            12 => |theme| theme.colors().terminal_ansi_bright_blue,
+                            13 => |theme| theme.colors().terminal_ansi_bright_magenta,
+                            14 => |theme| theme.colors().terminal_ansi_bright_cyan,
+                            15 => |theme| theme.colors().terminal_ansi_bright_white,
+                            // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
+                            // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
+                            // 16..=231 => {
+                            //     let (r, g, b) = rgb_for_index(index as u8);
+                            //     rgba_color(
+                            //         if r == 0 { 0 } else { r * 40 + 55 },
+                            //         if g == 0 { 0 } else { g * 40 + 55 },
+                            //         if b == 0 { 0 } else { b * 40 + 55 },
+                            //     )
+                            // }
+                            // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
+                            // 232..=255 => {
+                            //     let i = index as u8 - 232; // Align index to 0..24
+                            //     let value = i * 10 + 8;
+                            //     rgba_color(value, value, value)
+                            // }
+                            // For compatibility with the alacritty::Colors interface
+                            // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
+                            _ => |_| gpui::black(),
+                        }
+                    }
+                };
+
+                background_highlights.push(BackgroundHighlight {
+                    range,
+                    color_fetcher,
+                });
+            }
+            console.highlight_background_ranges::<ConsoleAnsiHighlight>(background_highlights, cx);
+
             console.set_read_only(true);
 
             cx.notify();
@@ -459,3 +635,69 @@ impl ConsoleQueryBarCompletionProvider {
         })
     }
 }
+
+#[derive(Default)]
+struct ConsoleHandler {
+    output: String,
+    spans: Vec<(Range<usize>, Option<ansi::Color>)>,
+    background_spans: Vec<(Range<usize>, Option<ansi::Color>)>,
+    current_range_start: usize,
+    current_background_range_start: usize,
+    current_color: Option<ansi::Color>,
+    current_background_color: Option<ansi::Color>,
+    pos: usize,
+}
+
+impl ConsoleHandler {
+    fn break_span(&mut self, color: Option<ansi::Color>) {
+        self.spans.push((
+            self.current_range_start..self.output.len(),
+            self.current_color,
+        ));
+        self.current_color = color;
+        self.current_range_start = self.pos;
+    }
+
+    fn break_background_span(&mut self, color: Option<ansi::Color>) {
+        self.background_spans.push((
+            self.current_background_range_start..self.output.len(),
+            self.current_background_color,
+        ));
+        self.current_background_color = color;
+        self.current_background_range_start = self.pos;
+    }
+}
+
+impl ansi::Handler for ConsoleHandler {
+    fn input(&mut self, c: char) {
+        self.output.push(c);
+        self.pos += 1;
+    }
+
+    fn linefeed(&mut self) {
+        self.output.push('\n');
+        self.pos += 1;
+    }
+
+    fn put_tab(&mut self, count: u16) {
+        self.output
+            .extend(std::iter::repeat('\t').take(count as usize));
+        self.pos += count as usize;
+    }
+
+    fn terminal_attribute(&mut self, attr: ansi::Attr) {
+        match attr {
+            ansi::Attr::Foreground(color) => {
+                self.break_span(Some(color));
+            }
+            ansi::Attr::Background(color) => {
+                self.break_background_span(Some(color));
+            }
+            ansi::Attr::Reset => {
+                self.break_span(None);
+                self.break_background_span(None);
+            }
+            _ => {}
+        }
+    }
+}

crates/debugger_ui/src/tests/console.rs πŸ”—

@@ -110,7 +110,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
     client
         .fake_event(dap::messages::Events::Output(dap::OutputEvent {
             category: Some(dap::OutputEventCategory::Stdout),
-            output: "Second output line after thread stopped!".to_string(),
+            output: "\tSecond output line after thread stopped!".to_string(),
             data: None,
             variables_reference: None,
             source: None,
@@ -124,7 +124,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
     client
         .fake_event(dap::messages::Events::Output(dap::OutputEvent {
             category: Some(dap::OutputEventCategory::Console),
-            output: "Second console output line after thread stopped!".to_string(),
+            output: "\tSecond console output line after thread stopped!".to_string(),
             data: None,
             variables_reference: None,
             source: None,
@@ -150,7 +150,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
                 .unwrap();
 
             assert_eq!(
-                "First console output line before thread stopped!\nFirst output line before thread stopped!\nSecond output line after thread stopped!\nSecond console output line after thread stopped!\n",
+                "First console output line before thread stopped!\nFirst output line before thread stopped!\n\tSecond output line after thread stopped!\n\tSecond console output line after thread stopped!\n",
                 active_session_panel.read(cx).running_state().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
             );
         })

crates/editor/src/display_map.rs πŸ”—

@@ -80,7 +80,7 @@ pub trait ToDisplayPoint {
     fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
 }
 
-type TextHighlights = TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+type TextHighlights = TreeMap<TypeId, Vec<(Range<Anchor>, HighlightStyle)>>;
 type InlayHighlights = TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>>;
 
 /// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints,
@@ -474,11 +474,9 @@ impl DisplayMap {
     pub fn highlight_text(
         &mut self,
         type_id: TypeId,
-        ranges: Vec<Range<Anchor>>,
-        style: HighlightStyle,
+        ranges: Vec<(Range<Anchor>, HighlightStyle)>,
     ) {
-        self.text_highlights
-            .insert(type_id, Arc::new((style, ranges)));
+        self.text_highlights.insert(type_id, ranges);
     }
 
     pub(crate) fn highlight_inlays(
@@ -500,16 +498,25 @@ impl DisplayMap {
         }
     }
 
-    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
-        let highlights = self.text_highlights.get(&type_id)?;
-        Some((highlights.0, &highlights.1))
+    pub fn text_highlights(&self, type_id: TypeId) -> Option<&[(Range<Anchor>, HighlightStyle)]> {
+        self.text_highlights
+            .get(&type_id)
+            .map(|highlights| highlights.as_slice())
     }
+
     pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
         let mut cleared = self.text_highlights.remove(&type_id).is_some();
         cleared |= self.inlay_highlights.remove(&type_id).is_some();
         cleared
     }
 
+    pub fn remove_text_highlights(
+        &mut self,
+        type_id: TypeId,
+    ) -> Option<Vec<(Range<Anchor>, HighlightStyle)>> {
+        self.text_highlights.remove(&type_id)
+    }
+
     pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut Context<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_font_with_size(font, font_size, cx))
@@ -1331,7 +1338,7 @@ impl DisplaySnapshot {
     #[cfg(any(test, feature = "test-support"))]
     pub fn text_highlight_ranges<Tag: ?Sized + 'static>(
         &self,
-    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+    ) -> Option<Vec<(Range<Anchor>, HighlightStyle)>> {
         let type_id = TypeId::of::<Tag>();
         self.text_highlights.get(&type_id).cloned()
     }
@@ -2296,12 +2303,17 @@ pub mod tests {
             map.highlight_text(
                 TypeId::of::<usize>(),
                 vec![
-                    buffer_snapshot.anchor_before(Point::new(3, 9))
-                        ..buffer_snapshot.anchor_after(Point::new(3, 14)),
-                    buffer_snapshot.anchor_before(Point::new(3, 17))
-                        ..buffer_snapshot.anchor_after(Point::new(3, 18)),
+                    (
+                        buffer_snapshot.anchor_before(Point::new(3, 9))
+                            ..buffer_snapshot.anchor_after(Point::new(3, 14)),
+                        red.into(),
+                    ),
+                    (
+                        buffer_snapshot.anchor_before(Point::new(3, 17))
+                            ..buffer_snapshot.anchor_after(Point::new(3, 18)),
+                        red.into(),
+                    ),
                 ],
-                red.into(),
             );
             map.insert_blocks(
                 [BlockProperties {
@@ -2620,11 +2632,13 @@ pub mod tests {
                 highlighted_ranges
                     .into_iter()
                     .map(|range| {
-                        buffer_snapshot.anchor_before(range.start)
-                            ..buffer_snapshot.anchor_before(range.end)
+                        (
+                            buffer_snapshot.anchor_before(range.start)
+                                ..buffer_snapshot.anchor_before(range.end),
+                            style,
+                        )
                     })
                     .collect(),
-                style,
             );
         });
 

crates/editor/src/display_map/custom_highlights.rs πŸ”—

@@ -1,16 +1,16 @@
 use collections::BTreeMap;
 use gpui::HighlightStyle;
 use language::Chunk;
-use multi_buffer::{Anchor, MultiBufferChunks, MultiBufferSnapshot, ToOffset as _};
+use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot, ToOffset as _};
 use std::{
     any::TypeId,
     cmp,
     iter::{self, Peekable},
     ops::Range,
-    sync::Arc,
     vec,
 };
-use sum_tree::TreeMap;
+
+use crate::display_map::TextHighlights;
 
 pub struct CustomHighlightsChunks<'a> {
     buffer_chunks: MultiBufferChunks<'a>,
@@ -19,15 +19,15 @@ pub struct CustomHighlightsChunks<'a> {
     multibuffer_snapshot: &'a MultiBufferSnapshot,
 
     highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
-    active_highlights: BTreeMap<TypeId, HighlightStyle>,
-    text_highlights: Option<&'a TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
+    active_highlights: BTreeMap<(TypeId, usize), HighlightStyle>,
+    text_highlights: Option<&'a TextHighlights>,
 }
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
 struct HighlightEndpoint {
     offset: usize,
     is_start: bool,
-    tag: TypeId,
+    tag: (TypeId, usize),
     style: HighlightStyle,
 }
 
@@ -35,7 +35,7 @@ impl<'a> CustomHighlightsChunks<'a> {
     pub fn new(
         range: Range<usize>,
         language_aware: bool,
-        text_highlights: Option<&'a TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
+        text_highlights: Option<&'a TextHighlights>,
         multibuffer_snapshot: &'a MultiBufferSnapshot,
     ) -> Self {
         Self {
@@ -66,7 +66,7 @@ impl<'a> CustomHighlightsChunks<'a> {
 
 fn create_highlight_endpoints(
     range: &Range<usize>,
-    text_highlights: Option<&TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
+    text_highlights: Option<&TextHighlights>,
     buffer: &MultiBufferSnapshot,
 ) -> iter::Peekable<vec::IntoIter<HighlightEndpoint>> {
     let mut highlight_endpoints = Vec::new();
@@ -74,10 +74,7 @@ fn create_highlight_endpoints(
         let start = buffer.anchor_after(range.start);
         let end = buffer.anchor_after(range.end);
         for (&tag, text_highlights) in text_highlights.iter() {
-            let style = text_highlights.0;
-            let ranges = &text_highlights.1;
-
-            let start_ix = match ranges.binary_search_by(|probe| {
+            let start_ix = match text_highlights.binary_search_by(|(probe, _)| {
                 let cmp = probe.end.cmp(&start, &buffer);
                 if cmp.is_gt() {
                     cmp::Ordering::Greater
@@ -88,7 +85,7 @@ fn create_highlight_endpoints(
                 Ok(i) | Err(i) => i,
             };
 
-            for range in &ranges[start_ix..] {
+            for (ix, (range, style)) in text_highlights[start_ix..].iter().enumerate() {
                 if range.start.cmp(&end, &buffer).is_ge() {
                     break;
                 }
@@ -96,14 +93,14 @@ fn create_highlight_endpoints(
                 highlight_endpoints.push(HighlightEndpoint {
                     offset: range.start.to_offset(&buffer),
                     is_start: true,
-                    tag,
-                    style,
+                    tag: (tag, ix),
+                    style: *style,
                 });
                 highlight_endpoints.push(HighlightEndpoint {
                     offset: range.end.to_offset(&buffer),
                     is_start: false,
-                    tag,
-                    style,
+                    tag: (tag, ix),
+                    style: *style,
                 });
             }
         }

crates/editor/src/display_map/inlay_map.rs πŸ”—

@@ -1085,7 +1085,7 @@ mod tests {
     use project::{InlayHint, InlayHintLabel, ResolveState};
     use rand::prelude::*;
     use settings::SettingsStore;
-    use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
+    use std::{any::TypeId, cmp::Reverse, env};
     use sum_tree::TreeMap;
     use text::Patch;
     use util::post_inc;
@@ -1593,16 +1593,16 @@ mod tests {
             log::info!("highlighting text ranges {text_highlight_ranges:?}");
             text_highlights.insert(
                 TypeId::of::<()>(),
-                Arc::new((
-                    HighlightStyle::default(),
-                    text_highlight_ranges
-                        .into_iter()
-                        .map(|range| {
+                text_highlight_ranges
+                    .into_iter()
+                    .map(|range| {
+                        (
                             buffer_snapshot.anchor_before(range.start)
-                                ..buffer_snapshot.anchor_after(range.end)
-                        })
-                        .collect(),
-                )),
+                                ..buffer_snapshot.anchor_after(range.end),
+                            HighlightStyle::default(),
+                        )
+                    })
+                    .collect(),
             );
 
             let mut inlay_highlights = InlayHighlights::default();

crates/editor/src/editor.rs πŸ”—

@@ -196,7 +196,7 @@ pub use sum_tree::Bias;
 use sum_tree::TreeMap;
 use text::{BufferId, FromAnchor, OffsetUtf16, Rope};
 use theme::{
-    ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings,
+    ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
     observe_buffer_font_size_adjustment,
 };
 use ui::{
@@ -704,7 +704,12 @@ impl EditorActionId {
 // type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
-type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
+#[derive(Clone)]
+pub struct BackgroundHighlight {
+    pub range: Range<Anchor>,
+    pub color_fetcher: fn(&Theme) -> Hsla,
+}
+
 type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
 
 #[derive(Default)]
@@ -1013,7 +1018,7 @@ pub struct Editor {
     placeholder_text: Option<Arc<str>>,
     highlight_order: usize,
     highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
-    background_highlights: TreeMap<TypeId, BackgroundHighlight>,
+    background_highlights: TreeMap<TypeId, Vec<BackgroundHighlight>>,
     gutter_highlights: TreeMap<TypeId, GutterHighlight>,
     scrollbar_marker_state: ScrollbarMarkerState,
     active_indent_guides_state: ActiveIndentGuidesState,
@@ -6141,7 +6146,7 @@ impl Editor {
             editor.update(cx, |editor, cx| {
                 editor.highlight_background::<Self>(
                     &ranges_to_highlight,
-                    |theme| theme.editor_highlighted_line_background,
+                    |theme| theme.colors().editor_highlighted_line_background,
                     cx,
                 );
             });
@@ -6496,12 +6501,12 @@ impl Editor {
 
                     this.highlight_background::<DocumentHighlightRead>(
                         &read_ranges,
-                        |theme| theme.editor_document_highlight_read_background,
+                        |theme| theme.colors().editor_document_highlight_read_background,
                         cx,
                     );
                     this.highlight_background::<DocumentHighlightWrite>(
                         &write_ranges,
-                        |theme| theme.editor_document_highlight_write_background,
+                        |theme| theme.colors().editor_document_highlight_write_background,
                         cx,
                     );
                     cx.notify();
@@ -6603,7 +6608,7 @@ impl Editor {
                     if !match_ranges.is_empty() {
                         editor.highlight_background::<SelectedTextHighlight>(
                             &match_ranges,
-                            |theme| theme.editor_document_highlight_bracket_background,
+                            |theme| theme.colors().editor_document_highlight_bracket_background,
                             cx,
                         )
                     }
@@ -7479,12 +7484,15 @@ impl Editor {
                     self.splice_inlays(&[], inlays, cx);
                 } else {
                     let background_color = cx.theme().status().deleted_background;
+                    let style = HighlightStyle {
+                        background_color: Some(background_color),
+                        ..Default::default()
+                    };
                     self.highlight_text::<InlineCompletionHighlight>(
-                        edits.iter().map(|(range, _)| range.clone()).collect(),
-                        HighlightStyle {
-                            background_color: Some(background_color),
-                            ..Default::default()
-                        },
+                        edits
+                            .iter()
+                            .map(|(range, _)| (range.clone(), style))
+                            .collect(),
                         cx,
                     );
                 }
@@ -15351,7 +15359,7 @@ impl Editor {
                     }
                     editor.highlight_background::<Self>(
                         &ranges,
-                        |theme| theme.editor_highlighted_line_background,
+                        |theme| theme.colors().editor_highlighted_line_background,
                         cx,
                     );
                 }
@@ -15506,25 +15514,28 @@ impl Editor {
                     })
                     .detach();
 
-                    let write_highlights =
-                        this.clear_background_highlights::<DocumentHighlightWrite>(cx);
-                    let read_highlights =
-                        this.clear_background_highlights::<DocumentHighlightRead>(cx);
+                    let write_highlights = this
+                        .clear_background_highlights::<DocumentHighlightWrite>(cx)
+                        .unwrap_or_default();
+                    let read_highlights = this
+                        .clear_background_highlights::<DocumentHighlightRead>(cx)
+                        .unwrap_or_default();
                     let ranges = write_highlights
                         .iter()
-                        .flat_map(|(_, ranges)| ranges.iter())
-                        .chain(read_highlights.iter().flat_map(|(_, ranges)| ranges.iter()))
+                        .chain(read_highlights.iter())
                         .cloned()
+                        .map(|highlight| {
+                            (
+                                highlight.range,
+                                HighlightStyle {
+                                    fade_out: Some(0.6),
+                                    ..Default::default()
+                                },
+                            )
+                        })
                         .collect();
 
-                    this.highlight_text::<Rename>(
-                        ranges,
-                        HighlightStyle {
-                            fade_out: Some(0.6),
-                            ..Default::default()
-                        },
-                        cx,
-                    );
+                    this.highlight_text::<Rename>(ranges, cx);
                     let rename_focus_handle = rename_editor.focus_handle(cx);
                     window.focus(&rename_focus_handle);
                     let block_id = this.insert_blocks(
@@ -18500,7 +18511,7 @@ impl Editor {
     pub fn set_search_within_ranges(&mut self, ranges: &[Range<Anchor>], cx: &mut Context<Self>) {
         self.highlight_background::<SearchWithinRange>(
             ranges,
-            |colors| colors.editor_document_highlight_read_background,
+            |theme| theme.colors().editor_document_highlight_read_background,
             cx,
         )
     }
@@ -18516,11 +18527,29 @@ impl Editor {
     pub fn highlight_background<T: 'static>(
         &mut self,
         ranges: &[Range<Anchor>],
-        color_fetcher: fn(&ThemeColors) -> Hsla,
+        color_fetcher: fn(&Theme) -> Hsla,
         cx: &mut Context<Self>,
     ) {
+        let highlights = ranges
+            .iter()
+            .map(|range| BackgroundHighlight {
+                range: range.clone(),
+                color_fetcher,
+            })
+            .collect();
         self.background_highlights
-            .insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
+            .insert(TypeId::of::<T>(), highlights);
+        self.scrollbar_marker_state.dirty = true;
+        cx.notify();
+    }
+
+    pub fn highlight_background_ranges<T: 'static>(
+        &mut self,
+        background_highlights: Vec<BackgroundHighlight>,
+        cx: &mut Context<'_, Editor>,
+    ) {
+        self.background_highlights
+            .insert(TypeId::of::<T>(), background_highlights);
         self.scrollbar_marker_state.dirty = true;
         cx.notify();
     }
@@ -18528,9 +18557,9 @@ impl Editor {
     pub fn clear_background_highlights<T: 'static>(
         &mut self,
         cx: &mut Context<Self>,
-    ) -> Option<BackgroundHighlight> {
+    ) -> Option<Vec<BackgroundHighlight>> {
         let text_highlights = self.background_highlights.remove(&TypeId::of::<T>())?;
-        if !text_highlights.1.is_empty() {
+        if !text_highlights.is_empty() {
             self.scrollbar_marker_state.dirty = true;
             cx.notify();
         }
@@ -18625,7 +18654,7 @@ impl Editor {
         let buffer = &snapshot.buffer_snapshot;
         let start = buffer.anchor_before(0);
         let end = buffer.anchor_after(buffer.len());
-        let theme = cx.theme().colors();
+        let theme = cx.theme();
         self.background_highlights_in_range(start..end, &snapshot, theme)
     }
 
@@ -18637,10 +18666,13 @@ impl Editor {
             .background_highlights
             .get(&TypeId::of::<items::BufferSearchHighlights>());
 
-        if let Some((_color, ranges)) = highlights {
-            ranges
+        if let Some(highlights) = highlights {
+            highlights
                 .iter()
-                .map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot))
+                .map(|highlight| {
+                    highlight.range.start.to_point(&snapshot)
+                        ..highlight.range.end.to_point(&snapshot)
+                })
                 .collect_vec()
         } else {
             vec![]
@@ -18654,20 +18686,18 @@ impl Editor {
     ) -> impl 'a + Iterator<Item = &'a Range<Anchor>> {
         let read_highlights = self
             .background_highlights
-            .get(&TypeId::of::<DocumentHighlightRead>())
-            .map(|h| &h.1);
+            .get(&TypeId::of::<DocumentHighlightRead>());
         let write_highlights = self
             .background_highlights
-            .get(&TypeId::of::<DocumentHighlightWrite>())
-            .map(|h| &h.1);
+            .get(&TypeId::of::<DocumentHighlightWrite>());
         let left_position = position.bias_left(buffer);
         let right_position = position.bias_right(buffer);
         read_highlights
             .into_iter()
             .chain(write_highlights)
-            .flat_map(move |ranges| {
-                let start_ix = match ranges.binary_search_by(|probe| {
-                    let cmp = probe.end.cmp(&left_position, buffer);
+            .flat_map(move |highlights| {
+                let start_ix = match highlights.binary_search_by(|probe| {
+                    let cmp = probe.range.end.cmp(&left_position, buffer);
                     if cmp.is_ge() {
                         Ordering::Greater
                     } else {
@@ -18677,29 +18707,32 @@ impl Editor {
                     Ok(i) | Err(i) => i,
                 };
 
-                ranges[start_ix..]
+                highlights[start_ix..]
                     .iter()
-                    .take_while(move |range| range.start.cmp(&right_position, buffer).is_le())
+                    .take_while(move |highlight| {
+                        highlight.range.start.cmp(&right_position, buffer).is_le()
+                    })
+                    .map(|highlight| &highlight.range)
             })
     }
 
     pub fn has_background_highlights<T: 'static>(&self) -> bool {
         self.background_highlights
             .get(&TypeId::of::<T>())
-            .map_or(false, |(_, highlights)| !highlights.is_empty())
+            .map_or(false, |highlights| !highlights.is_empty())
     }
 
     pub fn background_highlights_in_range(
         &self,
         search_range: Range<Anchor>,
         display_snapshot: &DisplaySnapshot,
-        theme: &ThemeColors,
+        theme: &Theme,
     ) -> Vec<(Range<DisplayPoint>, Hsla)> {
         let mut results = Vec::new();
-        for (color_fetcher, ranges) in self.background_highlights.values() {
-            let color = color_fetcher(theme);
-            let start_ix = match ranges.binary_search_by(|probe| {
+        for highlights in self.background_highlights.values() {
+            let start_ix = match highlights.binary_search_by(|probe| {
                 let cmp = probe
+                    .range
                     .end
                     .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
                 if cmp.is_gt() {
@@ -18710,8 +18743,9 @@ impl Editor {
             }) {
                 Ok(i) | Err(i) => i,
             };
-            for range in &ranges[start_ix..] {
-                if range
+            for highlight in &highlights[start_ix..] {
+                if highlight
+                    .range
                     .start
                     .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
                     .is_ge()
@@ -18719,8 +18753,9 @@ impl Editor {
                     break;
                 }
 
-                let start = range.start.to_display_point(display_snapshot);
-                let end = range.end.to_display_point(display_snapshot);
+                let start = highlight.range.start.to_display_point(display_snapshot);
+                let end = highlight.range.end.to_display_point(display_snapshot);
+                let color = (highlight.color_fetcher)(theme);
                 results.push((start..end, color))
             }
         }
@@ -18734,12 +18769,13 @@ impl Editor {
         count: usize,
     ) -> Vec<RangeInclusive<DisplayPoint>> {
         let mut results = Vec::new();
-        let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::<T>()) else {
+        let Some(highlights) = self.background_highlights.get(&TypeId::of::<T>()) else {
             return vec![];
         };
 
-        let start_ix = match ranges.binary_search_by(|probe| {
+        let start_ix = match highlights.binary_search_by(|probe| {
             let cmp = probe
+                .range
                 .end
                 .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
             if cmp.is_gt() {
@@ -18760,24 +18796,31 @@ impl Editor {
         };
         let mut start_row: Option<Point> = None;
         let mut end_row: Option<Point> = None;
-        if ranges.len() > count {
+        if highlights.len() > count {
             return Vec::new();
         }
-        for range in &ranges[start_ix..] {
-            if range
+        for highlight in &highlights[start_ix..] {
+            if highlight
+                .range
                 .start
                 .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
                 .is_ge()
             {
                 break;
             }
-            let end = range.end.to_point(&display_snapshot.buffer_snapshot);
+            let end = highlight
+                .range
+                .end
+                .to_point(&display_snapshot.buffer_snapshot);
             if let Some(current_row) = &end_row {
                 if end.row == current_row.row {
                     continue;
                 }
             }
-            let start = range.start.to_point(&display_snapshot.buffer_snapshot);
+            let start = highlight
+                .range
+                .start
+                .to_point(&display_snapshot.buffer_snapshot);
             if start_row.is_none() {
                 assert_eq!(end_row, None);
                 start_row = Some(start);
@@ -18873,13 +18916,11 @@ impl Editor {
 
     pub fn highlight_text<T: 'static>(
         &mut self,
-        ranges: Vec<Range<Anchor>>,
-        style: HighlightStyle,
+        ranges: Vec<(Range<Anchor>, HighlightStyle)>,
         cx: &mut Context<Self>,
     ) {
-        self.display_map.update(cx, |map, _| {
-            map.highlight_text(TypeId::of::<T>(), ranges, style)
-        });
+        self.display_map
+            .update(cx, |map, _| map.highlight_text(TypeId::of::<T>(), ranges));
         cx.notify();
     }
 
@@ -18898,7 +18939,7 @@ impl Editor {
     pub fn text_highlights<'a, T: 'static>(
         &'a self,
         cx: &'a App,
-    ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
+    ) -> Option<&'a [(Range<Anchor>, HighlightStyle)]> {
         self.display_map.read(cx).text_highlights(TypeId::of::<T>())
     }
 
@@ -18911,6 +18952,14 @@ impl Editor {
         }
     }
 
+    pub fn remove_text_highlights<T: 'static>(
+        &mut self,
+        cx: &mut Context<Self>,
+    ) -> Option<Vec<(Range<Anchor>, HighlightStyle)>> {
+        self.display_map
+            .update(cx, |map, _| map.remove_text_highlights(TypeId::of::<T>()))
+    }
+
     pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool {
         (self.read_only(cx) || self.blink_manager.read(cx).visible())
             && self.focus_handle.is_focused(window)
@@ -19573,11 +19622,11 @@ impl Editor {
 
     fn marked_text_ranges(&self, cx: &App) -> Option<Vec<Range<OffsetUtf16>>> {
         let snapshot = self.buffer.read(cx).read(cx);
-        let (_, ranges) = self.text_highlights::<InputComposition>(cx)?;
+        let ranges = self.text_highlights::<InputComposition>(cx)?;
         Some(
             ranges
                 .iter()
-                .map(move |range| {
+                .map(move |(range, _)| {
                     range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
                 })
                 .collect(),
@@ -19888,9 +19937,12 @@ impl Editor {
             pending = "".to_string();
         }
 
-        let existing_pending = self
-            .text_highlights::<PendingInput>(cx)
-            .map(|(_, ranges)| ranges.iter().cloned().collect::<Vec<_>>());
+        let existing_pending = self.text_highlights::<PendingInput>(cx).map(|ranges| {
+            ranges
+                .iter()
+                .map(|(range, _)| range.clone())
+                .collect::<Vec<_>>()
+        });
         if existing_pending.is_none() && pending.is_empty() {
             return;
         }
@@ -19918,28 +19970,27 @@ impl Editor {
             .all::<usize>(cx)
             .into_iter()
             .map(|selection| {
-                snapshot.buffer_snapshot.anchor_after(selection.end)
-                    ..snapshot
-                        .buffer_snapshot
-                        .anchor_before(selection.end + pending.len())
+                (
+                    snapshot.buffer_snapshot.anchor_after(selection.end)
+                        ..snapshot
+                            .buffer_snapshot
+                            .anchor_before(selection.end + pending.len()),
+                    HighlightStyle {
+                        underline: Some(UnderlineStyle {
+                            thickness: px(1.),
+                            color: None,
+                            wavy: false,
+                        }),
+                        ..Default::default()
+                    },
+                )
             })
             .collect();
 
         if pending.is_empty() {
             self.clear_highlights::<PendingInput>(cx);
         } else {
-            self.highlight_text::<PendingInput>(
-                ranges,
-                HighlightStyle {
-                    underline: Some(UnderlineStyle {
-                        thickness: px(1.),
-                        color: None,
-                        wavy: false,
-                    }),
-                    ..Default::default()
-                },
-                cx,
-            );
+            self.highlight_text::<PendingInput>(ranges, cx);
         }
 
         self.ime_transaction = self.ime_transaction.or(transaction);
@@ -22012,7 +22063,7 @@ impl EntityInputHandler for Editor {
 
     fn marked_text_range(&self, _: &mut Window, cx: &mut Context<Self>) -> Option<Range<usize>> {
         let snapshot = self.buffer.read(cx).read(cx);
-        let range = self.text_highlights::<InputComposition>(cx)?.1.first()?;
+        let (range, _) = self.text_highlights::<InputComposition>(cx)?.first()?;
         Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
     }
 
@@ -22149,7 +22200,18 @@ impl EntityInputHandler for Editor {
                     .disjoint_anchors()
                     .iter()
                     .map(|selection| {
-                        selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot)
+                        (
+                            selection.start.bias_left(&snapshot)
+                                ..selection.end.bias_right(&snapshot),
+                            HighlightStyle {
+                                underline: Some(UnderlineStyle {
+                                    thickness: px(1.),
+                                    color: None,
+                                    wavy: false,
+                                }),
+                                ..Default::default()
+                            },
+                        )
                     })
                     .collect::<Vec<_>>()
             };
@@ -22157,18 +22219,7 @@ impl EntityInputHandler for Editor {
             if text.is_empty() {
                 this.unmark_text(window, cx);
             } else {
-                this.highlight_text::<InputComposition>(
-                    marked_ranges.clone(),
-                    HighlightStyle {
-                        underline: Some(UnderlineStyle {
-                            thickness: px(1.),
-                            color: None,
-                            wavy: false,
-                        }),
-                        ..Default::default()
-                    },
-                    cx,
-                );
+                this.highlight_text::<InputComposition>(marked_ranges.clone(), cx);
             }
 
             // Disable auto-closing when composing text (i.e. typing a `"` on a Brazilian keyboard)
@@ -22184,7 +22235,7 @@ impl EntityInputHandler for Editor {
                 let snapshot = this.buffer.read(cx).read(cx);
                 let new_selected_ranges = marked_ranges
                     .into_iter()
-                    .map(|marked_range| {
+                    .map(|(marked_range, _)| {
                         let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
                         let new_start = OffsetUtf16(new_selected_range.start + insertion_start);
                         let new_end = OffsetUtf16(new_selected_range.end + insertion_start);

crates/editor/src/editor_tests.rs πŸ”—

@@ -13697,7 +13697,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
         let mut highlighted_ranges = editor.background_highlights_in_range(
             anchor_range(Point::new(3, 4)..Point::new(7, 4)),
             &snapshot,
-            cx.theme().colors(),
+            cx.theme(),
         );
         // Enforce a consistent ordering based on color without relying on the ordering of the
         // highlight's `TypeId` which is non-executor.
@@ -13727,7 +13727,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
             editor.background_highlights_in_range(
                 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
                 &snapshot,
-                cx.theme().colors(),
+                cx.theme(),
             ),
             &[(
                 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
@@ -19392,8 +19392,10 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
         let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
         let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
         editor.highlight_text::<TestHighlight>(
-            vec![highlight_range.clone()],
-            HighlightStyle::color(Hsla::green()),
+            vec![(
+                highlight_range.clone(),
+                HighlightStyle::color(Hsla::green()),
+            )],
             cx,
         );
         editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
@@ -20334,7 +20336,7 @@ async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
         let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
         editor.highlight_background::<DocumentHighlightRead>(
             &[highlight_range],
-            |c| c.editor_document_highlight_read_background,
+            |c| c.colors().editor_document_highlight_read_background,
             cx,
         );
     });
@@ -20412,7 +20414,7 @@ async fn test_rename_without_prepare(cx: &mut TestAppContext) {
         let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
         editor.highlight_background::<DocumentHighlightRead>(
             &[highlight_range],
-            |c| c.editor_document_highlight_read_background,
+            |c| c.colors().editor_document_highlight_read_background,
             cx,
         );
     });

crates/editor/src/element.rs πŸ”—

@@ -6105,7 +6105,7 @@ impl EditorElement {
                                 );
                             }
 
-                            for (background_highlight_id, (_, background_ranges)) in
+                            for (background_highlight_id, background_highlights) in
                                 background_highlights.iter()
                             {
                                 let is_search_highlights = *background_highlight_id
@@ -6124,18 +6124,22 @@ impl EditorElement {
                                     if is_symbol_occurrences {
                                         color.fade_out(0.5);
                                     }
-                                    let marker_row_ranges = background_ranges.iter().map(|range| {
-                                        let display_start = range
-                                            .start
-                                            .to_display_point(&snapshot.display_snapshot);
-                                        let display_end =
-                                            range.end.to_display_point(&snapshot.display_snapshot);
-                                        ColoredRange {
-                                            start: display_start.row(),
-                                            end: display_end.row(),
-                                            color,
-                                        }
-                                    });
+                                    let marker_row_ranges =
+                                        background_highlights.iter().map(|highlight| {
+                                            let display_start = highlight
+                                                .range
+                                                .start
+                                                .to_display_point(&snapshot.display_snapshot);
+                                            let display_end = highlight
+                                                .range
+                                                .end
+                                                .to_display_point(&snapshot.display_snapshot);
+                                            ColoredRange {
+                                                start: display_start.row(),
+                                                end: display_end.row(),
+                                                color,
+                                            }
+                                        });
                                     marker_quads.extend(
                                         scrollbar_layout
                                             .marker_quads_for_ranges(marker_row_ranges, Some(1)),
@@ -8033,7 +8037,7 @@ impl Element for EditorElement {
                             editor.read(cx).background_highlights_in_range(
                                 start_anchor..end_anchor,
                                 &snapshot.display_snapshot,
-                                cx.theme().colors(),
+                                cx.theme(),
                             )
                         })
                         .unwrap_or_default();

crates/editor/src/highlight_matching_bracket.rs πŸ”—

@@ -35,7 +35,7 @@ pub fn refresh_matching_bracket_highlights(
                 opening_range.to_anchors(&snapshot.buffer_snapshot),
                 closing_range.to_anchors(&snapshot.buffer_snapshot),
             ],
-            |theme| theme.editor_document_highlight_bracket_background,
+            |theme| theme.colors().editor_document_highlight_bracket_background,
             cx,
         )
     }

crates/editor/src/hover_links.rs πŸ”—

@@ -635,7 +635,7 @@ pub fn show_link_definition(
 
                         match highlight_range {
                             RangeInEditor::Text(text_range) => editor
-                                .highlight_text::<HoveredLinkState>(vec![text_range], style, cx),
+                                .highlight_text::<HoveredLinkState>(vec![(text_range, style)], cx),
                             RangeInEditor::Inlay(highlight) => editor
                                 .highlight_inlays::<HoveredLinkState>(vec![highlight], style, cx),
                         }
@@ -1403,7 +1403,6 @@ mod tests {
                 let snapshot = editor.snapshot(window, cx);
                 let actual_ranges = snapshot
                     .text_highlight_ranges::<HoveredLinkState>()
-                    .map(|ranges| ranges.as_ref().clone().1)
                     .unwrap_or_default();
 
                 assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
@@ -1635,7 +1634,6 @@ mod tests {
                     .snapshot(window, cx)
                     .text_highlight_ranges::<HoveredLinkState>()
                     .unwrap_or_default()
-                    .1
                     .is_empty()
             );
         });
@@ -1842,7 +1840,6 @@ mod tests {
                     .snapshot(window, cx)
                     .text_highlight_ranges::<HoveredLinkState>()
                     .unwrap_or_default()
-                    .1
                     .is_empty()
             );
         });

crates/editor/src/hover_popover.rs πŸ”—

@@ -520,7 +520,7 @@ fn show_hover(
                     // Highlight the selected symbol using a background highlight
                     editor.highlight_background::<HoverState>(
                         &hover_highlights,
-                        |theme| theme.element_hover, // todo update theme
+                        |theme| theme.colors().element_hover, // todo update theme
                         cx,
                     );
                 }

crates/editor/src/items.rs πŸ”—

@@ -1432,8 +1432,11 @@ impl SearchableItem for Editor {
     fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec<Range<Anchor>> {
         self.background_highlights
             .get(&TypeId::of::<BufferSearchHighlights>())
-            .map_or(Vec::new(), |(_color, ranges)| {
-                ranges.iter().cloned().collect()
+            .map_or(Vec::new(), |highlights| {
+                highlights
+                    .iter()
+                    .map(|highlight| highlight.range.clone())
+                    .collect()
             })
     }
 
@@ -1452,14 +1455,14 @@ impl SearchableItem for Editor {
         _: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let existing_range = self
+        let existing_ranges = self
             .background_highlights
             .get(&TypeId::of::<BufferSearchHighlights>())
-            .map(|(_, range)| range.as_ref());
-        let updated = existing_range != Some(matches);
+            .map(|highlights| highlights.iter().map(|highlight| &highlight.range));
+        let updated = !existing_ranges.is_some_and(|existing_ranges| existing_ranges.eq(matches));
         self.highlight_background::<BufferSearchHighlights>(
             matches,
-            |theme| theme.search_match_background,
+            |theme| theme.colors().search_match_background,
             cx,
         );
         if updated {
@@ -1480,7 +1483,12 @@ impl SearchableItem for Editor {
         if self.has_filtered_search_ranges() {
             self.previous_search_ranges = self
                 .clear_background_highlights::<SearchWithinRange>(cx)
-                .map(|(_, ranges)| ranges)
+                .map(|highlights| {
+                    highlights
+                        .iter()
+                        .map(|highlight| highlight.range.clone())
+                        .collect()
+                })
         }
 
         if !enabled {
@@ -1702,8 +1710,11 @@ impl SearchableItem for Editor {
         let search_within_ranges = self
             .background_highlights
             .get(&TypeId::of::<SearchWithinRange>())
-            .map_or(vec![], |(_color, ranges)| {
-                ranges.iter().cloned().collect::<Vec<_>>()
+            .map_or(vec![], |highlights| {
+                highlights
+                    .iter()
+                    .map(|highlight| highlight.range.clone())
+                    .collect::<Vec<_>>()
             });
 
         cx.background_spawn(async move {

crates/editor/src/test/editor_test_context.rs πŸ”—

@@ -510,10 +510,9 @@ impl EditorTestContext {
             editor
                 .background_highlights
                 .get(&TypeId::of::<Tag>())
-                .map(|h| h.1.clone())
-                .unwrap_or_default()
-                .iter()
-                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
+                .into_iter()
+                .flat_map(|highlights| highlights.as_slice())
+                .map(|highlight| highlight.range.to_offset(&snapshot.buffer_snapshot))
                 .collect()
         });
         assert_set_eq!(actual_ranges, expected_ranges);
@@ -525,7 +524,12 @@ impl EditorTestContext {
         let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
         let actual_ranges: Vec<Range<usize>> = snapshot
             .text_highlight_ranges::<Tag>()
-            .map(|ranges| ranges.as_ref().clone().1)
+            .map(|ranges| {
+                ranges
+                    .iter()
+                    .map(|(range, _)| range.clone())
+                    .collect::<Vec<_>>()
+            })
             .unwrap_or_default()
             .into_iter()
             .map(|range| range.to_offset(&snapshot.buffer_snapshot))

crates/language_tools/src/syntax_tree_view.rs πŸ”—

@@ -358,7 +358,7 @@ impl Render for SyntaxTreeView {
                                                 editor.clear_background_highlights::<Self>( cx);
                                                 editor.highlight_background::<Self>(
                                                     &[range],
-                                                    |theme| theme.editor_document_highlight_write_background,
+                                                    |theme| theme.colors().editor_document_highlight_write_background,
                                                      cx,
                                                 );
                                             });

crates/project/src/debugger/dap_command.rs πŸ”—

@@ -1547,7 +1547,7 @@ fn dap_client_capabilities(adapter_id: String) -> InitializeRequestArguments {
         supports_memory_event: Some(false),
         supports_args_can_be_interpreted_by_shell: Some(false),
         supports_start_debugging_request: Some(true),
-        supports_ansistyling: Some(false),
+        supports_ansistyling: Some(true),
     }
 }
 

crates/search/src/project_search.rs πŸ”—

@@ -1377,7 +1377,7 @@ impl ProjectSearchView {
                 }
                 editor.highlight_background::<Self>(
                     &match_ranges,
-                    |theme| theme.search_match_background,
+                    |theme| theme.colors().search_match_background,
                     cx,
                 );
             });

crates/vim/src/normal/yank.rs πŸ”—

@@ -222,7 +222,7 @@ impl Vim {
 
         editor.highlight_background::<HighlightOnYank>(
             &ranges_to_highlight,
-            |colors| colors.editor_document_highlight_read_background,
+            |theme| theme.colors().editor_document_highlight_read_background,
             cx,
         );
         cx.spawn(async move |this, cx| {

crates/vim/src/replace.rs πŸ”—

@@ -217,8 +217,8 @@ impl Vim {
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
-        if let Some((_, ranges)) = editor.clear_background_highlights::<VimExchange>(cx) {
-            let previous_range = ranges[0].clone();
+        if let Some(highlights) = editor.clear_background_highlights::<VimExchange>(cx) {
+            let previous_range = highlights[0].range.clone();
 
             let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot);
             let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot);
@@ -261,7 +261,7 @@ impl Vim {
             let ranges = [new_range];
             editor.highlight_background::<VimExchange>(
                 &ranges,
-                |theme| theme.editor_document_highlight_read_background,
+                |theme| theme.colors().editor_document_highlight_read_background,
                 cx,
             );
         }

crates/vim/src/test.rs πŸ”—

@@ -864,16 +864,13 @@ async fn test_jk(cx: &mut gpui::TestAppContext) {
 fn assert_pending_input(cx: &mut VimTestContext, expected: &str) {
     cx.update_editor(|editor, window, cx| {
         let snapshot = editor.snapshot(window, cx);
-        let highlights = editor
-            .text_highlights::<editor::PendingInput>(cx)
-            .unwrap()
-            .1;
+        let highlights = editor.text_highlights::<editor::PendingInput>(cx).unwrap();
         let (_, ranges) = marked_text_ranges(expected, false);
 
         assert_eq!(
             highlights
                 .iter()
-                .map(|highlight| highlight.to_offset(&snapshot.buffer_snapshot))
+                .map(|(highlight, _)| highlight.to_offset(&snapshot.buffer_snapshot))
                 .collect::<Vec<_>>(),
             ranges
         )
@@ -923,15 +920,12 @@ async fn test_jk_delay(cx: &mut gpui::TestAppContext) {
     cx.assert_state("Λ‡jhello", Mode::Insert);
     cx.update_editor(|editor, window, cx| {
         let snapshot = editor.snapshot(window, cx);
-        let highlights = editor
-            .text_highlights::<editor::PendingInput>(cx)
-            .unwrap()
-            .1;
+        let highlights = editor.text_highlights::<editor::PendingInput>(cx).unwrap();
 
         assert_eq!(
             highlights
                 .iter()
-                .map(|highlight| highlight.to_offset(&snapshot.buffer_snapshot))
+                .map(|(highlight, _)| highlight.to_offset(&snapshot.buffer_snapshot))
                 .collect::<Vec<_>>(),
             vec![0..1]
         )