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
+13 -20
View File
@@ -29,7 +29,6 @@ use crate::{
lsp_command::{self, *},
lsp_store::{
self,
inlay_hint_cache::BufferChunk,
log_store::{GlobalLogStore, LanguageServerKind},
},
manifest_tree::{
@@ -73,6 +72,7 @@ use language::{
serialize_lsp_edit, serialize_version,
},
range_from_lsp, range_to_lsp,
row_chunk::RowChunk,
};
use lsp::{
AdapterServerCapabilities, CodeActionKind, CompletionContext, CompletionOptions,
@@ -117,7 +117,7 @@ use std::{
time::{Duration, Instant},
};
use sum_tree::Dimensions;
use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, Point, ToPoint as _};
use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _};
use util::{
ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
@@ -3590,7 +3590,7 @@ pub struct BufferLspData {
code_lens: Option<CodeLensData>,
inlay_hints: BufferInlayHints,
lsp_requests: HashMap<LspKey, HashMap<LspRequestId, Task<()>>>,
chunk_lsp_requests: HashMap<LspKey, HashMap<BufferChunk, LspRequestId>>,
chunk_lsp_requests: HashMap<LspKey, HashMap<RowChunk, LspRequestId>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -6706,7 +6706,7 @@ impl LspStore {
self.latest_lsp_data(buffer, cx)
.inlay_hints
.applicable_chunks(ranges)
.map(|chunk| chunk.start..chunk.end)
.map(|chunk| chunk.row_range())
.collect()
}
@@ -6729,7 +6729,6 @@ impl LspStore {
known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
cx: &mut Context<Self>,
) -> HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>> {
let buffer_snapshot = buffer.read(cx).snapshot();
let next_hint_id = self.next_hint_id.clone();
let lsp_data = self.latest_lsp_data(&buffer, cx);
let query_version = lsp_data.buffer_version.clone();
@@ -6758,14 +6757,12 @@ impl LspStore {
let mut ranges_to_query = None;
let applicable_chunks = existing_inlay_hints
.applicable_chunks(ranges.as_slice())
.filter(|chunk| !known_chunks.contains(&(chunk.start..chunk.end)))
.filter(|chunk| !known_chunks.contains(&chunk.row_range()))
.collect::<Vec<_>>();
if applicable_chunks.is_empty() {
return HashMap::default();
}
let last_chunk_number = existing_inlay_hints.buffer_chunks_len() - 1;
for row_chunk in applicable_chunks {
match (
existing_inlay_hints
@@ -6779,16 +6776,12 @@ impl LspStore {
.cloned(),
) {
(None, None) => {
let end = if last_chunk_number == row_chunk.id {
Point::new(row_chunk.end, buffer_snapshot.line_len(row_chunk.end))
} else {
Point::new(row_chunk.end, 0)
let Some(chunk_range) = existing_inlay_hints.chunk_range(row_chunk) else {
continue;
};
ranges_to_query.get_or_insert_with(Vec::new).push((
row_chunk,
buffer_snapshot.anchor_before(Point::new(row_chunk.start, 0))
..buffer_snapshot.anchor_after(end),
));
ranges_to_query
.get_or_insert_with(Vec::new)
.push((row_chunk, chunk_range));
}
(None, Some(fetched_hints)) => hint_fetch_tasks.push((row_chunk, fetched_hints)),
(Some(cached_hints), None) => {
@@ -6796,7 +6789,7 @@ impl LspStore {
if for_server.is_none_or(|for_server| for_server == server_id) {
cached_inlay_hints
.get_or_insert_with(HashMap::default)
.entry(row_chunk.start..row_chunk.end)
.entry(row_chunk.row_range())
.or_insert_with(HashMap::default)
.entry(server_id)
.or_insert_with(Vec::new)
@@ -6810,7 +6803,7 @@ impl LspStore {
if for_server.is_none_or(|for_server| for_server == server_id) {
cached_inlay_hints
.get_or_insert_with(HashMap::default)
.entry(row_chunk.start..row_chunk.end)
.entry(row_chunk.row_range())
.or_insert_with(HashMap::default)
.entry(server_id)
.or_insert_with(Vec::new)
@@ -6896,7 +6889,7 @@ impl LspStore {
.map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints))))
.chain(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| {
(
chunk.start..chunk.end,
chunk.row_range(),
cx.spawn(async move |_, _| {
hints_fetch.await.map_err(|e| {
if e.error_code() != ErrorCode::Internal {
@@ -3,10 +3,12 @@ use std::{collections::hash_map, ops::Range, sync::Arc};
use collections::HashMap;
use futures::future::Shared;
use gpui::{App, Entity, Task};
use language::{Buffer, BufferRow, BufferSnapshot};
use language::{
Buffer,
row_chunk::{RowChunk, RowChunks},
};
use lsp::LanguageServerId;
use text::OffsetRangeExt;
use util::RangeExt as _;
use text::Anchor;
use crate::{InlayHint, InlayId};
@@ -46,8 +48,7 @@ impl InvalidationStrategy {
}
pub struct BufferInlayHints {
snapshot: BufferSnapshot,
buffer_chunks: Vec<BufferChunk>,
chunks: RowChunks,
hints_by_chunks: Vec<Option<CacheInlayHints>>,
fetches_by_chunks: Vec<Option<CacheInlayHintsTask>>,
hints_by_id: HashMap<InlayId, HintForId>,
@@ -62,25 +63,10 @@ struct HintForId {
position: usize,
}
/// 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 [`clock::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(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BufferChunk {
pub id: usize,
pub start: BufferRow,
pub end: BufferRow,
}
impl std::fmt::Debug for BufferInlayHints {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BufferInlayHints")
.field("buffer_chunks", &self.buffer_chunks)
.field("buffer_chunks", &self.chunks)
.field("hints_by_chunks", &self.hints_by_chunks)
.field("fetches_by_chunks", &self.fetches_by_chunks)
.field("hints_by_id", &self.hints_by_id)
@@ -92,58 +78,30 @@ const MAX_ROWS_IN_A_CHUNK: u32 = 50;
impl BufferInlayHints {
pub fn new(buffer: &Entity<Buffer>, cx: &mut App) -> Self {
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
let buffer_point_range = (0..buffer.len()).to_point(&snapshot);
let last_row = buffer_point_range.end.row;
let buffer_chunks = (buffer_point_range.start.row..=last_row)
.step_by(MAX_ROWS_IN_A_CHUNK as usize)
.enumerate()
.map(|(id, chunk_start)| BufferChunk {
id,
start: chunk_start,
end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row),
})
.collect::<Vec<_>>();
let chunks = RowChunks::new(buffer.read(cx).text_snapshot(), MAX_ROWS_IN_A_CHUNK);
Self {
hints_by_chunks: vec![None; buffer_chunks.len()],
fetches_by_chunks: vec![None; buffer_chunks.len()],
hints_by_chunks: vec![None; chunks.len()],
fetches_by_chunks: vec![None; chunks.len()],
latest_invalidation_requests: HashMap::default(),
hints_by_id: HashMap::default(),
hint_resolves: HashMap::default(),
snapshot,
buffer_chunks,
chunks,
}
}
pub fn applicable_chunks(
&self,
ranges: &[Range<text::Anchor>],
) -> impl Iterator<Item = BufferChunk> {
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.buffer_chunks
.iter()
.filter(move |chunk| {
let chunk_range = chunk.start..=chunk.end;
row_ranges
.iter()
.any(|row_range| chunk_range.overlaps(&row_range))
})
.copied()
) -> impl Iterator<Item = RowChunk> {
self.chunks.applicable_chunks(ranges)
}
pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> {
pub fn cached_hints(&mut self, chunk: &RowChunk) -> Option<&CacheInlayHints> {
self.hints_by_chunks[chunk.id].as_ref()
}
pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option<CacheInlayHintsTask> {
pub fn fetched_hints(&mut self, chunk: &RowChunk) -> &mut Option<CacheInlayHintsTask> {
&mut self.fetches_by_chunks[chunk.id]
}
@@ -177,8 +135,8 @@ impl BufferInlayHints {
}
pub fn clear(&mut self) {
self.hints_by_chunks = vec![None; self.buffer_chunks.len()];
self.fetches_by_chunks = vec![None; self.buffer_chunks.len()];
self.hints_by_chunks = vec![None; self.chunks.len()];
self.fetches_by_chunks = vec![None; self.chunks.len()];
self.hints_by_id.clear();
self.hint_resolves.clear();
self.latest_invalidation_requests.clear();
@@ -186,7 +144,7 @@ impl BufferInlayHints {
pub fn insert_new_hints(
&mut self,
chunk: BufferChunk,
chunk: RowChunk,
server_id: LanguageServerId,
new_hints: Vec<(InlayId, InlayHint)>,
) {
@@ -225,10 +183,6 @@ impl BufferInlayHints {
Some(hint)
}
pub fn buffer_chunks_len(&self) -> usize {
self.buffer_chunks.len()
}
pub(crate) fn invalidate_for_server_refresh(
&mut self,
for_server: LanguageServerId,
@@ -263,7 +217,7 @@ impl BufferInlayHints {
true
}
pub(crate) fn invalidate_for_chunk(&mut self, chunk: BufferChunk) {
pub(crate) fn invalidate_for_chunk(&mut self, chunk: RowChunk) {
self.fetches_by_chunks[chunk.id] = None;
if let Some(hints_by_server) = self.hints_by_chunks[chunk.id].take() {
for (hint_id, _) in hints_by_server.into_values().flatten() {
@@ -272,4 +226,8 @@ impl BufferInlayHints {
}
}
}
pub fn chunk_range(&self, chunk: RowChunk) -> Option<Range<Anchor>> {
self.chunks.chunk_range(chunk)
}
}