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
+212 -57
View File
@@ -1,9 +1,12 @@
pub mod row_chunk;
use crate::{
DebuggerTextObject, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
TextObject, TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
language_settings::{LanguageSettings, language_settings},
outline::OutlineItem,
row_chunk::RowChunks,
syntax_map::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatch,
SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
@@ -18,9 +21,9 @@ pub use crate::{
proto,
};
use anyhow::{Context as _, Result};
use clock::Lamport;
pub use clock::ReplicaId;
use collections::HashMap;
use clock::{Global, Lamport};
use collections::{HashMap, HashSet};
use fs::MTime;
use futures::channel::oneshot;
use gpui::{
@@ -28,8 +31,9 @@ use gpui::{
Task, TaskLabel, TextStyle,
};
use itertools::Itertools;
use lsp::{LanguageServerId, NumberOrString};
use parking_lot::Mutex;
use parking_lot::{Mutex, RawMutex, lock_api::MutexGuard};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use settings::WorktreeId;
@@ -45,7 +49,7 @@ use std::{
iter::{self, Iterator, Peekable},
mem,
num::NonZeroU32,
ops::{Deref, Range},
ops::{Deref, Not, Range},
path::PathBuf,
rc,
sync::{Arc, LazyLock},
@@ -126,6 +130,29 @@ pub struct Buffer {
has_unsaved_edits: Cell<(clock::Global, bool)>,
change_bits: Vec<rc::Weak<Cell<bool>>>,
_subscriptions: Vec<gpui::Subscription>,
tree_sitter_data: Arc<Mutex<TreeSitterData>>,
}
#[derive(Debug, Clone)]
pub struct TreeSitterData {
chunks: RowChunks,
brackets_by_chunks: Vec<Option<Vec<BracketMatch<usize>>>>,
}
const MAX_ROWS_IN_A_CHUNK: u32 = 50;
impl TreeSitterData {
fn clear(&mut self) {
self.brackets_by_chunks = vec![None; self.chunks.len()];
}
fn new(snapshot: text::BufferSnapshot) -> Self {
let chunks = RowChunks::new(snapshot, MAX_ROWS_IN_A_CHUNK);
Self {
brackets_by_chunks: vec![None; chunks.len()],
chunks,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -149,6 +176,7 @@ pub struct BufferSnapshot {
remote_selections: TreeMap<ReplicaId, SelectionSet>,
language: Option<Arc<Language>>,
non_text_state_update_count: usize,
tree_sitter_data: Arc<Mutex<TreeSitterData>>,
}
/// The kind and amount of indentation in a particular line. For now,
@@ -819,11 +847,18 @@ impl EditPreview {
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BracketMatch {
pub open_range: Range<usize>,
pub close_range: Range<usize>,
pub struct BracketMatch<T> {
pub open_range: Range<T>,
pub close_range: Range<T>,
pub newline_only: bool,
pub depth: usize,
pub syntax_layer_depth: usize,
pub color_index: Option<usize>,
}
impl<T> BracketMatch<T> {
pub fn bracket_ranges(self) -> (Range<T>, Range<T>) {
(self.open_range, self.close_range)
}
}
impl Buffer {
@@ -974,8 +1009,10 @@ impl Buffer {
let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime());
let snapshot = buffer.snapshot();
let syntax_map = Mutex::new(SyntaxMap::new(&snapshot));
let tree_sitter_data = TreeSitterData::new(snapshot);
Self {
saved_mtime,
tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
saved_version: buffer.version(),
preview_version: buffer.version(),
reload_task: None,
@@ -1025,12 +1062,14 @@ impl Buffer {
let language_registry = language_registry.clone();
syntax.reparse(&text, language_registry, language);
}
let tree_sitter_data = TreeSitterData::new(text.clone());
BufferSnapshot {
text,
syntax,
file: None,
diagnostics: Default::default(),
remote_selections: Default::default(),
tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
language,
non_text_state_update_count: 0,
}
@@ -1048,9 +1087,11 @@ impl Buffer {
)
.snapshot();
let syntax = SyntaxMap::new(&text).snapshot();
let tree_sitter_data = TreeSitterData::new(text.clone());
BufferSnapshot {
text,
syntax,
tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
file: None,
diagnostics: Default::default(),
remote_selections: Default::default(),
@@ -1075,9 +1116,11 @@ impl Buffer {
if let Some(language) = language.clone() {
syntax.reparse(&text, language_registry, language);
}
let tree_sitter_data = TreeSitterData::new(text.clone());
BufferSnapshot {
text,
syntax,
tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
file: None,
diagnostics: Default::default(),
remote_selections: Default::default(),
@@ -1097,6 +1140,7 @@ impl Buffer {
BufferSnapshot {
text,
syntax,
tree_sitter_data: self.tree_sitter_data.clone(),
file: self.file.clone(),
remote_selections: self.remote_selections.clone(),
diagnostics: self.diagnostics.clone(),
@@ -1611,6 +1655,7 @@ impl Buffer {
self.syntax_map.lock().did_parse(syntax_snapshot);
self.request_autoindent(cx);
self.parse_status.0.send(ParseStatus::Idle).unwrap();
self.tree_sitter_data.lock().clear();
cx.emit(BufferEvent::Reparsed);
cx.notify();
}
@@ -4120,61 +4165,166 @@ impl BufferSnapshot {
self.syntax.matches(range, self, query)
}
/// Finds all [`RowChunks`] applicable to the given range, then returns all bracket pairs that intersect with those chunks.
/// Hence, may return more bracket pairs than the range contains.
///
/// Will omit known chunks.
/// The resulting bracket match collections are not ordered.
pub fn fetch_bracket_ranges(
&self,
range: Range<usize>,
known_chunks: Option<(&Global, &HashSet<Range<BufferRow>>)>,
) -> HashMap<Range<BufferRow>, Vec<BracketMatch<usize>>> {
let mut tree_sitter_data = self.latest_tree_sitter_data().clone();
let known_chunks = match known_chunks {
Some((known_version, known_chunks)) => {
if !tree_sitter_data
.chunks
.version()
.changed_since(known_version)
{
known_chunks.clone()
} else {
HashSet::default()
}
}
None => HashSet::default(),
};
let mut new_bracket_matches = HashMap::default();
let mut all_bracket_matches = HashMap::default();
for chunk in tree_sitter_data
.chunks
.applicable_chunks(&[self.anchor_before(range.start)..self.anchor_after(range.end)])
{
if known_chunks.contains(&chunk.row_range()) {
continue;
}
let Some(chunk_range) = tree_sitter_data.chunks.chunk_range(chunk) else {
continue;
};
let chunk_range = chunk_range.to_offset(&tree_sitter_data.chunks.snapshot);
let bracket_matches = match tree_sitter_data.brackets_by_chunks[chunk.id].take() {
Some(cached_brackets) => cached_brackets,
None => {
let mut bracket_pairs_ends = Vec::new();
let mut matches =
self.syntax
.matches(chunk_range.clone(), &self.text, |grammar| {
grammar.brackets_config.as_ref().map(|c| &c.query)
});
let configs = matches
.grammars()
.iter()
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
.collect::<Vec<_>>();
let chunk_range = chunk_range.clone();
let new_matches = iter::from_fn(move || {
while let Some(mat) = matches.peek() {
let mut open = None;
let mut close = None;
let depth = mat.depth;
let config = configs[mat.grammar_index];
let pattern = &config.patterns[mat.pattern_index];
for capture in mat.captures {
if capture.index == config.open_capture_ix {
open = Some(capture.node.byte_range());
} else if capture.index == config.close_capture_ix {
close = Some(capture.node.byte_range());
}
}
matches.advance();
let Some((open_range, close_range)) = open.zip(close) else {
continue;
};
let bracket_range = open_range.start..=close_range.end;
if !bracket_range.overlaps(&chunk_range) {
continue;
}
return Some((open_range, close_range, pattern, depth));
}
None
})
.sorted_by_key(|(open_range, _, _, _)| open_range.start)
.map(|(open_range, close_range, pattern, syntax_layer_depth)| {
while let Some(&last_bracket_end) = bracket_pairs_ends.last() {
if last_bracket_end <= open_range.start {
bracket_pairs_ends.pop();
} else {
break;
}
}
let bracket_depth = bracket_pairs_ends.len();
bracket_pairs_ends.push(close_range.end);
BracketMatch {
open_range,
close_range,
syntax_layer_depth,
newline_only: pattern.newline_only,
color_index: pattern.rainbow_exclude.not().then_some(bracket_depth),
}
})
.collect::<Vec<_>>();
new_bracket_matches.insert(chunk.id, new_matches.clone());
new_matches
}
};
all_bracket_matches.insert(chunk.row_range(), bracket_matches);
}
let mut latest_tree_sitter_data = self.latest_tree_sitter_data();
if latest_tree_sitter_data.chunks.version() == &self.version {
for (chunk_id, new_matches) in new_bracket_matches {
let old_chunks = &mut latest_tree_sitter_data.brackets_by_chunks[chunk_id];
if old_chunks.is_none() {
*old_chunks = Some(new_matches);
}
}
}
all_bracket_matches
}
fn latest_tree_sitter_data(&self) -> MutexGuard<'_, RawMutex, TreeSitterData> {
let mut tree_sitter_data = self.tree_sitter_data.lock();
if self
.version
.changed_since(tree_sitter_data.chunks.version())
{
*tree_sitter_data = TreeSitterData::new(self.text.clone());
}
tree_sitter_data
}
pub fn all_bracket_ranges(
&self,
range: Range<usize>,
) -> impl Iterator<Item = BracketMatch> + '_ {
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
grammar.brackets_config.as_ref().map(|c| &c.query)
});
let configs = matches
.grammars()
.iter()
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
.collect::<Vec<_>>();
iter::from_fn(move || {
while let Some(mat) = matches.peek() {
let mut open = None;
let mut close = None;
let depth = mat.depth;
let config = &configs[mat.grammar_index];
let pattern = &config.patterns[mat.pattern_index];
for capture in mat.captures {
if capture.index == config.open_capture_ix {
open = Some(capture.node.byte_range());
} else if capture.index == config.close_capture_ix {
close = Some(capture.node.byte_range());
}
}
matches.advance();
let Some((open_range, close_range)) = open.zip(close) else {
continue;
};
let bracket_range = open_range.start..=close_range.end;
if !bracket_range.overlaps(&range) {
continue;
}
return Some(BracketMatch {
open_range,
close_range,
newline_only: pattern.newline_only,
depth,
});
}
None
})
) -> impl Iterator<Item = BracketMatch<usize>> {
self.fetch_bracket_ranges(range.clone(), None)
.into_values()
.flatten()
.filter(move |bracket_match| {
let bracket_range = bracket_match.open_range.start..bracket_match.close_range.end;
bracket_range.overlaps(&range)
})
}
/// Returns bracket range pairs overlapping or adjacent to `range`
pub fn bracket_ranges<T: ToOffset>(
&self,
range: Range<T>,
) -> impl Iterator<Item = BracketMatch> + '_ {
) -> impl Iterator<Item = BracketMatch<usize>> + '_ {
// Find bracket pairs that *inclusively* contain the given range.
let range = range.start.to_previous_offset(self)..range.end.to_next_offset(self);
self.all_bracket_ranges(range)
@@ -4320,15 +4470,19 @@ impl BufferSnapshot {
pub fn enclosing_bracket_ranges<T: ToOffset>(
&self,
range: Range<T>,
) -> impl Iterator<Item = BracketMatch> + '_ {
) -> impl Iterator<Item = BracketMatch<usize>> + '_ {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let result: Vec<_> = self.bracket_ranges(range.clone()).collect();
let max_depth = result.iter().map(|mat| mat.depth).max().unwrap_or(0);
let max_depth = result
.iter()
.map(|mat| mat.syntax_layer_depth)
.max()
.unwrap_or(0);
result.into_iter().filter(move |pair| {
pair.open_range.start <= range.start
&& pair.close_range.end >= range.end
&& pair.depth == max_depth
&& pair.syntax_layer_depth == max_depth
})
}
@@ -4815,6 +4969,7 @@ impl Clone for BufferSnapshot {
remote_selections: self.remote_selections.clone(),
diagnostics: self.diagnostics.clone(),
language: self.language.clone(),
tree_sitter_data: self.tree_sitter_data.clone(),
non_text_state_update_count: self.non_text_state_update_count,
}
}
+121
View File
@@ -0,0 +1,121 @@
//! A row chunk is an exclusive range of rows, [`BufferRow`] within a buffer of a certain version, [`Global`].
//! All but the last chunk are of a constant, given size.
use std::{ops::Range, sync::Arc};
use clock::Global;
use text::{Anchor, OffsetRangeExt as _, Point};
use util::RangeExt;
use crate::BufferRow;
/// An range of rows, exclusive as [`lsp::Range`] and
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
/// denote.
///
/// Represents an area in a text editor, adjacent to other ones.
/// Together, chunks form entire document at a particular version [`Global`].
/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
#[derive(Clone)]
pub struct RowChunks {
pub(crate) snapshot: text::BufferSnapshot,
chunks: Arc<[RowChunk]>,
}
impl std::fmt::Debug for RowChunks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RowChunks")
.field("version", self.snapshot.version())
.field("chunks", &self.chunks)
.finish()
}
}
impl RowChunks {
pub fn new(snapshot: text::BufferSnapshot, max_rows_per_chunk: u32) -> Self {
let buffer_point_range = (0..snapshot.len()).to_point(&snapshot);
let last_row = buffer_point_range.end.row;
let chunks = (buffer_point_range.start.row..=last_row)
.step_by(max_rows_per_chunk as usize)
.enumerate()
.map(|(id, chunk_start)| RowChunk {
id,
start: chunk_start,
end_exclusive: (chunk_start + max_rows_per_chunk).min(last_row),
})
.collect::<Vec<_>>();
Self {
snapshot,
chunks: Arc::from(chunks),
}
}
pub fn version(&self) -> &Global {
self.snapshot.version()
}
pub fn len(&self) -> usize {
self.chunks.len()
}
pub fn applicable_chunks(
&self,
ranges: &[Range<text::Anchor>],
) -> impl Iterator<Item = RowChunk> {
let row_ranges = ranges
.iter()
.map(|range| range.to_point(&self.snapshot))
// Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
// This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
.map(|point_range| point_range.start.row..point_range.end.row + 1)
.collect::<Vec<_>>();
self.chunks
.iter()
.filter(move |chunk| -> bool {
let chunk_range = chunk.row_range().to_inclusive();
row_ranges
.iter()
.any(|row_range| chunk_range.overlaps(&row_range))
})
.copied()
}
pub fn chunk_range(&self, chunk: RowChunk) -> Option<Range<Anchor>> {
if !self.chunks.contains(&chunk) {
return None;
}
let start = Point::new(chunk.start, 0);
let end = if self.chunks.last() == Some(&chunk) {
Point::new(
chunk.end_exclusive,
self.snapshot.line_len(chunk.end_exclusive),
)
} else {
Point::new(chunk.end_exclusive, 0)
};
Some(self.snapshot.anchor_before(start)..self.snapshot.anchor_after(end))
}
pub fn previous_chunk(&self, chunk: RowChunk) -> Option<RowChunk> {
if chunk.id == 0 {
None
} else {
self.chunks.get(chunk.id - 1).copied()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RowChunk {
pub id: usize,
pub start: BufferRow,
pub end_exclusive: BufferRow,
}
impl RowChunk {
pub fn row_range(&self) -> Range<BufferRow> {
self.start..self.end_exclusive
}
}
+11 -4
View File
@@ -1111,9 +1111,10 @@ fn test_text_objects(cx: &mut App) {
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut App) {
let mut assert = |selection_text, range_markers| {
#[track_caller]
fn assert(selection_text: &'static str, range_markers: Vec<&'static str>, cx: &mut App) {
assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
};
}
assert(
indoc! {"
@@ -1130,6 +1131,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
«}»
let foo = 1;"}],
cx,
);
assert(
@@ -1156,6 +1158,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
let foo = 1;"},
],
cx,
);
assert(
@@ -1182,6 +1185,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
let foo = 1;"},
],
cx,
);
assert(
@@ -1199,6 +1203,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
«}»
let foo = 1;"}],
cx,
);
assert(
@@ -1209,7 +1214,8 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
}
let fˇoo = 1;"},
vec![],
Vec::new(),
cx,
);
// Regression test: avoid crash when querying at the end of the buffer.
@@ -1221,7 +1227,8 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
}
let foo = 1;ˇ"},
vec![],
Vec::new(),
cx,
);
}
+9 -3
View File
@@ -1323,6 +1323,7 @@ struct BracketsConfig {
#[derive(Clone, Debug, Default)]
struct BracketsPatternConfig {
newline_only: bool,
rainbow_exclude: bool,
}
pub struct DebugVariablesConfig {
@@ -1685,9 +1686,13 @@ impl Language {
.map(|ix| {
let mut config = BracketsPatternConfig::default();
for setting in query.property_settings(ix) {
if setting.key.as_ref() == "newline.only" {
let setting_key = setting.key.as_ref();
if setting_key == "newline.only" {
config.newline_only = true
}
if setting_key == "rainbow.exclude" {
config.rainbow_exclude = true
}
}
config
})
@@ -2640,8 +2645,9 @@ pub fn rust_lang() -> Arc<Language> {
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#,
(closure_parameters "|" @open "|" @close)
(("\"" @open "\"" @close) (#set! rainbow.exclude))
(("'" @open "'" @close) (#set! rainbow.exclude))"#,
)),
text_objects: Some(Cow::from(
r#"
+8 -5
View File
@@ -54,14 +54,14 @@ pub struct AllLanguageSettings {
pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct WhitespaceMap {
pub space: SharedString,
pub tab: SharedString,
}
/// The settings for a particular language.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct LanguageSettings {
/// How many columns a tab should occupy.
pub tab_size: NonZeroU32,
@@ -153,9 +153,11 @@ pub struct LanguageSettings {
pub completions: CompletionSettings,
/// Preferred debuggers for this language.
pub debuggers: Vec<String>,
/// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor.
pub colorize_brackets: bool,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct CompletionSettings {
/// Controls how words are completed.
/// For large documents, not all words may be fetched for completion.
@@ -207,7 +209,7 @@ pub struct IndentGuideSettings {
pub background_coloring: settings::IndentGuideBackgroundColoring,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct LanguageTaskSettings {
/// Extra task variables to set for a particular language.
pub variables: HashMap<String, String>,
@@ -225,7 +227,7 @@ pub struct LanguageTaskSettings {
/// Allows to enable/disable formatting with Prettier
/// and configure default Prettier, used when no project-level Prettier installation is found.
/// Prettier formatting is disabled by default.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct PrettierSettings {
/// Enables or disables formatting with Prettier for a given language.
pub allowed: bool,
@@ -584,6 +586,7 @@ impl settings::Settings for AllLanguageSettings {
},
show_completions_on_input: settings.show_completions_on_input.unwrap(),
show_completion_documentation: settings.show_completion_documentation.unwrap(),
colorize_brackets: settings.colorize_brackets.unwrap(),
completions: CompletionSettings {
words: completions.words.unwrap(),
words_min_length: completions.words_min_length.unwrap() as usize,