As a holiday gift from Zed, we're shipping our current most-requested feature: rainbow brackets.

If you've ever stared at a deeply-nested function wondering which bracket matches what, this is for you. Rainbow brackets color each nesting level differently, so you can visually follow where blocks begin and end at a glance. Rainbow brackets has been the most popular standing Zed feature for more than 3 years. It's available on stable today.
How we built it
Why not use language servers?
On top of text and coordinates, Zed primarily relies on two things: tree-sitter and language servers. Language servers are defined by the LSP specification, and there's no such thing as "bracket colorization" or "rainbow brackets" in the spec.
Certain servers could provide semantic highlights or other bracket-related information, but this doesn't scale well. Most servers won't implement this, and it's hard to push for such a feature in each of them.
So we had to build this on Zed's side.
Why not copy VS Code?
When VS Code implemented bracket colorization, they built a system that maintains the entire syntax tree in memory. But Zed's architecture is different: we use tree-sitter purely as a query engine. We send queries, get results, and store no syntax trees in memory ourselves.
Querying and maintaining a full syntax tree just for bracket colors would be quite a task. We had a different idea.
Chunks are good enough
So we have nothing and everything at the same time: no simple way to get a syntax tree (and concerns about how it will scale for large files), yet dozens of extensions that can already query their bracket pairs within a given range.
Zed already has quite an infrastructure around tree-sitter. Every language extension defines *.scm query files that extract semantic information from code: highlights.scm for syntax highlighting, outline.scm for document symbols, and brackets.scm for finding bracket pairs.
Here's what the Rust bracket queries look like:
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
(closure_parameters "|" @open "|" @close)
(("\"" @open "\"" @close) (#set! rainbow.exclude))
(("'" @open "'" @close) (#set! rainbow.exclude))Note the (#set! rainbow.exclude) bits. Tree-sitter's query language lets us set properties that propagate into match results. This is how we exclude string quotes from rainbow coloring.
Before this feature, Zed used these queries to highlight the bracket pair under the cursor. Each time the cursor moved, we'd re-run the queries for just that position. But for rainbow brackets, we need to color all visible brackets, and we need to know their nesting depth.
Recently, I'd done a fairly large refactoring of the inlay hints system. While inlay hints come from the LSP world, they share many interesting properties with what we'd need for bracket coloring: stored per buffer, queried within a certain visible range, tracking buffer changes and versions.
This pointed toward a solution: Zed's viewport with default settings shows around 35 lines. What if we color brackets in chunks of 50 rows, without worrying about perfect consistency across chunk boundaries?
If we color by depth and cycle through 7 colors, processing chunks independently might occasionally produce a color offset at boundaries. But most people don't count brackets meticulously. They want visual distinction between nesting levels. If a bracket is colored "one off" at a chunk boundary deep in a file, it's unlikely anyone will notice, and readability still improves.
This let us skip the complexity of maintaining global bracket state entirely.
Chunks are non-overlapping row ranges inside a buffer, for now 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 range (scrolling, editing, resizing excerpts). If needed later, we can enlarge the chunk size or add code to consider neighbor chunks.
Design decisions
A few other decisions shaped the implementation:
Store bracket data in the buffer, not the editor. Editors in Zed are ephemeral. You can split them, close them, have multiple editors pointing at the same file. The buffer is the stable entity, so that's where we cache bracket query results.
Invalidate eagerly, re-query lazily. Chunks invalidate on each buffer change (i.e., clock::Global version bump), but we only re-query tree-sitter when an editor actually needs to render those brackets, typically just the visible range. Re-population happens based on editors' visible ranges and actions (scrolling, resizing multi buffer excerpts, etc.).
Reuse theme accent colors. Each nesting level gets the next accent color, cycling back after we've used them all. You can customize these via theme overrides.
Per-language opt-in. Not every language benefits equally from rainbow brackets, so we allow enabling it per-language in settings.
Putting it together
With those design decisions in mind, the actual code changes were relatively contained. The core addition is a TreeSitterData struct that lives 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 tracking its own version. The brackets_by_chunks field caches the bracket query results for each chunk. When a buffer changes, we eagerly invalidate the cache—but we only re-query tree-sitter when an editor actually needs to render that range.
On the editor side, each editor tracks which chunks it has already fetched. When you scroll or edit, the editor asks the buffer for bracket data in the visible range. If those chunks are cached and still valid, we return them immediately. Otherwise, we query tree-sitter, cache the results, and apply the colors as text highlights.
We reuse Zed's existing highlight infrastructure for the actual rendering, and we ensure minimum contrast against the editor background so brackets stay readable regardless of your theme.
Visual test notation
A large piece of the implementation was testing. We iterated on the editor API quite a bit, and having tests at that level helped a lot to reason about the implementation.
We ended up visualizing tests with a notation similar to what Zed uses for selection tests. 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»]7»)6»]5»
}4»
}3»
}2»;
}1»You can see how deeply-nested brackets cycle through colors 1-7 and wrap back around.
Performance
Tree-sitter is quite fast, but we still wanted to be careful. We made some upstream improvements to reduce the number of tree nodes processed for bracket queries:
Fix slow tree-sitter query execution by limiting the range that queries searchAdd "containing range" APIs to QueryCursor
With these optimizations and the chunk-based approach, querying is quick even for unusual grammars. Lukas Wirth verified the performance, and it holds up well.
Remote development works the same way. At that level, remote clients are just editors with a buffer that gets synchronized and re-parsed. No special handling needed.
Try it today
Search in the Settings Editor (cmd-,) for the Colorize Brackets setting. It's off by default. You can enable it at a per-language level, and the brackets use theme-aware coloring. You can also exclude specific bracket types.

What's next
Releasing a feature this pervasive is always a bit of a leap of faith. It touches every language extension and could surface unexpected edge cases in grammars we haven't tested extensively.
A few things we're thinking about:
Rainbow tags. The "bracket pairs" concept extends beyond (), [], and {}. In HTML and JSX, <div> and </div> are also bracket pairs. The infrastructure is in place (extensions can already define these in their brackets.scm), but we excluded tag-style pairs for the initial release to keep scope manageable. It looks sized well for a community contribution if you want to try it!
Feedback and refinement. We'll be watching discussions and issues over the coming weeks. Unbalanced bracket behavior can vary between tree-sitter grammars, and we don't fully control that on Zed's side.
More tree-sitter caching. Now that we have chunk-based caching infrastructure for brackets, we should evaluate whether other *.scm query results could benefit from similar treatment.
Amazing to see colleagues collaborating and interested in the feature. We hope you like it.
Happy coding, and happy holidays!
Related Posts
Check out similar blogs from the Zed team.
Looking for a better editor?
You can try Zed today on macOS, Windows, or Linux. Download now!
We are hiring!
If you're passionate about the topics we cover on our blog, please consider joining our team to help us ship the future of software development.