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

- [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.
This commit is contained in:
Cole Miller 2025-06-16 17:39:53 -04:00 committed by GitHub
parent 1f457169ba
commit ffc6218349
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 558 additions and 234 deletions

View file

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