Introducing Rainbow Brackets in Zed: Enhancing Code Readability
Zed editor launches rainbow brackets, a long-awaited feature that color-codes nested code blocks for improved readability. Discover its efficient Tree-sitter and chunk-based implementation.
Zed is excited to announce the release of rainbow brackets, a highly anticipated feature designed to significantly enhance code readability. This enhancement is for anyone who has struggled to discern nesting levels in complex code. Rainbow brackets visually differentiate each nesting level with distinct colors, making it easy to track where code blocks begin and end at a glance. The demand for rainbow brackets has consistently topped Zed's feature requests for over three years, and it's available in the stable version today.
How it was Built
Why Not Use Language Servers?
Zed's architecture primarily relies on Tree-sitter and language servers, which are defined by the LSP specification. However, the LSP specification does not include provisions for features like "bracket colorization" or "rainbow brackets." While some language servers might offer semantic highlights or other bracket-related information, this approach lacks scalability. Most servers would not implement it, making it difficult to push such a feature across all of them. Therefore, the decision was made to build this functionality directly within Zed.
Why Not Mimic VS Code?
When VS Code implemented bracket colorization, they developed a system that maintains the entire syntax tree in memory. Zed's architecture differs significantly; it uses Tree-sitter primarily as a query engine. Zed sends queries and receives results without storing full syntax trees in memory. Maintaining a complete syntax tree solely for bracket colors would be a substantial undertaking and deviate from Zed's core design. A different strategy was required.
Leveraging Chunks for Efficiency
Zed already possesses robust Tree-sitter infrastructure. Every language extension defines *.scm query files to extract semantic information from code, such as highlights.scm for syntax highlighting, outline.scm for document symbols, and brackets.scm for identifying bracket pairs.
Here's an example of the Rust bracket queries:
("("
@open
")"
@close)
("["
@open
"]"
@close)
("{"
@open
"}"
@close)
("<"
@open
">"
@close)
(closure_parameters
"|"
@open
"|"
@close)
(( "\"" @open "\"" @close) (#set! rainbow.exclude))
(( "'" @open "'" @close) (#set! rainbow.exclude))
Notice the (#set! rainbow.exclude) directives. Tree-sitter's query language allows setting properties that propagate into match results, enabling the exclusion of string quotes from rainbow coloring.
Previously, Zed used these queries to highlight the bracket pair under the cursor by re-running them for that specific position. However, for rainbow brackets, all visible brackets need to be colored, along with their nesting depth.
A recent, fairly large refactoring of the inlay hints system provided a critical insight. While inlay hints originate from the LSP world, they share many interesting properties with what was needed for bracket coloring: stored per buffer, queried within a visible range, and tracking buffer changes and versions.
This pointed toward a solution: Zed's viewport with default settings shows around 35 lines. What if brackets were colored in chunks of 50 rows, without worrying about perfect consistency across chunk boundaries?
If coloring by depth and cycling through 7 colors, processing chunks independently might occasionally produce a color offset at boundaries. However, most users prioritize visual distinction between nesting levels over meticulous bracket counting. If a bracket is colored "one off" at a chunk boundary deep in a file, it's unlikely anyone will notice, and readability still significantly improves. This approach allowed the team to completely bypass the complexity of maintaining global bracket state.
Chunks are non-overlapping row ranges inside a buffer, currently set at 50 rows maximum. Each chunk tracks its version via clock::Global (a version vector). Chunks invalidate on buffer changes and are re-queried only when an editor needs to render that specific range (e.g., during scrolling, editing, or resizing excerpts). If needed later, the chunk size can be enlarged, or code to consider neighbor chunks can be added.
Design Decisions
Several key decisions guided the implementation:
- Store bracket data in the buffer, not the editor. Editors in Zed are ephemeral; they can be split, closed, or multiple editors can point to the same file. The buffer is the stable entity, making it the ideal location to cache bracket query results.
- Invalidate eagerly, re-query lazily. Chunks are invalidated with every buffer change (i.e.,
clock::Globalversion bump). However, Tree-sitter is only re-queried when an editor actively needs to render those brackets, typically within the visible range. Repopulation occurs based on editors' visible ranges and actions (scrolling, resizing multi-buffer excerpts, etc.). - Reuse theme accent colors. Each nesting level cycles through the available accent colors defined by the theme. These colors can be customized via theme overrides.
- Per-language opt-in. Not all languages benefit equally from rainbow brackets. The feature can be enabled or disabled on a per-language basis in the settings.
Bringing it Together
With these design principles, the actual code changes were relatively focused. A core addition is the TreeSitterData struct, residing on each buffer:
pub struct TreeSitterData {
chunks: RowChunks,
brackets_by_chunks: Vec<Option<Vec<BracketMatch<usize>>>>,
}
pub struct BracketMatch<T> {
pub open_range: Range<T>,
pub close_range: Range<T>,
pub color_index: Option<usize>,
}
The chunks field divides the buffer into 50-row segments, each managing its own version. The brackets_by_chunks field caches the bracket query results for each chunk. While the cache is eagerly invalidated upon buffer changes, Tree-sitter is only re-queried when an editor actually needs to render that range.
On the editor side, each editor tracks which chunks it has already fetched. When a user scrolls or edits, the editor requests bracket data for the visible range from the buffer. If these chunks are cached and still valid, they are returned immediately. Otherwise, Tree-sitter is queried, results are cached, and colors are applied as text highlights.
Zed's existing highlight infrastructure is reused for rendering, and minimum contrast against the editor background is ensured so brackets remain readable regardless of the user's theme.
Visual Test Notation
Testing was a significant part of the implementation, particularly iterating on the editor API. A visual test notation, similar to Zed's selection tests, proved invaluable. Each bracket pair is annotated with its color index:
fn main «1()1» «1{ let a = one «2(«3()3», «3{ «4()4» }3», «3()3»)2»; println! «2( "{a}" )2»; println! «2( "{a}" )2»; for i in 0 .. a «2{ println! «3( "{i}" )3»; }2» let b = «2{ «3{ «4{ «5[«6(«7[«1(«2[«3(«4[«5(«6[«7(«1[«2(«3[«4(«5[«6(«7[«1(«2[«3(«4()4», «4()4»)3»]2»)1»]7»)6»]5»)4»]3»)2»]1»)7»]6»)5»}4»}3»}2»; }1»
This notation clearly illustrates how deeply-nested brackets cycle through colors 1-7 and then wrap back to the beginning.
Performance
Despite Tree-sitter's inherent speed, careful attention was paid to performance. Upstream improvements were made to reduce the number of tree nodes processed for bracket queries:
- Fix slow Tree-sitter query execution by limiting the range that queries search
- Add "containing range" APIs to QueryCursor
These optimizations, combined with the chunk-based approach, ensure quick querying even for unusual grammars. Performance has been verified by Lukas Wirth and holds up well.
Remote development environments function identically, as remote clients simply act as editors with synchronized and re-parsed buffers, requiring no special handling.
Try it Today
To enable rainbow brackets, search for the "Colorize Brackets" setting in the Settings Editor (accessible via cmd-, on macOS). It is disabled by default. You can enable it per-language, and the brackets will use theme-aware coloring. Specific bracket types can also be excluded.
The Colorize Brackets setting in the Settings Editor.
What's Next
Releasing a feature of this scope involves inherent challenges, as it impacts every language extension and might reveal unexpected edge cases in less-tested grammars.
Future considerations include:
- Rainbow tags. The concept of "bracket pairs" extends beyond
(),[], and{}to include HTML/JSX tags like<div>and</div>. The underlying infrastructure supports this (extensions can define these in theirbrackets.scm), but tag-style pairs were excluded from the initial release for scope management. This presents a suitable opportunity for community contributions. - Feedback and refinement. The team will actively monitor discussions and issues. The behavior of unbalanced brackets can vary between Tree-sitter grammars, and this is not fully controlled by Zed.
- More Tree-sitter caching. Now that chunk-based caching infrastructure exists for brackets, evaluating its applicability to other
*.scmquery results for potential performance benefits is a logical next step.
The collaboration and interest from colleagues in this feature have been remarkable. We hope users appreciate this addition.
Happy coding, and happy holidays!