github zed-industries/zed v0.1

latest releases: v0.134.1-pre, v0.134.0-pre, v0.134.1...
2 years ago

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.

Screen Shot 2021-06-02 at 2 28 05 PM

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.

Bypassing the code signing restriction

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-[: Fold
  • alt-cmd-]: Unfold

Multi-cursor / columnar editing:

  • cmd-shift-l: Split selection into lines
  • cmd-alt-up: Columnar select up
  • cmd-alt-down: Columnar select down
  • escape: Cancel columnar editing

Line-oriented navigation and editing

  • ctrl-a: Move to beginning of line
  • ctrl-e: Move to end of line
  • cmd-l: Select line
  • ctrl-shift-k: Delete line
  • cmd-backspace: Delete to beginning of line
  • cmd-delete: Delete to end of line
  • cmd-shift-d: Duplicate line
  • ctrl-cmd-up: Move line up
  • ctrl-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 node
  • alt-down: Select smaller syntax node
  • ctrl-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:

Screen Shot 2021-06-02 at 2 16 26 PM

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.

Screen Shot 2021-06-02 at 2 12 21 PM

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.

Screen Shot 2021-06-02 at 2 23 28 PM

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 parameters. That’s something that would be pretty hard to do correctly without a full syntax tree.

Iterating on the editor

In early performance testing, we noticed that the performance for reading data out of Zed’s buffer data structure wasn’t meeting our standards. To support collaborative editing and other advanced features, we maintain a history of every operation in a B-tree. Originally, we also stored the text associated with every insertion in this same tree, but this forced us to read the text in tiny pieces and from lots of different locations in memory.

To improve read performance, we changed our approach to storing buffer text. We still maintain a tree of every operation, but we no longer store the text in this tree. Instead, we maintain two rope data structures, which group our text into a series of chunks, each between 16 and 32 bytes in length. When reading, we now can now work with text one chunk at a time rather than one character at a time for the vast majority of cases. We maintain one rope that represents the buffer’s current contents and another that represents everything that’s ever been deleted from the buffer. This setup optimizes read efficiency for the latest state of the buffer, but if we ever need to understand what the buffer looked like in a historical state, we can use the operation tree to weave together contents from the two different ropes.

Next steps

Our initial goal was a basic editor that allowed you to open files, make edits, and save. Then we added syntax highlighting and some basic editing operations. Now we’re turning our focus towards real-time collaboration.

It will be possible to enable sharing for any worktree that you’re editing in Zed. When sharing is on, you’ll upload a snapshot of the current worktree state and stream every operation to our servers. When sharing is off, you’ll never send any data, and we’ll never send any intermediate operations. If you type a password into a file and then delete it and then enable sharing, we’ll never see that password.

To start this effort last week, we made a big structural change to the buffer CRDT based on some recent insights. With that change behind us, we have a pretty clear idea of how we want to structure communication between Zed running on the desktop and our server-side code, as well as a strong initial hypothesis around how we architect the server. We're nearly finished with an OAuth-based authentication flow, and then we can start on the fun parts.

We think minimizing latency will be really important to the user experience. It would be a shame for two users collaborating in Europe or Asia to need to roundtrip every keystroke through a server in Virginia. For that reason, we’re planning on experimenting with Fly.io, a service that makes it easy to manage a fleet of geo-distributed containers. If they deliver on their promises, it should be as simple as tweaking a configuration file to spin up a server in Europe, Asia, etc. We’re also intrigued by CockroachDB as a scalable, geo-distributed data store to pair with our geo-distributed app servers.

Thanks!

Thanks for your interest and support. We'd love your feedback on anything we're doing and we're looking forward to showing you a ton more progress as we get into collaboration support.

Don't miss a new zed release

NewReleases is sending notifications on new releases.