Releases: zed-industries/zed
v0.1
Zed is starting to feel real. It’s amazing what a simple editor with syntax highlighting and advanced text editing commands can feel like when it’s screaming fast. I want this tool. I've been waiting for Zed for so long.
This demo video of cursor movement bindings can probably give you a feel for it. To really see the performance, you’ll want to fully download the video and watch on a desktop computer rather than watching on a phone or trying to stream from Google Drive.
Trying it out
If you want to give Zed 0.1 a try, use the download link at the left.
We don’t code sign our application bundle yet, so you’ll need to jump through a hoop in order to run Zed on your Mac. After you open the DMG, rather than double-clicking the app icon, right-click and select “Open”. You’ll need to do this twice. On the second try, you’ll see the option to open the application anyway even though it isn’t signed.
The following commands are worth playing with. Most will work with any text file, but the syntax-specific ones only work in Rust for now.
Code folding:
These are indentation-based for now, but folding based on syntax would be straightforward to add.
alt-cmd-[
: Foldalt-cmd-]
: Unfold
Multi-cursor / columnar editing:
cmd-shift-l
: Split selection into linescmd-alt-up
: Columnar select upcmd-alt-down
: Columnar select downescape
: Cancel columnar editing
Line-oriented navigation and editing
ctrl-a
: Move to beginning of linectrl-e
: Move to end of linecmd-l
: Select linectrl-shift-k
: Delete linecmd-backspace
: Delete to beginning of linecmd-delete
: Delete to end of linecmd-shift-d
: Duplicate linectrl-cmd-up
: Move line upctrl-cmd-down
: Move line down
Syntax-oriented selection
We can take this further. One idea we’d like to explore is a syntactic selection mode where the cursor is positioned on a node of the syntax tree. Up and down arrows would move up and down the syntax tree, whereas left and right would move through siblings at that level. For now we just offer a few basics that showcase our syntactic understanding.
alt-up
: Select larger syntax nodealt-down
: Select smaller syntax nodectrl-m
: Move to the enclosing / matching bracket
Technical details
Here are some highlights of the technology we’ve developed so far:
Graphics
The first thing we shipped post close was a Metal-based custom graphics backend for our UI framework. I had previously depended on a third-party graphics library called Pathfinder that was introducing complexity and an unacceptable performance overhead, so we decided to take direct control.
It was definitely a learning experience to render 2D graphics on the GPU efficiently. Signed distance fields are an important tool that’s quite fascinating… You’re programming the color of each pixel based on its distance from the perimeter of a mathematically defined shape. They worked great for rounded corners.
We’re also rasterizing Bezier curves on the GPU for our selection outlines. Here's a peek at Metal rasterizing a selection:
Drop shadows were interesting, too, and we leaned heavily on an article by Evan Wallace, CTO of Figma, to implement them.
You may be wondering about text. Previously, we were rasterizing glyphs on the GPU with Pathfinder based on the curve data in fonts. This was overkill however, because we don’t actually need to render glyphs at arbitrary sizes and angles since we’re not trying to make Zed usable in VR. Instead, we’ve found that it’s simpler and faster to rasterize glyphs on the CPU, upload them to the GPU in an atlas texture, then texture map polygons that we place at the position of each glyph. Here's another screenshot from the Metal frame debugger. You can see how glyphs are just polygons. It's a lot like a video game, just way simpler.
Here you can see the atlas texture to which we write all our glyphs and icons. The polygons pictured above are textured based on sub-regions within this atlas. We actually render up to 16 variants of each glyph to account for sub-pixel positioning both vertically and horizontally.
Currently, we repaint the entire window any time anything changes. As you can see from the demo video, it's plenty fast, but we may eventually want to to explore caching layers to avoid repainting everything in order to gain more power efficiency. Compared to Electron, though, I think we're still in really good shape taking a straightforward, video-game-like approach.
The Worktree
We maintain an index of every path in the source tree the user is editing in a structure called the Worktree
. It’s a copy-on-write B-tree that’s cool in a few ways. When you open a new worktree, we kick off a file system scan in the background on 16 threads. These threads contend on a lock to write new subdirectories into the tree as they crawl the file system in parallel. What’s cool about the Worktree is that its state is O(1) to clone, meaning we can periodically clone its latest state from the background workers to the UI thread every 100ms. This allows the user to start querying the paths we’ve scanned so far before we’ve finished a full scan.
The path-matching implementation is based on the Needleman-Wunsch algorithm, which I believe is German for “I wish for a needle, man”. To find that needle faster, we divide up the haystack and run matching on all of the user’s cores in parallel. Our B-tree is helpful here as well for helping us determine which part of the tree should be processed by each thread, since non-ignored file paths are unevenly distributed throughout the tree. The B-tree lets us index the count of visible files and quickly jump to a subset of those files in each thread.
Tree-sitter
We have also integrated Tree-sitter, an incremental generalized LR parser that Max Brunsfeld developed. After the user edits, Tree-sitter is able to recycle data from the previous syntax tree to produce an updated syntax tree quickly. Anecdotally, we’re seeing the majority of edits in large, complex Rust files re-parse in around 1ms, although we’ll need telemetry under real-world usage to fully understand the distribution of parsing latencies.
Parsing is asynchronous, so we never block rendering on parsing after the user types a key. We interpolate the current state based on the previous syntax tree while the parser works in a background thread to deliver the latest state.
We’ve had some interesting thoughts around selective synchronization, where we can intelligently block on data being available only so long as it won’t cause us to drop a frame. Otherwise we interpolate, and your syntax node changes color 1 frame later than the edit that caused it to become a keyword, for example. Our goal is to pack as much value in the 0-16.6ms available between a keystroke and the next frame as possible, hopefully with plenty of time to spare.
Unlike many other editors, Zed’s syntax highlighting is completely syntactically accurate, because it’s based on a parse tree from a formal grammar rather than a bunch of hacked-together regular expressions. Tree-sitter has a query system that allows you to match specific syntactic patterns, and we use it for highlights. For example, here’s how we style Rust function call expressions in Zed 0.1.
(call_expression
function: [
(identifier) @function
(scoped_identifier
name: (identifier) @function)
(field_expression
field: (field_identifier) @function.method)
])
The S-Expressions describe the patterns of a part of the syntax tree, and the @
-prefixed labels give pieces of those patterns a name. In the example above, a call expression whose function is an identifier can be styled differently from a method call, where the function being called is a field_expression
. We use these names directly to unify with a theme that associates the syntax decoration classes with colors, font weight, etc.
We also use the query language elsewhere. We used it to make short work of the command to move to the nearest enclosing bracket. Here’s how we express all of Rust’s bracket pairs as a query in the Rust language definition:
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)
When the user moves to enclosing bracket command, we query the tree for any matches to the above patterns that intersect the user’s selection. We then find the smallest match and ask the open
and close
nodes for their position in the tree. Note that we can also match identical pairs like the "
s that surround strings and the |
s that surround Rust closure paramete...