Next to writing their own operating system, another dream shared by many developers is building their own text editor. Conrad Irwin, a software engineer at Zed, is doing just that. Zed is a fully extensible, open-source text editor written entirely in Rust. It's fast, lightweight, and comes with excellent language support out of the box.In the first episode of the third season, I sit down with Conrad to discuss Zed's mission to build a next-generation text editor and why it was necessary to rebuild the very foundation of text editing software from scratch to achieve their goals.
This is Rust in Production, a podcast about companies who use Rust to shape the future of infrastructure. My name is Matthias Endler from Corot, and today we're talking to Conrad Irvin from Zed about building a high-performance code editor in Rust. Conrad, thanks for being here. Can you quickly introduce yourself and Zed, the company you work for?
Yep. I'm Conrad. I work at Zed, which is trying to build the next generation of text editor. Coding at the speed of thought is our motto. Prior to Zed, I built Superhuman, which is also focused on speed, building the fastest email client in the world. So I really like building tools that make people get what they need to get done faster and easier. And that's how I found myself working on Zed.
It's pretty amazing. I have to say, I'm a set user myself. I completely switched from VS Code. And I can tell you the experience so far is fantastic. You did a great job. And this is also why I wanted to talk to you folks. First off, in your words... What's wrong with the existing editors? What are some pain points? Why do we need another editor?
I guess you hear that a lot, but I want to hear the answer from you. Makes sense.
I think there's kind of two approaches to think about. One of which is, as you look at the world today, everything is collaborative by default. People are designing in Figma, docs are in Google Docs, and then programmers are still stuck, kind of each person editing their own file and then git commit, git push. Oh no, what happened to my git commits?
It seems ludicrous that we don't have real-time collaboration for code. And so that was one of the key kind of design things is how do we build real-time collaboration incorrectly? But we knew that if we wanted to persuade people that they wanted to use that, it also needed to be better than what's out there.
And if you compare it to something like a VS Code, you can take the browser emulator and the extensions APIs and all of that stuff that makes VS Code kind of slow and clunky and rebuild it natively in Rust and using an actual fast GPU native rendering. So taking more of kind of like the React approach and like a game would be built.
On the other side, you have things like Vim, which are fast and people love them for the speed and the ease, but they don't work with any of the modern tools. So you spend all your time configuring language servers and like breaking plugins and fixing plugins and breaking plugins.
And so we wanted to make something that had this kind of trio of collaborative, extremely fast and just works out of the box. So really helping people get their work done, like not spending time configuring your editor, if that makes sense.
If Z didn't exist, which editor would you use?
Well, before it did exist, I kept switching between NeoVim and VS Code. VS Code, because I like the language servers and all of that stuff, but it would just frustrate me too often. So I'd go back to NeoVim where I know how to be productive. And so I've spent a lot of my time making Zed's Vim mode work for me and for the other people who use Vim. So that's been fun.
Yeah, and you did a great job there. I have to say there are very few gaps in the Vim support by now. It wasn't the case just half a year ago, but just seeing the rapid development is really, really good, really surprising. And also I found myself switching between VS Code and NeoVim as well.
So every time I became sick of VS Code's laggy performance, I would switch to NeoVim only to find out that the configuration was a bit of a hassle. And so I have to switch back at some point. Exactly.
And so you're kind of the ideal target user, someone who wants better things from their tools. And so building that the right way has been very fun.
Did that also entail any differences in how Z is architectured in comparison to other editors? Or was it just mostly Rust's performance that made it so snappy?
Rust definitely helps a lot. It's not JavaScript, which is really nice. But one of the things that we do that is very different is that we render like a game renders. So each frame, so every 8 milliseconds or 16 milliseconds, we redraw the entire screen to the GPU.
and that means that we are not CPU-bound when it comes to rendering stuff, unlike, say, a VS Code or an HTML that has to do a lot of CPU work to get stuff onto the screen. Ours is all GPU out there. And so that's kind of the biggest difference, I would say. Also, because of the time we started it, language servers are built in in VS Code. Each language server is wrapped in an extension.
In Z, we do have some extensions that provide that, but language server is really kind of like the thing it's based on. And then the other big piece of tech that we use a lot of is TreeSitter. So when you're trying to do things like jumping to matching brackets, we don't have to pause the entire file.
We already have a tree of what the syntax looks like, and we can just jump you to the other end of the node. And so building on sensible foundations means that most operations are faster. We're not trying to iterate over the whole file at one time or that kind of stuff.
Right, so the editing part works on the CPU, I would assume. That's the tree set apart. And the rendering part of the UI that works on the GPU, is that correct?
Yeah, there's still some CPU work involved in rendering the screen, like when you change the UI. But the CPU creates this kind of tree of nodes, very similar to HTML. And then the GPU is responsible for taking that tree of nodes and flushing that to pixels.
And is that something that you have to handle on the Rust side or does that more or less evolve automatically when you build the application with that in mind? Is that something where, for example, you have a hypervisor for the GPU and then you have some other thing that takes care of state of text files that you have currently loaded? Or is that something that more or less happens automatically?
Yeah.
It doesn't happen automatically. A lot of the code in Zed is built to handle those things. And so where Rust really helps is things like lifetimes. You close a file that should be deallocated, and Rust makes really good guarantees about that kind of stuff.
For things like the GPU pipelines, because Rust is good interoperability with C, we can just call straight into the graphics APIs and just dump, here's the buffers that we need you to render GPU. Go do that. And so there's a whole framework, GPUI, which is the framework that Zed uses for all of that. And a couple other apps are using it, but mostly we built that to build Zed.
And so once you've built that, then you can build an editor on top of it.
Someone might listen and think, oh, why didn't they use Leptos or Deoxys or all the other Rust UI frameworks? Why did they have to invent their own thing? What would be your answer?
Primarily speed of fixing things. And so Zed moves very quickly compared to any other software team I've worked on. I've never been moved along so quickly. And so we're very careful to avoid putting things in the way that will move slowly, and particularly unnecessary abstractions. So we spend a lot of time trying to make the macOS integration feel right.
And if we had to each time go through someone else's crate, fix it there, then pipe it all through, the overall development time is slower. It also means that it's very concretely designed for Zed. There's no extra APIs that we don't need or stuff that we don't want. And that comes important. One good example of that is keyboard shortcut handling.
It's very unlikely that someone else would have a keyboard shortcut thing that can support FIM mode and all of the other things that we want to do with Zed. And so skipping all of that and doing it ourselves makes some things easier, less obstructions to get in the way.
And absolutely, you can feel that when you use it because it's snappy. It feels native. That, by the way, was one other thing that I always missed in NeoVim. It didn't really feel like a native application. It felt like a terminal emulated thing that ran inside of, you know... Some terminal, of course, which it kind of was, but that always kept me away from other things like Emacs, for example.
I do like all of these experiences, but I also want the native look and feel because especially on macOS, if you're used to this, it's very hard to switch away from that experience again. Yeah, I strongly agree.
And some really interesting design decisions we have, like in Vim mode, the colon key brings up the command palette. And one of the nice things about that is we can provide autocompletion. In Vim, there's no space to do that because you don't have a proper graphical UI. You only have lines in the command panel. And so, yeah, it's definitely freeing to not be in a terminal emulator.
Yeah. Let's see, which keyboard shortcuts do you support? What sort of keyboard key maps, I would say, do you support? You support Vim. Then you support the normal command or control-based keyboard chords, almost. Then you have actual chords, which are things like GD or GR, anything else that I'm missing.
One that JetBrains users love, you can double tap shift to bring up the, if you enable the JetBrains keymap, double tap shift brings up the command palette. And that was a fun one that we added recently just to kind of appease those people. I'm actually working on a PR with a contributor right now to try and automatically support non-Latin keyboards.
So for example, in a macOS, if you do command A on a Russian keyboard, it's obviously going to do the same, select all. But in Vim mode, A in Vim normal mode, it sees the Russian character instead and doesn't do anything. And so I've been trying to figure out how to make that work as well.
Well, when you describe it, it sounds kind of straightforward, but you have to plan for this from the very beginning, because otherwise you will not be able to manage to support all of these different ways of input, right?
Exactly. And actually, one thing that helps us avoid planning is because we own the whole thing from the very top level Swift layer all the way down to the GPI layer, we can control how that works. But handling things like international input and making that work sensibly with keyboard shortcuts, it's not an obvious problem. And so I look forward to fixing all the remaining edge cases.
But we're a long way there right now.
Can you just quickly and briefly describe to us how that part works, how that part of set the keyboard input works? Because in my mind, it's event based. You have things like you definitely don't want to block the rendering in any way. You probably need to do that asynchronously or event based. But I don't want to put words into your mouth. I want to hear from you. How does that part work?
Yeah, so starting kind of from the top, macOS gives us an event-based API where they say, hey, someone pressed a key down at this point. We then have to say to them, okay, well, is this part of a multi-key key? So on a US keyboard, if you do option backtick, you get into the mode where you can type A with an acute accent on top. So we then have to integrate with that.
And that's just a weird edge case of it has a reentrant callback system. So it gives you the event, then you call back into it, then it calls back into you, which Rust does not like. It's very, very unsafe as far as Rust is concerned. But once we've gone through a few rounds of that, we know, OK, the user pressed this key. This is the character it's going to generate.
This is the key that was pressed. We then send that to the matcher. And we're like, OK, did either the character that was generated or the key that was pressed with the Modifiers or Shift Control command Does that match any of the defined bindings? If it matches the binding, we do the action. If it doesn't, then we input the text.
And one of the things that we do to try and make the keyboard bindings easier to program is that each component in the app, so the editor is one big component, has a bunch of key bindings. And then the pane, which contains the editor, has a bunch of key bindings. And so you can set them at the right level.
And that means you can have multiple different meanings for a given key binding, depending on where you're focused right now. which is really important for something that's complicated as that.
And all of these events, they end up in some sort of state machine, or are they more or less modifying the state and then they are more or less gone from the system again? A little bit of both.
So as you mentioned, we have the ability to do multi-key keystrokes. So if you do a G and then an R, it goes to all references in that mode. And so that piece is a state machine of like, okay, is there a key? Like, could it be a pending match? Is it something else? But once it's happened, it really just happens. And it's kind of like a callback into the rest of the code.
And so you hit GR and it goes, okay, run the final references code. That's going to probably, in that case, kick off an async task. So we don't want to run that on the main thread because we're going to communicate back to the language server and ask it questions. So we kick it to the background, wait for the response, and then update the UI in response to that.
So using a lot of the ASIC Rust stuff to make that not happen on the main thread.
And this is probably also how you circumvent issues with self-referential structs in Rust where maybe you want to, you know, you described a mechanism where you had an event that would trigger another event and you would have a callback upon a callback maybe. And these things, they trigger some sort of PTSD in my mind because...
If you do it the wrong way, then you will end up in a thing that doesn't compile anymore. And if you kind of have a dispatcher in between, you can completely get rid of this problem because asynchronous solves that problem for you. Is that correct?
The, I'm not sure exactly if AsyncRust solves that problem, but the GPUI framework has some tools for it. So mostly the UI is a tree. It's really the only data structure you can have in Rust where you have the root view that maintains handles to all of the subviews.
But obviously often you want to be able to refer back up the tree of like, okay, I'm an editor, but I know I'm rendered inside a pane or I know I'm rendered inside something else. And so we have a system of kind of strong pointers and weak pointers. And that works pretty well for avoiding it.
But it is one area where I wish the compiler was better because it can't tell you, hey, you have a smart pointer cycle, right? It's like you have two strong pointers in each direction. But as long as you're somewhat careful to maintain the order in your head, it's mostly fine. And we don't really have that many problems with memory leaks, at least none that we found yet.
Are there any issues that you defer to runtime or is all of that done at compile time?
Yes. So we have a whole bunch of smart pointers for view components because we need to be able to reference them from the renderer and also from the view tree. And so they need to be kind of like an arc reference from multiple places. We don't use an arc directly. Instead, we have our own wrapper around it that's a little bit more type safe for some things.
And so if you do have things like a double bar, you get a panic instead of a compile time check. And so it would be nice if the compiler supported that, but I understand why it doesn't.
It'd be really hard to check. What are the additional type safety guarantees that you mentioned in your own implementation of this Arc alternative?
So the framework gives you, it calls them a view, and you can read it or update it, just like kind of an ArcMutex. What's nice about the view is it supports a bunch of other things. So a view can be rendered, a view can be converted into kind of like a black box, any view that the renderer can deal with.
And so, you know, when you're given a view handle, like there are some things you can do with it. It's not just a knock around an arbitrary struct.
Right. Is that something that I would be able to use as an external person, maybe as part of my application? I know that you open source the entire code, but is that something that I could use in any other context?
Yeah, I mean, yes, but. So if you're using GPUI to build an app, you definitely would use it. And it gives you the whole suite of tools. If you were just trying to pull out bits of GPUI on its own, that's probably not a bit I would pull out, just because it's so tied into the rendering system, if that makes sense.
Right. So these are abstractions that you built specifically for GPU UI and things that are paper cuts in your working environment and you just had the power to do it. So you went ahead and did it. Exactly. And so, yeah, it gives us a whole bunch of nice, nice guarantees.
Is that a very common pattern, I'd say, where maybe the ecosystem is just not there yet, or you have very specific requirements, so you have to build it yourself? Or does the ecosystem help you a lot?
I would say we very much are on the rebuild side of the spectrum. So there have been a lot of things where we haven't used what's out there. I think the other big one is Tokyo. So Tokyo has its own async runtime. But on macOS, it's not main thread aware. And macOS has a whole bunch of cool scheduling stuff to do when you have...
a graphical app so there's kind of a specific thread that coco knows okay like regardless of how busy the system is like we're going to keep this thread running because it's visible to the user and so we wanted to be able to hook into that system and so we have our own async runtime that's not tokyo but but kind of works works in that more ui based way of like if you want ui updates you have to be on the main thread otherwise you have to be on a background thread and we have a bunch of helpers to make sure that your code is always running on the thread you expect
When I looked through the code, I actually saw that you use both Tokyo and Small. I would assume that your own async runtime is based on top of Small, or is that even a separate runtime? You have three in total.
We don't use the runtimes for Tokyo or Small. I think we use a bunch of the helper stuff from them. But I know that was a push to get rid of Small because it's like, well, we don't really need anything from here. But that's kind of where the Rust ecosystem stuff comes in. It's a very big ecosystem with a whole bunch of stuff in it.
And so it's very easy to end up with a small dependency on these things through someone else, even if we're not using the main parts of their libraries.
Yeah, and I also would assume that if you took your async runtime and published it as a separate project, that would essentially mean that you would have to maintain it for external people. And it could also be a bit of an issue in regard to how you want to use that runtime inside Z. So I'm assuming that you don't want to do that.
I think we actually did. I was trying to look it up. It was something we were trying to do. And so we took that piece of GPUI and split it out into a separate crate. But I'm now looking, and I can't see it within 10 seconds. So I'll have to look it up after this and find it.
It's very nice to know that you have your own runtime, because I'm kind of a big proponent of diversity. And we have Tokyo, we had AsyncSTD, we have Small, and there's Glomio and a couple others. What's the state of the AsyncGross ecosystem in your mind? Yeah. I can already see that. I have some stuff to say.
A little side story. We're using a really old HTTP client right now called ISHC, which was very popular four or five years ago. And the reason we're using it is it's the only one that lets you plug and play the async runtime.
And so I really like the idea of having this kind of diverse set of async runtimes, but there seems to be some kind of... The abstraction is not in quite the right place, because it seems like if you're building something, you can't just say, oh, give me any async runtime. That's not as easy to do as it is to say, okay, we'll just use Tokyo. So that one seems to be landing on right now.
And so in the abstract, it's good for people to kind of solidify around Tokyo. That seems to be where most of the energy is, because it gives you more batteries included. But... Suddenly, you're in this situation where there's tools that we want to use, like Tokyo's HTTP stuff, which we can't because we don't use Tokyo's runtime.
And so, yeah, it's on the back burner right now, but we need to upgrade the HTTP client to upgrade LibSSL, which we don't really want to link, but there's no alternative async HTTP client that isn't Tokyo-based. And I don't want to build my own, please. So we'll have to kind of figure that out. And that's kind of been my experience with it.
You know, there's the day-to-day async pain points in the language itself, but the ecosystem is just very either Tokyo or kind of you're on your own a little bit.
Is that, in your opinion, something that will just solve itself at some point once the ecosystem grows a bit? Or is it a systemic issue?
I don't know the answer. I think it's, as with all of these things, it's part cultural, part technical. It's hard to write an async runtime because it's such an abstract piece of thing to do. The end result's only a few hundred lines of code, but the right few hundred lines of code, if that makes sense.
And so most people don't think to do that, which means kind of like, okay, culturally, sure, let's just use Tokyo. It's the most common one. And so I think
if we go that way it's i don't know maybe that's okay but if they don't support the async runtimes that everyone wants to use then then you kind of end up with these problems i don't know and it'll it'll be interesting to see how the community kind of like responds to it i think the ross community kind of likes rebuilding things over and over again so i'm sure we'll be fine it'll be interesting to see what comes out if you compared it with ross's arrow handling story
where we had multiple iterations and we ended up with, anyhow, this error, snafu, and all of these other abstractions that we didn't really have. All of these dependencies are relatively new in comparison to how old Rust is. that took a different turn because it took some time to mature and we didn't really stabilize on one error handling library.
Whereas in asyncrust, pretty much from the beginning, people kind of settled on Tokyo because as you said, it's a much more complicated piece of software and the ecosystem just wasn't as mature yet. But now we find ourselves in a situation where Tokyo is the dominant asyncrunt time. And is it just me or do you also see that problem there? And what would be the revelation here?
Yeah, I mean, I definitely see the problem, given the examples that we have. So one of the things that I'm hopeful for is that as the language evolves, you know, AsyncRust is still very beta, as I think they called it in the latest Rust planning blog post.
As they evolve the language support for it, what I hope is that it becomes easier to do both sides of things, both easier to use as a consumer, but also easier to create as a kind of like, hey, here's my new Async library for you.
And I think if you look at something like jQuery to use a very different example, it was something that started out very dominant or started out small, became very dominant very quickly. And then as the underlying browser APIs improved, became kind of less relevant.
So that's kind of the hope I'd see, if that makes sense, of as the APIs get more sensible and people get more used to all of the concepts involved, it becomes easier to do both sides of the coin. So I have nothing against Tokyo. It's a great piece of software. But I also, to your point earlier, I really like the diversity of being able to do things in multiple different ways.
At the same time, it gives me hope when you say that, because the jQuery example was a good outcome in my book, because we were able to experiment with these alternative JavaScript framework or so. And then browsers caught up and then they added some of the features into their own native implementation.
I guess the same could happen in Rust where we take parts of Tokyo and stabilize that, put it into the standard library, but we have to be careful there. For example, there's...
async read and async write trade and technically you could already stabilize that but it's a bit debated right now whether this is the right abstraction that we want to settle on going forward exactly and it it takes time to figure those out particularly for something like rust that's such a big complicated project like any extra complexity they bring into the standard library it like deserves a lot of scrutiny
But even things like async traits and stuff like that, there's so much obvious stuff that needs improvement first that I think there's definitely time to figure it out, even if it feels like it's not figured out yet. Come on. What percentage of Z is async? I would guess 50-50, but I don't know. Here we could have a look. But pretty much everything that requires accessing the disk is async.
The network is async, and text editors do a lot of disk access and lots of language server access. And so the stuff that's not async is really just, oh, you typed a key? Okay, left. One move the cursor one position to the left. We can do that synchronously.
But even things like search, if you search for a character, we kick that off to an async background thread so that we can keep rendering UI while we search. And so... Pretty much anything that uses a lot of CPU is not on the main thread.
Is that rule somewhere codified? Is that part of your code style? For example, you separate sync and asynchrost, or is that something that evolved naturally?
It's something that evolved naturally. So back to GPUI again, it provides contexts. And contexts are roughly a way of storing global state, but not in a global. But what that lets you do is that code that is sync takes an app context, and code that is async takes an async context. And so if you build something slow, you make it take an async context. You can't call it on the main thread anymore.
And so it gets easy to manage it kind of at a type level. But it does require when you're building a new feature that is CPU bound to notice and put that in the background.
That's so funny when you say that because you almost take the function coloring problem and make it a feature where you take the slow parts and you mark them as async so that you know exactly that this is something that you should not run on the main thread.
Yeah, exactly. I hadn't thought about it that way, but you're right. It's kind of nice. This is async. Go away.
We'll be back later. And you can see it in the type system too. Are there any other challenging aspects of building a text editor in Rust? Any unexpected hurdles?
Well, I mean, beyond the complexity of the editor piece, one of the things that is tricky with Rust, and we kind of touched on this a bit of time, is the ownership rules make it challenging because the text editor is fairly self-referential. You have all of the... All of the things want to know about the editor and the editor needs to be able to tell lots of things stuff.
And so we have several different solutions for, okay, here's the editor, we'll send events this way, or you can observe it and get things out. But it's not as easy as it would be in a language like JavaScript to just, okay, we'll plonk this thing on the side and the editor could talk to it and it could talk to the editor and we don't have to worry about it.
And so that's probably the main piece that Rust makes it tricky. Other than that, the hard bit is just building a text editor. There's so many features that people expect to just work.
Yeah, because everyone wants a different set of features. That too. Vim Mode is the classic example of that.
Why is that? Because the people who want Vim Mode can't live without it, and everyone else is like, I don't care about this. So that's true for not every feature we build, but a number of them. We've been working on Jupyter Notebooks, for example, and people who need Jupyter Notebooks, they love that feature. And everyone else is like, yeah, whatever. I don't use that.
And so kind of trying to navigate the trade-offs of which features do we build and who do we make happy in what order is a big problem. But that's definitely a text editor problem, not a Rust problem.
In preparation for this interview, I also checked out your new YouTube channel because you recently started a channel about talking about the internals and said, and I will link it in the show notes. It's pretty majestic. The one thing that I realized from these interviews was that you sometimes touched on a library called TreeSitter.
You mentioned it before, but that seems to be a bit of the secret sauce in there because apparently, and correct me if I'm wrong here, Other editors are not built this way. They don't work on this level of abstraction, almost like an AST, an abstract syntax tree level of abstraction to modify text. Can you elaborate on this a bit? What is it? And also, do you agree?
Is that really critical central part of that?
Yeah, definitely agree. So if you think about a programming text editor, kind of one of the first features you want to build is syntax highlighting. And if you look at really all editors, it's a hand-coded parser for each language that does it. Not going to fly. We don't have time to build a hand-coded parser for every language.
Then maybe a decade or two ago, people started using regular expressions. Like, cool, here's a regular expression that does it. And in some cases, like the KDE text editor was kind of influential early in this. It's like a half XML language, half regular expression. So you get a bit of recursion, a bit of regular expression, and kind of a mix in there.
And these things are all fine, and they work for what they work for. But they only solve the syntax highlighting problem. And so if you want to be able to understand a little bit more about, okay, so this text on the screen is just an array of Unicode bytes. But what does it mean? You need something that can not just look at it byte by byte, but really divide it up into syntax.
And one of the Z founders, Max, built TreeSitter to do this in Atom. But it's like, okay, we get syntax highlighting for this for free because it understands the language. But we also get things like jumping to matching brackets or If you want to look at a file and say, what's in this file? We have a thing called the outline view. And so you can just see all the things that are defined in there.
And that's all powered, again, by TreeSitter. And so it's fundamental to the way that we do programming language stuff, which can all be done instead of having to do it by byte, by tree traversal instead, which is orders of magnitude faster.
And how does TreeSitter work on a type level? Is it like an intermediate representation where you map certain keywords in different languages to the same keyword in TreeSitter?
So TreeSitter, each language in TreeSitter has its own kind of definitions. And then Zed has a couple of things that map from those definitions to our definitions. And so each supported language has a mapping of like, OK, in the TreeSitter grammar, there's a thing called comment. In the Z code highlighting, there's a thing called comment. Those are the same thing.
So we have that mapping for each language. Similarly, it's like, OK, if you want to extract all the function definitions from a file, this is the TreeSitter query used to get that. And those queries can all run on the background thread. And TreeSitter itself is kind of crazy. It's a bunch of C libraries written for each language.
So we run those inside a WebAssembly module to avoid the obvious problems with running C libraries. And that's been good for reliability.
That's a very smart move. Where does the name TreeSitter come from? Is that because it's almost a recursive structure where you can think of it as a tree inside a tree inside a tree where you have different levels of abstraction that you can iterate on? Or does it come from the abstract syntax tree?
No idea. I do know that kind of the key feature of it as opposed to like a pauser for a compiler is that it's error tolerant. So if you have a trailing quote mark, it's not going to throw it off. It can always do something. And so it has optimized small edits in the text lead to small changes in the tree. So I guess it monitors or babysits your tree. Maybe that's where it comes from. I don't know.
Do you know Semgrep? I know the name, but you'd have to remind me.
It's sort of library where you can modify code based on certain, I would even say like a grammar or something where you can say, I want to do a code modification and I give it an expression and then it figures out what to do based on this expression. It's,
more high level than a regular expression it's also more powerful i would say can you do certain things like this in z as well on a syntax level i know it's not exposed to the user but internally could you do such modifications
Yes, is kind of the answer. So yeah, we don't have much built on that right now, but it's kind of there in the background. One of the most obvious things we can do, there are bindings, select more and select less, and they work on the tree setter definition. So you can kind of, okay, I start in this quote, then I expand more until I have the whole function or less until I'm back down to the thing.
But we don't really have many go edit via syntax tree stuff right now. One small example of something we do have in Vim, we have an argument object. So you can select an argument with VIA, you know, select inside an argument. And that uses the tree set of grammar to find the argument that you're in.
Every day I get an update, which is nice because they work flawlessly and they work every single time. And I find myself reading the change log a lot because the change log is nicely formatted. You can see what's going on. I wonder how you do these updates. How does the pipeline work to create these updates? How do you push them to the clients? And how do you make it so flawless?
Keeping it simple and building it ourselves, I guess the two parts of that. So internally, we have a bunch of branches, one for each kind of version. So right now we're on 149 stable, 150 preview. Any commits that get added to those branches don't do anything until you run a script, which is script trigger release, and you give it a table of preview. That kicks off a build that uploads to GitHub.
And there's a lot involved in making a build of something like this because you have x86, you have ARM64, you have Mac and Linux. And so you end up with four or five different binary downloads that it creates. It uploads them all to GitHub, and then it marks releases preview.
And then Joseph, usually, but it could be anyone, goes in, takes all the commit messages, and formats out the release notes. And we have some tooling to kind of help with most of that work. But to your point, if you want to make them readable and nicely formatted, there's no auto, like, oh, yeah, we just pull it in from the PR call of the day. It doesn't work well enough.
And so we want to make sure that it's easy to understand how that is changing. And so we spend time on, spend manual time on that. And then, yeah, then go from there. And then the auto-updater on the client side is just a loop. Sleep for an hour. Is there an update? If there is, download it, copy it into place, and then reboot.
The tree sitter is written in C and is wrapped in WebAssembly. That means you must run some sort of WebAssembly runtime. Is that a custom build or do you use anything?
No, we use something that's out there. I don't remember which one off the top of my head, but we've, yeah, we use something that's out there. We have fixed some bugs in there because we saw some crashes from it. So, you know, it's kind of fun.
Is that also something that you use for the extensions?
Extensions can register tree set of grammars, and that's kind of the main interaction there. And then extensions themselves, they also have the ability to run some code in WebAssembly. Right now, it's pretty limited. So the most common use for an extension today is a language server. And trying to download the correct version of a language server requires a little bit of code.
And so that code all runs in WebAssembly as well.
And will you support traditional extensions at some point too? And how will that look like? Definitely yes. And who knows?
So kind of one of the things that we really care about is the speed and the performance piece of it. And so when we were first talking about extensions, it's like, well, we could have like a JavaScript runtime and run JavaScript extensions. But I don't think that's going to happen anymore. There are enough languages that are easy to compile down to a WebAssembly thing that will do it there.
The second piece that's tricky is we have a completely custom UI framework that doesn't have any bindings for any other language. And so how I imagine things going is that we kind of continue down the approach we have today, which is that we expose simple things that you can extend, one of which is the AI features have some hook points where you can pull in more context for the model. That's one.
Another is language server. A third is themes. But it's not like, hey, you can run arbitrary code in our process. Thank you very much. I think we'll keep it pretty tight for now. The next piece I really want to build is being able to kind of bind keyboard shortcuts and then run some code that modifies the editor.
And I think that that's kind of a solvable piece that we could kind of ship by yourself. And so let's say you want, you know, let's say SHA-256 support, right? You could imagine an extension that registers a new command and a keyboard shortcut to SHA-256 the file and gives you the answer. But trying to build something that allows you to render UI is a long way off, I think. Yeah.
I definitely appreciate the focus on stability and performance because those are the main two reasons why I use it. I would like to keep it this way. That's kind of nice.
Yeah, if you're doing a little bit of user research and talking to people working on very large code bases in VS Code, they really, really try not to restart their computer or turn off VS Code ever because they turn it back on and it takes five or six minutes to update all the extensions and spin around and show up banners until it's ready to use. And it's like, oh... We have to avoid that.
And it's hard because all those things do something useful for someone, but really try to make sure that they don't get in the way, I think is really important.
And once the project grows, it will not get easier because I just checked yesterday and Z is approaching 500,000 lines of Rust code, which is crazy. It's crazy. And I saw some interesting bits that I learned from you. For example, you have a flat hierarchy of crates. You have one workspace still. You keep it all in one workspace. Right. And then you follow a very flat hierarchy.
Can you elaborate on this design decisions and maybe other decisions that are reasonable or maybe even necessary after reaching a certain size of like a code base?
Yeah. So the primary input to the create structure is compile time because 500,000 lines of Rust takes a long time to compile. And so really the question is, what code gets recompiled a lot, and how do we reduce the amount of it? And so that's where the create structure comes from.
This is obviously a little bit around abstractions and not leaking between things, but really primarily it's the speed thing. So we don't really want a kind of a deeply nested thing where visibility is tightly controlled and we have lots of, you know, that's not important. We trust the whole code base. And so it's making sure that it's somewhat easy to find what you're looking for.
And when you rebuild, you know, if you make a change to the editor, you don't have to rebuild too much else of the code base to get into testing.
It's funny because lately I read an article by Matt Klett, who is the author of Rust Analyzer. And he mentioned this one thing about nested trees where you have to make a decision where you put things. And it's a conscious decision. It might be wrong. And so eventually you might end up in a suboptimal space, in a suboptimal structure of your project.
So he advocated for a flat hierarchy too, where you don't even have to make the decision because, yeah, it's flat anyway. If you are wondering where to put it, the answer is put it into the root of your workspace.
Right. Yeah, and I like the pragmatism there. We do have some crates that are too big, but mostly it's pretty well factored out, I think.
Any other such tips or perhaps even things that you would avoid now?
So one thing I learned, we did a rewrite of GPUI, the graphics framework, that was fairly heavy on use of generics and ended up exporting a bunch of things that were instantiated multiple times with multiple different types. And the way that Rust compiles generics is to kind of copy-paste the code each time for each type.
And so we had some kind of big compile time regressions at that point just because of the way the code was structured. And so one thing to look out for is as often as you can avoid having a generic type exported from a crate.
So if you have a function that takes generic arguments, you kind of want to keep that internal because otherwise every time someone uses it, you get another copy of that whole thing out. And obviously, like everything, there's a trade-off. There are some things where it is worth the pain, but there are other things where if you can avoid that kind of type boilerplate exploding, you should.
And just to clarify for the people who are listening, this also happens if you just use these generics inside of your workspace in a different crate, because that's a separate compilation unit. And that means even just exposing it within your project might be problematic at times. And what about lifetimes? Because... you have a very high focus on performance.
You want to make this thing as fast as possible. And one suggestion of various people that come from systems level programming languages like C++ and C is that you want to avoid allocations. You want to make everything a view into memory as much as possible. And you want to deal with the raw data as much as possible.
Is that something that you have to follow to reach that level of performance or are there ways around it? So for the hard code paths, yes, for sure.
So for things like the rendering pipeline, there's two frames that are just big static chunks of memory that we switch between, a fairly common approach there. And Rust is actually kind of helpful for that. It tells you if you mess up because you know which one is live and what you have access to every given time.
For most of the rest, not yet, because if you think about the way it works, so let's say you're on a 120-hertz screen, you have eight milliseconds to render every frame. So rendering a frame needs to be really fast. But the average person can only type a couple of characters every second.
So it's kind of fine if it's slow to respond to a key press, where slow means you have 8 milliseconds and you're not doing that much work. And so we use a fair amount of ref counted pointers just to maintain all of this to keep the code sane, even though that wouldn't strictly be optimal, just because we're not using them often enough for it to be a problem.
And when you run into a problem with performance, what's your approach? Do you benchmark that before you make any changes or do you just guess where the bottleneck is and have an intuition for it?
Mostly the instruments, tools from Xcode have been super helpful on Mac. Linux is a little newer and there are some tools there, but I'm not as familiar with them. One of the really interesting things that's kind of on the back burner for me is that deallocating a frame in Linux can take nearly a millisecond or two, which we're like, that shouldn't be the case.
And so if anyone listening is a good Linux performance person, figuring that out would be great. But if we're running a profiler on it, it's like, why is dropping the frame taking so much time? Because, you know, you only have eight milliseconds. And if you're using two of them doing nothing, it's a complete waste of time.
By frame, you mean what? A frame of memory in the kernel? A frame of pixels to update. Ah, okay. Do you use a lot of macros for code generation, or is that another thing that you tend to avoid in the hot paths of the code?
Relatively few. We have a couple that are used a lot, but for most things, we just write the code out.
And that's for ergonomics reasons or for other reasons?
Mostly stylistic, I think. That's the way the code base is. But again, I mean, macros can be a performance problem. They haven't been for us. Is that because we got lucky by choosing the style or did, you know, before my time, someone chose that style and now we all copy it.
When you ported Z to Linux, were there any surprises that you hit other than the mentioned issues with dropping frames?
Yes, quite a lot. So macOS is actually a really nice target to develop against because similar to their reputation on iPhone, there's only really one platform you need to support. And sure, the APIs shift a little bit as time goes on, but you can look at one number that's like, this is the version of Cocoa that you have, and you know all the libraries that you have.
Linux is not like that at all, right down to the fact of about half of Linux users use X11, the old Windows server, half of them using Wayland, the new Windows server. They both work differently, quite fundamentally differently. And so we have two graphics pipelines on Linux, one for Wayland, one for X11. And that kind of fragmentation hits us at every layer of the stack.
So on macOS, you want to choose a file, just open the system file chooser. On Linux, well, they might not even have a system file chooser installed. Now what are you going to do? And so that was kind of the most surprising thing for me is just how... just how customized everyone's look setup is.
Like even, even what I would consider like, surely this is just provided like a file picker isn't there. And so trying to navigate those trade-offs of like making it work for as many people as possible without going truly insane has been hard. Another good example is GPUs. Mac OS has a GPU. It works always. You just do it.
Linux has a GPU, but maybe the drivers are out of date or the drivers that are the wrong version or the closed source or the crash or whatever. And so we have a whole bunch of people who have tried to use ZLX and then it just hasn't worked. And it's like, well, when we try and talk to your GPU, it crashes. So is that our problem? Maybe. Is it your problem? Maybe. I don't know.
We have to try and find more people who know more about how GPUs work under the hood and why they might not be working.
I learned that it also compiles on Windows. Do you want to comment on that?
We have a dedicated... A dedicated team of volunteers. There's three or four people who I see regularly doing Windows fixes and ports. We need a breath after Linux before we dump into the next platform. But it is something we'd like to have. Windows is going to be fun for different reasons than Linux. Some of the same problems. It's a little bit more fragmented, though less so.
But the big one is the file path separator is the wrong way around. And we use Rust's path buff extensively internally. But if we allow collaboration between Linux and Windows, we can't represent a path in a path buff because it might be a Windows UTF-16 path or it might be a Linux UTF-8 path.
So we need some kind of new file path abstraction that is not tied to the current system, which is one of the downsides of the way Rust does that.
When you explained that, I wondered, how would I test that? I would have a really sophisticated test environment for different environments. Do you test it in VMs? Do you test it just with unit tests or manual testing? How does that part work? Testing in general or cross-platform testing? Interested in both, yeah, but specifically cross-platform. Okay.
Cross-platform is a little manual, to be honest. So the way that the app is set up, you have kind of a platform-specific layer, and then everything else is Rust. We have a test implementation of the platform-specific layer, so we can very easily test all the stuff that's not platform-specific. And it mostly just works.
And sure, there are a couple of if statements that depend on what platform you're on, but mostly the code is the same for everyone. And that is one of the nice things about Rust. It is just Rust. When it comes to testing platform integrations, like back to keyboard shortcut handling, like when you type these keys on this keyboard layout on Mac OS, it should do this instead.
I have not figured out a better way than just getting yourself into that setup and trying it. And so to be determined.
Do you focus on unit tests or integration tests for the rest of the code base?
pretty much integration tests for the most part. So we have, as you know, we have collaboration. And because the server piece is also written in Rust and also part of the same repository, we boot up the server, we boot up Zeds, and we talk through both of them. And so we have full integration tests. And I kind of like that approach because A, it lets you test interesting ordering properties.
So the test platform will reorder different async events that happen simultaneously so that you get more test coverage, for example. It also means you can refactor the code and it doesn't break the tests. That's always been my gripe with unit tests. You change the code and then you have to change the tests. What's the point?
How do you communicate with the server? JSON? Protobuf. Why exactly do you use Protobuf and not anything else?
Yeah, Protobuf is kind of like the classic solution to this. We're actually thinking about changing off them because they don't integrate with Rust super well. And as all of our code is in Rust, it'd be nice to have something that integrates better. But one of the main challenges of a distributed system is you have to be able to deal with messages sent from the past. So like, you know, forward .
And then you also need to be able to deal with messages sent from the future. So if someone is on a newer version of the set than you, they could send you a message. And you need to be able to do something in that case that isn't this crash. And so there's not much that handles that. There's protobufs and kind of a couple other big, heavy solutions.
But there seems to be kind of a missing niche for a Rust-based thing that can solve this. Because one of the downsides of protobufs is it generates a whole bunch of struct definitions. It's like, well, we have all the structs defined in our code base, and we have to map between the two.
It would be nice if we could, more like Serde, just say, and make the struct forward, backward compatible over the wire, please. But I haven't found anyone who's built that yet.
There are a few things that come to mind. One is Postcard by James Muntz, who, it has a slightly different focus. It's serialization format, yeah, but it's not based on Rust structure as far as I remember. Then there's Seabore, which is another serialization format. I honestly don't know what that wire format looks like, but I think there's also one that is based on Rust structs themselves.
We use one, I'm trying to remember what it's called in a different part of the code base. It's like MessagePack or something like that, which works fine, but it doesn't have any versioning support. Yeah.
I think the one that I meant was called Arson. Okay, no, that's different again. It's like a JSON-like thing, but with Rust structs. Because the one issue that I found with protobuf was that you need to carry the definition file. You need to put it somewhere, and then you need to compile your stuff against whatever protobuf definition you have somewhere.
And that can be a little annoying in the workflow.
Definitely. And we have a build step that does it for you. But for example, if you have an error in the protobuf file, it breaks everything because the build step fails. And then it's like, oh, you can't build. And you have to really dig in and find out why that is. But yeah, thanks for the postcard link. And I will look into those. Thank you.
Yeah. Shout out to James for building that. I took a look at the issue tracker and I found that one of the most often, if not the most often requested features that's missing in set right now is debug support. And a lot of people might say, well, why haven't they added it just yet? And how can it be so complicated? Can you say a few words about that? Sure.
I guess what makes it so complicated is that there are 50 different programming languages that we're trying to support. The other thing that makes it complicated is it's actually a very fiddly piece of UI and UX. And obviously, there are lots of existing ones, so we can kind of copy them. But it's not just a copy-paste from VS Code or something like that.
There's a lot to think about and a lot to build so that it not only works well, but it feels intuitive and you can actually understand how to use it. So one of the things that's really interesting, there's kind of a debugger protocol that's beginning to feel somewhat standard, which is the one that Chrome uses for its dev tools. That's the one the VS Code builds on.
There are obviously other implementations, like some go directly to the debuggers. But what I imagine we'll do first is kind of support the debug protocol, kind of punt a little bit on the languages that don't work with that and make it a language problem. But I hope if we do that, we can kind of like language servers. We get most of the benefit with a tenth of the work.
But they're still building all the UI, so you can look at local variables, job list lines. If you start to think about all the things that a debugger can do, it's definitely a lot more than just play and pause.
Which languages work best with this new protocol?
So JavaScript definitely works well with the protocol because it was written for that. I know that Go's debugger, Delve, also has support for it. One thing I'm not sure about is does LLPP, which is the Rust debugger, or NEC-based language, I don't know if it supports that protocol yet, but that's definitely a debugger that we would like to have support for.
Nice. I'm really looking forward to that. Yeah, you and about 500 other people, I think, based on the outputs. The pressure's on, but I'm sure that when it hits, it will be fine because this is how I experienced this editor or this approach so far. It's always very well thought through, which is great. And speaking of which...
About things that look easy on the surface but are hard in hindsight, is there any peculiar bug that you remember? Anything that people take for granted but in reality is very hard to pull off?
A particularly unique bug, I guess. So Zed allows you to edit files of any size. And we had a bug where if you had a file that was over about 100,000 lines long, and you scrolled down, the line numbers would not be at the right position. You'd have the line of text, and the line number would be plus or minus a couple of pixels.
And we looked into it, and it turned out that because our graphics coordinates float 32s, when we were multiplying the line number by the float 32 to try and figure out the distance from the very top of the file, it just didn't work out at all. And so we ended up having to first subtract from the first visible line and then do the offset. And then that just fixed it.
But it's really interesting to think about, how do you have a file that's so long and you could just edit it without having to rewrite the entire file every time?
Yeah, most text editors would even crash at this point or they would not even get to that point.
Well, you can still open files that are big enough that Z will crash. We don't do anything super clever around paging things in yet. But one of the things that we do do is when you load a file, we break it up into a bunch of chunks.
And so as you're editing a file, we're not having to say, you know, take a evacuate, which is the Rust underlying string type, insert one in the middle and reallocate the rest. That would be way too slow. So when you insert into the middle, we use our CRDT to say, oh, we're just the first end characters from here, then this character, then these end characters.
And so because we're representing it as a tree, kind of like a rope, if you've heard of that data structure, that means that everything is kind of quick. And because it's a CRDT, it also works for collaboration natively. So we know for each chunk who added it and in which order.
And so even as multiple people are concurrently editing the string, everyone ends up with the same representation without having to copy and paste everything across.
Exactly. And it's also elegant because if you use two big vectors, you probably have some jank on the interface between the two or like in between the two when you jump from one block to the other. But if you use a smarter data structure, you kind of circumvent that issue altogether.
Exactly. And then one of the things that wraps that that I think is pretty cool is we have to maintain where the line breaks are. And we don't want to scan through the string and figure out where all the line breaks are. And so we maintain a couple of indexes on top so that we know, OK, in this half of the file, there's 1,500 line breaks. In this half of the file, there's more.
So that as you scroll, we can quickly jump to the right part of the file without having to, from the beginning, scan and count the new lines. And so using indexes like that to help us navigate the strings means that we're basically never doing anything that's order n in the size of the file, which is nice.
It sounds like a very hard problem that you have to figure out once, but then you never have to touch. Are there other things like this in the editor?
Well, we've talked about one of them, which is TreeSitter. It's like syntax highlighting for arbitrary languages is a very hard problem. But once you've solved it with something like TreeSitter, you only have to solve it once.
yeah beyond that it's it's hard to say i think the yeah i think the crdt it's one of the few parts of the code base where i basically never find myself there because everything just works which is nice let me tell you about my first interaction we've said i had a problem with vim support and i went into the issue tracker and i found an issue that exactly described my problem and
And so I read the last couple of comments. And what I found interesting was that you, Conrad, reached out to the people in this issue and said, I'm open to hack on this together. That was the first time I ever saw this level of interactivity with any project, because it's not only...
Through issues, you would invite people to block time in your calendar to use the tool you're going to improve together and work interactively. Is that something that you do a lot? How does that influence your workflow?
Yeah. So kind of back to the beginning, one of the things that I love working on is tools that help people. And I believe that collaboration in programming is way behind where it should be. And so I have kind of a secret goal, which is to get more people trying it out instead and trying to get more converts that way.
But what I found is that it's very slow comparatively to have a discussion about how to fix an issue in GitHub issues, because it's kind of like email. You send a bang, maybe you wait a few days, someone replies back. You don't understand what they said, but another few days to clarify. If you can say, hey, let's work on this together. Some people opt out.
Either they don't feel like they could be useful in Rust or they don't feel like the English is good enough. But for the people who say, sure, and join in, we can, in half an hour of working together, solve an issue that would have taken us each an hour or so back and forth on GitHub. So I have on Calendly link two hours on Tuesdays, two hours on Fridays that are just like compare with me time.
And it's kind of fun both to work on issues that other people feel are important. You know, sometimes, particularly Vim, there are so many features in Vim where I'm like, I never even knew this existed and I don't understand why you need it. But you really, really want it. Great. Let's build it together. Because then I'm getting you into Z and getting more people kind of helping out.
And you feel amazing because you've got to implement the thing that you wanted to build. And so it's been really helpful to do that. In general, obviously, the whole code base is open source. We intend to keep it that way. We want people to make Z the editor that they want. And so really encouraging people to send in changes. And they can be pretty major.
There's been one contributor working tirelessly on what he calls Zen mode, which is how do you get rid of all the UI? And so setting by setting, he's adding them all in so he can do that. But a lot of people just come in, they're like, hey, I hit this bug, here's a fix, let's do that.
And so really trying to make sure that the community is kind of making Zed the editor they want to see, it helps everyone. And so I would strongly encourage you if you are using Zed or if you're not yet using Zed to use it and then fix the bugs that you find.
If someone out there wants to contribute to Zed now, how do they get started? Very simply, look in the issue tracker.
There's about 2000 issues. Some of them are tagged with, I can't remember the name, but it's like a first issue tag. Otherwise, my suggestion to anyone trying to get into anything is find something that irks you or something that you miss from the editor you were in before and build it or file an issue about it and discuss it. We're very happy as a team to pair with people. It saves a lot of time.
And so, you know, filing an issue and trying to talk to someone in Discord helps a lot.
We're coming to the end and it's become a bit of a tradition around here to ask this one final question. What will be your statement to the Rust community?
I think what it would be is remember to keep things simple for newcomers. I've been doing Rust for about a year now and I just about feel like a beginner. So if we can make it simpler for people joining in, I think that will help with everything.
Very nice final statement. Conrad, thank you so much for taking the time. It was amazing.
Well, thank you for organizing this and great to spend time.
Rostium Production is a podcast by Corot. It is hosted by me, Matthias Endler, and produced by Simon Brüggen. For show notes, transcripts, and to learn more about how we can help your company make the most of Rost, visit corot.dev. Thanks for listening to Rostium Production.