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
+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)