debugger: Parse and highlight text with ANSI escape sequences (#32915)

Relanding #32817 with an improved approach, bugs fixed, and a test.

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2025-06-17 23:39:31 -04:00 committed by GitHub
parent 4da58188fb
commit bfffc293a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 606 additions and 80 deletions

1
Cargo.lock generated
View file

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

View file

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

View file

@ -2,13 +2,15 @@ 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 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 +19,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 {
@ -136,18 +138,193 @@ impl Console {
cx: &mut App,
) {
self.console.update(cx, |console, cx| {
let mut to_insert = String::default();
for event in events {
use std::fmt::Write;
console.set_read_only(false);
_ = write!(to_insert, "{}\n", event.output.trim_end());
for event in events {
let to_insert = format!("{}\n", event.output.trim_end());
let mut ansi_handler = ConsoleHandler::default();
let mut ansi_processor = ansi::Processor::<ansi::StdSyncHandler>::default();
let len = console.buffer().read(cx).len(cx);
ansi_processor.advance(&mut ansi_handler, to_insert.as_bytes());
let output = std::mem::take(&mut ansi_handler.output);
let mut spans = std::mem::take(&mut ansi_handler.spans);
let mut background_spans = std::mem::take(&mut ansi_handler.background_spans);
if ansi_handler.current_range_start < output.len() {
spans.push((
ansi_handler.current_range_start..output.len(),
ansi_handler.current_color,
));
}
if ansi_handler.current_background_range_start < output.len() {
background_spans.push((
ansi_handler.current_background_range_start..output.len(),
ansi_handler.current_background_color,
));
}
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
console.insert(&output, window, cx);
let buffer = console.buffer().read(cx).snapshot(cx);
struct ConsoleAnsiHighlight;
for (range, color) in spans {
let Some(color) = color else { continue };
let start_offset = len + range.start;
let range = start_offset..len + range.end;
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()
};
console.highlight_text_key::<ConsoleAnsiHighlight>(
start_offset,
vec![range],
style,
cx,
);
}
for (range, color) in background_spans {
let Some(color) = color else { continue };
let start_offset = len + range.start;
let range = start_offset..len + range.end;
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(),
}
}
};
console.highlight_background_key::<ConsoleAnsiHighlight>(
start_offset,
&[range],
color_fetcher,
cx,
);
}
}
console.set_read_only(false);
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
console.insert(&to_insert, window, cx);
console.set_read_only(true);
cx.notify();
});
}
@ -459,3 +636,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 += c.len_utf8();
}
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);
}
_ => {}
}
}
}

View file

@ -3,6 +3,7 @@ use crate::{
*,
};
use dap::requests::StackTrace;
use editor::{DisplayPoint, display_map::DisplayRow};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
@ -110,7 +111,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 +125,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,13 +151,209 @@ 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()
);
})
.unwrap();
}
#[gpui::test]
async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.read_with(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<StackTrace, _>(move |_, _| {
Ok(dap::StackTraceResponse {
stack_frames: Vec::default(),
total_frames: None,
})
});
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: "Checking latest version of JavaScript...".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: " \u{1b}[1m\u{1b}[38;2;173;127;168m▲ Next.js 15.1.5\u{1b}[39m\u{1b}[22m"
.to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: " - Local: http://localhost:3000\n - Network: http://192.168.1.144:3000\n\n \u{1b}[32m\u{1b}[1m✓\u{1b}[22m\u{1b}[39m Starting..."
.to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
// [crates/debugger_ui/src/session/running/console.rs:147:9] &to_insert = "Could not read source map for file:///Users/cole/roles-at/node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/typescript.js: ENOENT: no such file or directory, open '/Users/cole/roles-at/node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/typescript.js.map'\n"
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: "Something else...".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: " \u{1b}[32m\u{1b}[1m✓\u{1b}[22m\u{1b}[39m Ready in 1009ms\n".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
// introduce some background highlight
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: "\u{1b}[41m\u{1b}[37mBoth background and foreground!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
// another random line
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: "Even more...".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
cx.run_until_parked();
let _running_state =
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
cx.focus_self(window);
item.running_state().clone()
});
cx.run_until_parked();
workspace
.update(cx, |workspace, window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_debug_session_panel = debug_panel
.update(cx, |this, _| this.active_session())
.unwrap();
let editor =
active_debug_session_panel
.read(cx)
.running_state()
.read(cx)
.console()
.read(cx)
.editor().clone();
assert_eq!(
"Checking latest version of JavaScript...\n ▲ Next.js 15.1.5\n - Local: http://localhost:3000\n - Network: http://192.168.1.144:3000\n\n ✓ Starting...\nSomething else...\n ✓ Ready in 1009ms\nBoth background and foreground!\nEven more...\n",
editor
.read(cx)
.text(cx)
.as_str()
);
let text_highlights = editor.update(cx, |editor, cx| {
let mut text_highlights = editor.all_text_highlights(window, cx).into_iter().flat_map(|(_, ranges)| ranges).collect::<Vec<_>>();
text_highlights.sort_by(|a, b| a.start.cmp(&b.start));
text_highlights
});
pretty_assertions::assert_eq!(
text_highlights,
[
DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 21),
DisplayPoint::new(DisplayRow(1), 21)..DisplayPoint::new(DisplayRow(2), 0),
DisplayPoint::new(DisplayRow(5), 1)..DisplayPoint::new(DisplayRow(5), 4),
DisplayPoint::new(DisplayRow(5), 4)..DisplayPoint::new(DisplayRow(6), 0),
DisplayPoint::new(DisplayRow(7), 1)..DisplayPoint::new(DisplayRow(7), 4),
DisplayPoint::new(DisplayRow(7), 4)..DisplayPoint::new(DisplayRow(8), 0),
DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(9), 0),
]
);
let background_highlights = editor.update(cx, |editor, cx| {
editor.all_text_background_highlights(window, cx).into_iter().map(|(range, _)| range).collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(
background_highlights,
[
DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(9), 0),
]
)
})
.unwrap();
}
// #[gpui::test]
// async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);

