Support bracket colorization (rainbow brackets) (#43172)

Deals with https://github.com/zed-industries/zed/issues/5259

Highlights brackets with different colors based on their depth.
Uses existing tree-sitter queries from brackets.scm to find brackets,
uses theme's accents to color them.


https://github.com/user-attachments/assets/cc5f3aba-22fa-446d-9af7-ba6e772029da

1. Adds `colorize_brackets` language setting that allows, per language
or globally for all languages, to configure whether Zed should color the
brackets for a particular language.

Disabled for all languages by default.

2. Any given language can opt-out a certain bracket pair by amending the
brackets.scm like `("\"" @open "\"" @close) ` -> `(("\"" @open "\""
@close) (#set! rainbow.exclude))`

3. Brackets are using colors from theme accents, which can be overridden
as

```jsonc
"theme_overrides": {
  "One Dark": {
    "accents": ["#ff69b4", "#7fff00", "#ff1493", "#00ffff", "#ff8c00", "#9400d3"]
  }
},
```

Release Notes:

- Added bracket colorization (rainbow brackets) support. Use
`colorize_brackets` language setting to enable.

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
Co-authored-by: MrSubidubi <finn@zed.dev>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
Kirill Bulatov
2025-11-20 19:47:39 +00:00
committed by GitHub
co-authored by MrSubidubi Lukas Wirth MrSubidubi Lukas Wirth Smit Barmase
parent e6e5ccbf10
commit 7e341bcf94
46 changed files with 1988 additions and 252 deletions
File diff suppressed because it is too large Load Diff
+60 -2
View File
@@ -483,8 +483,26 @@ impl DisplayMap {
key: HighlightKey,
ranges: Vec<Range<Anchor>>,
style: HighlightStyle,
merge: bool,
cx: &App,
) {
self.text_highlights.insert(key, Arc::new((style, ranges)));
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let to_insert = match self.text_highlights.remove(&key).filter(|_| merge) {
Some(previous) => {
let mut merged_ranges = previous.1.clone();
for new_range in ranges {
let i = merged_ranges
.binary_search_by(|probe| {
probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
})
.unwrap_or_else(|i| i);
merged_ranges.insert(i, new_range);
}
Arc::new((style, merged_ranges))
}
None => Arc::new((style, ranges)),
};
self.text_highlights.insert(key, to_insert);
}
pub(crate) fn highlight_inlays(
@@ -523,6 +541,15 @@ impl DisplayMap {
.text_highlights
.remove(&HighlightKey::Type(type_id))
.is_some();
self.text_highlights.retain(|key, _| {
let retain = if let HighlightKey::TypePlus(key_type_id, _) = key {
key_type_id != &type_id
} else {
true
};
cleared |= !retain;
retain
});
cleared |= self.inlay_highlights.remove(&type_id).is_some();
cleared
}
@@ -1382,6 +1409,33 @@ impl DisplaySnapshot {
.cloned()
}
#[cfg(any(test, feature = "test-support"))]
pub fn all_text_highlight_ranges<Tag: ?Sized + 'static>(
&self,
) -> Vec<(gpui::Hsla, Range<Point>)> {
use itertools::Itertools;
let required_type_id = TypeId::of::<Tag>();
self.text_highlights
.iter()
.filter(|(key, _)| match key {
HighlightKey::Type(type_id) => type_id == &required_type_id,
HighlightKey::TypePlus(type_id, _) => type_id == &required_type_id,
})
.map(|(_, value)| value.clone())
.flat_map(|ranges| {
ranges
.1
.iter()
.flat_map(|range| {
Some((ranges.0.color?, range.to_point(self.buffer_snapshot())))
})
.collect::<Vec<_>>()
})
.sorted_by_key(|(_, range)| range.start)
.collect()
}
#[allow(unused)]
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn inlay_highlights<Tag: ?Sized + 'static>(
@@ -2387,6 +2441,8 @@ pub mod tests {
..buffer_snapshot.anchor_after(Point::new(3, 18)),
],
red.into(),
false,
cx,
);
map.insert_blocks(
[BlockProperties {
@@ -2698,7 +2754,7 @@ pub mod tests {
..Default::default()
};
map.update(cx, |map, _cx| {
map.update(cx, |map, cx| {
map.highlight_text(
HighlightKey::Type(TypeId::of::<MyType>()),
highlighted_ranges
@@ -2710,6 +2766,8 @@ pub mod tests {
})
.collect(),
style,
false,
cx,
);
});
@@ -40,7 +40,6 @@ impl<'a> CustomHighlightsChunks<'a> {
buffer_chunks: multibuffer_snapshot.chunks(range.clone(), language_aware),
buffer_chunk: None,
offset: range.start,
text_highlights,
highlight_endpoints: create_highlight_endpoints(
&range,
@@ -75,16 +74,9 @@ fn create_highlight_endpoints(
let style = text_highlights.0;
let ranges = &text_highlights.1;
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe.end.cmp(&start, buffer);
if cmp.is_gt() {
cmp::Ordering::Greater
} else {
cmp::Ordering::Less
}
}) {
Ok(i) | Err(i) => i,
};
let start_ix = ranges
.binary_search_by(|probe| probe.end.cmp(&start, buffer).then(cmp::Ordering::Less))
.unwrap_or_else(|i| i);
for range in &ranges[start_ix..] {
if range.start.cmp(&end, buffer).is_ge() {
+92 -16
View File
@@ -13,6 +13,7 @@
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior.
pub mod actions;
mod blink_manager;
mod bracket_colorization;
mod clangd_ext;
pub mod code_context_menus;
pub mod display_map;
@@ -118,11 +119,11 @@ use language::{
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
IndentSize, Language, LanguageRegistry, OffsetRangeExt, OutlineItem, Point, Runnable,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
IndentSize, Language, LanguageName, LanguageRegistry, OffsetRangeExt, OutlineItem, Point,
Runnable, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
language_settings,
self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
},
point_from_lsp, point_to_lsp, text_diff_with_options,
};
@@ -175,6 +176,7 @@ use std::{
borrow::Cow,
cell::{OnceCell, RefCell},
cmp::{self, Ordering, Reverse},
collections::hash_map,
iter::{self, Peekable},
mem,
num::NonZeroU32,
@@ -1193,6 +1195,9 @@ pub struct Editor {
folding_newlines: Task<()>,
select_next_is_case_sensitive: Option<bool>,
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
accent_overrides: Vec<SharedString>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
}
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
@@ -2333,12 +2338,18 @@ impl Editor {
folding_newlines: Task::ready(()),
lookup_key: None,
select_next_is_case_sensitive: None,
applicable_language_settings: HashMap::default(),
accent_overrides: Vec::new(),
fetched_tree_sitter_chunks: HashMap::default(),
};
if is_minimap {
return editor;
}
editor.applicable_language_settings = editor.fetch_applicable_language_settings(cx);
editor.accent_overrides = editor.fetch_accent_overrides(cx);
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
._subscriptions
@@ -2378,6 +2389,7 @@ impl Editor {
InlayHintRefreshReason::NewLinesShown,
cx,
);
editor.colorize_brackets(false, cx);
})
.ok();
});
@@ -21141,13 +21153,16 @@ impl Editor {
key: usize,
ranges: Vec<Range<Anchor>>,
style: HighlightStyle,
merge: bool,
cx: &mut Context<Self>,
) {
self.display_map.update(cx, |map, _| {
self.display_map.update(cx, |map, cx| {
map.highlight_text(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
ranges,
style,
merge,
cx,
);
});
cx.notify();
@@ -21159,8 +21174,14 @@ impl Editor {
style: HighlightStyle,
cx: &mut Context<Self>,
) {
self.display_map.update(cx, |map, _| {
map.highlight_text(HighlightKey::Type(TypeId::of::<T>()), ranges, style)
self.display_map.update(cx, |map, cx| {
map.highlight_text(
HighlightKey::Type(TypeId::of::<T>()),
ranges,
style,
false,
cx,
)
});
cx.notify();
}
@@ -21308,7 +21329,6 @@ impl Editor {
self.active_indent_guides_state.dirty = true;
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(window, cx);
self.refresh_selected_text_highlights(true, window, cx);
self.refresh_single_line_folds(window, cx);
self.refresh_matching_bracket_highlights(window, cx);
if self.has_active_edit_prediction() {
@@ -21364,6 +21384,7 @@ impl Editor {
}
self.update_lsp_data(Some(buffer_id), window, cx);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
self.colorize_brackets(false, cx);
cx.emit(EditorEvent::ExcerptsAdded {
buffer: buffer.clone(),
predecessor: *predecessor,
@@ -21401,10 +21422,16 @@ impl Editor {
multi_buffer::Event::ExcerptsExpanded { ids } => {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
self.refresh_document_highlights(cx);
for id in ids {
self.fetched_tree_sitter_chunks.remove(id);
}
self.colorize_brackets(false, cx);
cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
}
multi_buffer::Event::Reparsed(buffer_id) => {
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
self.refresh_selected_text_highlights(true, window, cx);
self.colorize_brackets(true, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
cx.emit(EditorEvent::Reparsed(*buffer_id));
@@ -21475,7 +21502,52 @@ impl Editor {
cx.notify();
}
fn fetch_accent_overrides(&self, cx: &App) -> Vec<SharedString> {
if !self.mode.is_full() {
return Vec::new();
}
theme::ThemeSettings::get_global(cx)
.theme_overrides
.get(cx.theme().name.as_ref())
.map(|theme_style| &theme_style.accents)
.into_iter()
.flatten()
.flat_map(|accent| accent.0.clone())
.collect()
}
fn fetch_applicable_language_settings(
&self,
cx: &App,
) -> HashMap<Option<LanguageName>, LanguageSettings> {
if !self.mode.is_full() {
return HashMap::default();
}
self.buffer().read(cx).all_buffers().into_iter().fold(
HashMap::default(),
|mut acc, buffer| {
let buffer = buffer.read(cx);
let language = buffer.language().map(|language| language.name());
if let hash_map::Entry::Vacant(v) = acc.entry(language.clone()) {
let file = buffer.file();
v.insert(language_settings(language, file, cx).into_owned());
}
acc
},
)
}
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let new_language_settings = self.fetch_applicable_language_settings(cx);
let language_settings_changed = new_language_settings != self.applicable_language_settings;
self.applicable_language_settings = new_language_settings;
let new_accent_overrides = self.fetch_accent_overrides(cx);
let accent_overrides_changed = new_accent_overrides != self.accent_overrides;
self.accent_overrides = new_accent_overrides;
if self.diagnostics_enabled() {
let new_severity = EditorSettings::get_global(cx)
.diagnostics_max_severity
@@ -21547,15 +21619,19 @@ impl Editor {
})
}
}
}
if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
}) {
if !inlay_splice.is_empty() {
self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
if language_settings_changed || accent_overrides_changed {
self.colorize_brackets(true, cx);
}
if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
}) {
if !inlay_splice.is_empty() {
self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
}
self.refresh_colors_for_visible_range(None, window, cx);
}
self.refresh_colors_for_visible_range(None, window, cx);
}
cx.notify();
@@ -22668,7 +22744,7 @@ fn insert_extra_newline_tree_sitter(
_ => return false,
};
let pair = {
let mut result: Option<BracketMatch> = None;
let mut result: Option<BracketMatch<usize>> = None;
for pair in buffer
.all_bracket_ranges(range.start.0..range.end.0)
+14 -8
View File
@@ -17565,7 +17565,9 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
let mut assert = |before, after| {
#[track_caller]
fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
let _state_context = cx.set_state(before);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -17573,30 +17575,33 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
});
cx.run_until_parked();
cx.assert_editor_state(after);
};
}
// Outside bracket jumps to outside of matching bracket
assert("console.logˇ(var);", "console.log(var)ˇ;");
assert("console.log(var)ˇ;", "console.logˇ(var);");
assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
// Inside bracket jumps to inside of matching bracket
assert("console.log(ˇvar);", "console.log(varˇ);");
assert("console.log(varˇ);", "console.log(ˇvar);");
assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
// When outside a bracket and inside, favor jumping to the inside bracket
assert(
"console.log('foo', [1, 2, 3]ˇ);",
"console.log(ˇ'foo', [1, 2, 3]);",
"console.log('foo', ˇ[1, 2, 3]);",
&mut cx,
);
assert(
"console.log(ˇ'foo', [1, 2, 3]);",
"console.log('foo', [1, 2, 3]ˇ);",
"console.log('foo'ˇ, [1, 2, 3]);",
&mut cx,
);
// Bias forward if two options are equally likely
assert(
"let result = curried_fun()ˇ();",
"let result = curried_fun()()ˇ;",
&mut cx,
);
// If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
@@ -17609,6 +17614,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
function test() {
console.logˇ('test')
}"},
&mut cx,
);
}
+1
View File
@@ -500,6 +500,7 @@ impl Editor {
editor.register_visible_buffers(cx);
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
editor.update_lsp_data(None, window, cx);
editor.colorize_brackets(false, cx);
})
.ok();
});