View file

@ -76,11 +76,17 @@ pub enum FoldStatus {
Foldable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum HighlightKey {
Type(TypeId),
TypePlus(TypeId, usize),
}
pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
type TextHighlights = TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
type TextHighlights = TreeMap<HighlightKey, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
type InlayHighlights = TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>>;
/// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints,
@ -473,12 +479,11 @@ impl DisplayMap {
pub fn highlight_text(
&mut self,
type_id: TypeId,
key: HighlightKey,
ranges: Vec<Range<Anchor>>,
style: HighlightStyle,
) {
self.text_highlights
.insert(type_id, Arc::new((style, ranges)));
self.text_highlights.insert(key, Arc::new((style, ranges)));
}
pub(crate) fn highlight_inlays(
@ -501,11 +506,22 @@ impl DisplayMap {
}
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
let highlights = self.text_highlights.get(&type_id)?;
let highlights = self.text_highlights.get(&HighlightKey::Type(type_id))?;
Some((highlights.0, &highlights.1))
}
#[cfg(feature = "test-support")]
pub fn all_text_highlights(
&self,
) -> impl Iterator<Item = &Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
self.text_highlights.values()
}
pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
let mut cleared = self.text_highlights.remove(&type_id).is_some();
let mut cleared = self
.text_highlights
.remove(&HighlightKey::Type(type_id))
.is_some();
cleared |= self.inlay_highlights.remove(&type_id).is_some();
cleared
}
@ -1333,7 +1349,9 @@ impl DisplaySnapshot {
&self,
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
let type_id = TypeId::of::<Tag>();
self.text_highlights.get(&type_id).cloned()
self.text_highlights
.get(&HighlightKey::Type(type_id))
.cloned()
}
#[allow(unused)]
@ -2294,7 +2312,7 @@ pub mod tests {
// Insert a block in the middle of a multi-line diagnostic.
map.update(cx, |map, cx| {
map.highlight_text(
TypeId::of::<usize>(),
HighlightKey::Type(TypeId::of::<usize>()),
vec![
buffer_snapshot.anchor_before(Point::new(3, 9))
..buffer_snapshot.anchor_after(Point::new(3, 14)),
@ -2616,7 +2634,7 @@ pub mod tests {
map.update(cx, |map, _cx| {
map.highlight_text(
TypeId::of::<MyType>(),
HighlightKey::Type(TypeId::of::<MyType>()),
highlighted_ranges
.into_iter()
.map(|range| {

View file

@ -1,16 +1,15 @@
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::{HighlightKey, TextHighlights};
pub struct CustomHighlightsChunks<'a> {
buffer_chunks: MultiBufferChunks<'a>,
@ -19,15 +18,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<HighlightKey, HighlightStyle>,
text_highlights: Option<&'a TextHighlights>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct HighlightEndpoint {
offset: usize,
is_start: bool,
tag: TypeId,
tag: HighlightKey,
style: HighlightStyle,
}
@ -35,7 +34,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 +65,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();

View file

@ -1115,7 +1115,7 @@ mod tests {
use super::*;
use crate::{
InlayId, MultiBuffer,
display_map::{InlayHighlights, TextHighlights},
display_map::{HighlightKey, InlayHighlights, TextHighlights},
hover_links::InlayHighlight,
};
use gpui::{App, HighlightStyle};
@ -1629,7 +1629,7 @@ mod tests {
text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
log::info!("highlighting text ranges {text_highlight_ranges:?}");
text_highlights.insert(
TypeId::of::<()>(),
HighlightKey::Type(TypeId::of::<()>()),
Arc::new((
HighlightStyle::default(),
text_highlight_ranges

View file

@ -197,7 +197,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::{
@ -708,7 +708,7 @@ 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>]>);
type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
#[derive(Default)]
@ -1017,7 +1017,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<HighlightKey, BackgroundHighlight>,
gutter_highlights: TreeMap<TypeId, GutterHighlight>,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
@ -6180,7 +6180,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,
);
});
@ -6535,12 +6535,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();
@ -6642,7 +6642,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,
)
}
@ -15397,7 +15397,7 @@ impl Editor {
}
editor.highlight_background::<Self>(
&ranges,
|theme| theme.editor_highlighted_line_background,
|theme| theme.colors().editor_highlighted_line_background,
cx,
);
}
@ -18552,7 +18552,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,
|colors| colors.colors().editor_document_highlight_read_background,
cx,
)
}
@ -18568,11 +18568,28 @@ 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>,
) {
self.background_highlights
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
self.background_highlights.insert(
HighlightKey::Type(TypeId::of::<T>()),
(color_fetcher, Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
}
pub fn highlight_background_key<T: 'static>(
&mut self,
key: usize,
ranges: &[Range<Anchor>],
color_fetcher: fn(&Theme) -> Hsla,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
(color_fetcher, Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
}
@ -18581,7 +18598,9 @@ impl Editor {
&mut self,
cx: &mut Context<Self>,
) -> Option<BackgroundHighlight> {
let text_highlights = self.background_highlights.remove(&TypeId::of::<T>())?;
let text_highlights = self
.background_highlights
.remove(&HighlightKey::Type(TypeId::of::<T>()))?;
if !text_highlights.1.is_empty() {
self.scrollbar_marker_state.dirty = true;
cx.notify();
@ -18667,6 +18686,30 @@ impl Editor {
.insert(TypeId::of::<T>(), (color_fetcher, gutter_highlights));
}
#[cfg(feature = "test-support")]
pub fn all_text_highlights(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<(HighlightStyle, Vec<Range<DisplayPoint>>)> {
let snapshot = self.snapshot(window, cx);
self.display_map.update(cx, |display_map, _| {
display_map
.all_text_highlights()
.map(|highlight| {
let (style, ranges) = highlight.as_ref();
(
*style,
ranges
.iter()
.map(|range| range.clone().to_display_points(&snapshot))
.collect(),
)
})
.collect()
})
}
#[cfg(feature = "test-support")]
pub fn all_text_background_highlights(
&self,
@ -18677,8 +18720,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();
self.background_highlights_in_range(start..end, &snapshot, theme)
self.background_highlights_in_range(start..end, &snapshot, cx.theme())
}
#[cfg(feature = "test-support")]
@ -18687,7 +18729,9 @@ impl Editor {
let highlights = self
.background_highlights
.get(&TypeId::of::<items::BufferSearchHighlights>());
.get(&HighlightKey::Type(TypeId::of::<
items::BufferSearchHighlights,
>()));
if let Some((_color, ranges)) = highlights {
ranges
@ -18706,11 +18750,11 @@ impl Editor {
) -> impl 'a + Iterator<Item = &'a Range<Anchor>> {
let read_highlights = self
.background_highlights
.get(&TypeId::of::<DocumentHighlightRead>())
.get(&HighlightKey::Type(TypeId::of::<DocumentHighlightRead>()))
.map(|h| &h.1);
let write_highlights = self
.background_highlights
.get(&TypeId::of::<DocumentHighlightWrite>())
.get(&HighlightKey::Type(TypeId::of::<DocumentHighlightWrite>()))
.map(|h| &h.1);
let left_position = position.bias_left(buffer);
let right_position = position.bias_right(buffer);
@ -18737,7 +18781,7 @@ impl Editor {
pub fn has_background_highlights<T: 'static>(&self) -> bool {
self.background_highlights
.get(&TypeId::of::<T>())
.get(&HighlightKey::Type(TypeId::of::<T>()))
.map_or(false, |(_, highlights)| !highlights.is_empty())
}
@ -18745,7 +18789,7 @@ impl Editor {
&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() {
@ -18786,7 +18830,10 @@ 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((_, ranges)) = self
.background_highlights
.get(&HighlightKey::Type(TypeId::of::<T>()))
else {
return vec![];
};
@ -18923,6 +18970,23 @@ impl Editor {
.collect()
}
pub fn highlight_text_key<T: 'static>(
&mut self,
key: usize,
ranges: Vec<Range<Anchor>>,
style: HighlightStyle,
cx: &mut Context<Self>,
) {
self.display_map.update(cx, |map, _| {
map.highlight_text(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
ranges,
style,
);
});
cx.notify();
}
pub fn highlight_text<T: 'static>(
&mut self,
ranges: Vec<Range<Anchor>>,
@ -18930,7 +18994,7 @@ impl Editor {
cx: &mut Context<Self>,
) {
self.display_map.update(cx, |map, _| {
map.highlight_text(TypeId::of::<T>(), ranges, style)
map.highlight_text(HighlightKey::Type(TypeId::of::<T>()), ranges, style)
});
cx.notify();
}

View file

@ -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),
@ -20334,7 +20334,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,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@ -20412,7 +20412,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,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});

View file

@ -12,8 +12,8 @@ use crate::{
ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk,
ToDisplayPoint,
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightKey,
HighlightedChunk, ToDisplayPoint,
},
editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, MinimapThumb,
@ -6166,13 +6166,15 @@ impl EditorElement {
background_highlights.iter()
{
let is_search_highlights = *background_highlight_id
== TypeId::of::<BufferSearchHighlights>();
== HighlightKey::Type(TypeId::of::<BufferSearchHighlights>());
let is_text_highlights = *background_highlight_id
== TypeId::of::<SelectedTextHighlight>();
== HighlightKey::Type(TypeId::of::<SelectedTextHighlight>());
let is_symbol_occurrences = *background_highlight_id
== TypeId::of::<DocumentHighlightRead>()
== HighlightKey::Type(TypeId::of::<DocumentHighlightRead>())
|| *background_highlight_id
== TypeId::of::<DocumentHighlightWrite>();
== HighlightKey::Type(
TypeId::of::<DocumentHighlightWrite>(),
);
if (is_search_highlights && scrollbar_settings.search_results)
|| (is_text_highlights && scrollbar_settings.selected_text)
|| (is_symbol_occurrences && scrollbar_settings.selected_symbol)
@ -8091,7 +8093,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();

View file

@ -40,7 +40,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,
)
}

View file

@ -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,
);
}

View file

@ -2,6 +2,7 @@ use crate::{
Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects,
ToPoint as _,
display_map::HighlightKey,
editor_settings::SeedQuerySetting,
persistence::{DB, SerializedEditor},
scroll::ScrollAnchor,
@ -1431,7 +1432,7 @@ impl SearchableItem for Editor {
fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec<Range<Anchor>> {
self.background_highlights
.get(&TypeId::of::<BufferSearchHighlights>())
.get(&HighlightKey::Type(TypeId::of::<BufferSearchHighlights>()))
.map_or(Vec::new(), |(_color, ranges)| {
ranges.iter().cloned().collect()
})
@ -1454,12 +1455,12 @@ impl SearchableItem for Editor {
) {
let existing_range = self
.background_highlights
.get(&TypeId::of::<BufferSearchHighlights>())
.get(&HighlightKey::Type(TypeId::of::<BufferSearchHighlights>()))
.map(|(_, range)| range.as_ref());
let updated = existing_range != Some(matches);
self.highlight_background::<BufferSearchHighlights>(
matches,
|theme| theme.search_match_background,
|theme| theme.colors().search_match_background,
cx,
);
if updated {
@ -1701,7 +1702,7 @@ impl SearchableItem for Editor {
let buffer = self.buffer().read(cx).snapshot(cx);
let search_within_ranges = self
.background_highlights
.get(&TypeId::of::<SearchWithinRange>())
.get(&HighlightKey::Type(TypeId::of::<SearchWithinRange>()))
.map_or(vec![], |(_color, ranges)| {
ranges.iter().cloned().collect::<Vec<_>>()
});

View file

@ -1,6 +1,6 @@
use crate::{
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, RowExt,
display_map::ToDisplayPoint,
display_map::{HighlightKey, ToDisplayPoint},
};
use buffer_diff::DiffHunkStatusKind;
use collections::BTreeMap;
@ -509,7 +509,7 @@ impl EditorTestContext {
let snapshot = editor.snapshot(window, cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.get(&HighlightKey::Type(TypeId::of::<Tag>()))
.map(|h| h.1.clone())
.unwrap_or_default()
.iter()

View file

@ -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,
);
});

View file

@ -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),
}
}

View file

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

View file

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

View file

@ -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,
);
}