Zed Weekly: #26

November 10th, 2023

The current state of Zed under the GPUI2 port
The current state of Zed under the GPUI2 port

Nathan

Our transition to GPUI 2 continues, and we're making good progress. The editor, one of our most complex pieces of UI, is almost fully ported. As expected, the new framework has needed some adjustment as we've begun using it in our application.

This week brought major improvement around our handling of textual input and keyboard events. Here's a sketch of how you could implement some simple keyboard handling for a menu view containing a list of items. Explanation below.

struct Menu {
    items: Vec<SharedString>,
    selected_index: usize,
}
 
actions!(MoveUp, MoveDown);
 
impl Render for Menu {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
        div()
            .id("menu")
            .focusable()
            .key_context("menu")
            .on_action(Menu::move_up)
            .on_action(Menu::move_down)
            .children(self.items.iter().cloned().enumerate().map(|(ix, item)| {
                div()
                    .when(ix == self.selected_index, |div| {
                        div.bg(cx.theme().colors().selected)
                    })
                    .child(item)
            }))
    }
}
 
impl Menu {
    fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
        if self.selected_index > 0 {
            self.selected_index -= 1;
            cx.notify();
        }
    }
 
    fn move_down(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
        if self.selected_index < self.items.len() - 1 {
            self.selected_index += 1;
            cx.notify();
        }
    }
}

To make the Menu struct a view, we implement Render on it. We then enable this view to match key events against the keymap by declaring a key_context of "menu". This will cause us to match bindings in the keymap that require a this context. We then add listeners for the MoveUp and MoveDown actions, which mutate the selected_index and notify the application that this view has changed.

Looking forward to sharing more.

Julia

Earlier this week I was chasing down some issues we've been seeing where NPM, which we download to install Node based language servers like TSServer, is seemingly installing server files to users' home directories when I stumbled upon an even worse issue. Our tsserver integration had completely broken, meaning JS and TS files did not have autocomplete/diagnostics/code-actions/etc.

The TypeScript language server itself does not natively speak the Language Server Protocol (LSP) which we rely on. This is because tsserver actually predates the LSP specification itself. To avoid having to implement the bespoke protocol that tsserver speaks, we use a community project which wraps around tsserver, acting as the intermediary between editors speaking LSP and tsserver speaking its own protocol.

The awesome thing about using existing projects is that we don't have to think too hard about the details about this, someone else already is. That's the core tenet of code sharing and reusable libraries, with the network effect of everyone working on their own thing it can be more efficient overall. However in order for the system to work, individual projects must be able to make the sorts of changes necessary to improve, benefiting the overall system.

That's exactly what had happened here; on Wednesday this wrapper project tagged a new release, which removed a set of deprecated command line flags which we were regrettably still using. Once I figured this out I was able to update our usage, improve some correctness on our side, and the server sprung back to life!

The takeaway is that whenever relying on external projects and code it is important to always remain aware of changes coming down the pipe, especially if it is a component which you cannot reasonably pin the version you are depending on. If we had been more cognizant of what was going on we would have been able to update our usage well before the deprecation became a breaking change.

Kirill

There's so much happening around GPUI2, so all my attempts to work on related things do not look impactful. So instead, I've decided to hold the keep improving our bugs and features, since somebody has to. Interestingly, during that process I did find a few places where I could be helpful with porting old things to GPUI2, I will try to work on this next.

So far — two main streams of work

  • Prettier and bugs around it — now it's more stable, has less ridiculous bugs and supports NPM workspaces. Great to see feedback on the features and knowing that it should help many people now.

  • Diagnostic UI elements and bugs around them — our previous model had deteriorated gracefully, and not very well prepared to "dynamic" ways certain language servers return their diagnostics on the files. I have fixed the most visible pain points, but there's more to deal with.

When I was looking for better ways to (not)show diagnostics in git excluded files, I've got more ideas on how search & indexing of the excluded files could work, which I'm trying to pursue right now.

Nate

Both the gpui2 design+component grind and the learning Rust grind continue. I'm starting to feel like I'm getting a handle on the language–At least enough that I'm not constantly having to bug someone for help, which feels great.

Now that more folks are jumping in rewriting crates for gpui2/zed2, the components we've built thus far are quickly getting stress tested; we are finding the patterns that work and finding the places where there are gaps.

Having the ability to create stories for ui components to test them in isolation has really made it easy to iterate on them. Here is an example of a story of all the possible player colors:

Player Colors
Player Colors

At this point, all themes implement the same set of player colors, but that will change pretty soon.

Here is a really rough example of how a story works:

Player Colors Story Example
Player Colors Story Example

In this case, the "story components" are mostly just theme colors, but you could render any component there.

As we continue to build out the theme, being able to pull up a story to check how things are looking in specific states is really useful.

The development of the new theme marches on as well. Marshall has been a massive help in getting the theme to where I want it. One of the complexities of working with Rust is that it is quite strict about the format the theme is in/what must be provided when building a theme, and if it can be available at compile time.

Because of that, we've been building out a few systems to differentiate between system themes (those fully built in Rust and baked into the app), third-party default themes (external themes that we import and dynamically build as Rust files to bake into the app) and eventual user themes (won't be available at build time, will need to be deserialized and made available on the fly.)

Part of that work led us to build a work-in-progress VSCode theme importer (Marshall wrote more about this below.)

There is a ton to talk about but equally a ton to do, so I'll stop there.

We are super excited to get Zed running on the new gpui in all your hands soon, and equally excited for people to see the work we've been doing on gpui.

Until next time!

Mikayla

I've been flitting around, putting the final touches on my understanding of our new UI framework and trying to bring back some 30,000 lines of test and server code with the new framework. I also worked with max to build a custom element for efficiently displaying a list of uniform element sizes, and am working on putting together the first talk of my career at Aaron Schwartz Day in San Francisco! Next big goal: getting Zed usable as our daily driver :D

Joseph

This week, I began building an AI dashboard, so that we can see how frequently users are interacting with our built-in AI tools. Also in the realm of telemetry, I updated our documentation to be very explicit about the types of events we send and the data that is stored in each.

As a side note, we now are consistently seeing over 1000 weekly active installations:

Weekly active installations
Weekly active installations

An installation is considered "active" for that day if it sends up a single editor event. If that installation is active for at least 3 days within a week, if it considered "active" that week.

I've been working with Zed since the pre-alpha days. At that time, there were probably only 5-10 active users, founders included, and Zed looked like this:

Zed, circa July 5th, 2021
Zed, circa July 5th, 2021

Watching new users take a leap of faith to try something different is incredibly exciting. We have some ground to cover before Zed can become a universally viable editor for all types of developers, but to those who've been actively using Zed, or even just occasionally experimenting, your support means so much to us. Seeing the Zed family grow, and witnessing this editor become bigger than any one of us, is truly inspiring.

Conrad

I've been working on making the nuts and bolts of the new UI framework actually hold the app together! Most of the week was spent rebuilding ctrl-g for "Go To Line". The component itself should only take a few hours, but the joy of being the first feature to use various aspects of the framework was finding all the "gotcha"s, debugging them, and figuring out how we want to fix them.

This has been everything from simple oversights (calling "focus" on the currently focused element shouldn't panic), to subtle concurrency bugs in the test framework (calling a destructor shouldn't cause your test to deadlock!).

I'm looking forward to getting back to making Zed better on its new and improved foundation. No longer will we be "coding at the speed of ham" to use a phrase we coined at the recent offsite.

Marshall

In addition to pairing with folks on various GPUI 2 migration work, I spent some time this week continuing to improve our new theme system.

To give a brief overview of our current theme pipeline in GPUI 2, we have a theme_importer CLI tool that can ingest VS Code themes represented as JSON and emit them as Zed themes represented as Rust source code. These themes are then compiled into the binary for distribution.

Previously we were emitting a complete Zed theme, meaning that color values that weren't specified by the VS Code theme were replaced by the values from the default Zed theme. While this is the behavior we want at runtime, having the default values duplicated into each theme wasn't ideal, and made it hard to see which colors were coming from the theme itself and which ones were coming from the default theme.

I reworked our user themes to model them as overlays on top of a base theme. Now each theme only specifies which values it needs to override from the base theme. Then at runtime we can load a user theme, apply it on top of the base theme, and get a full Zed theme back out. This same mechanism also lays the foundation for loading themes that aren't distributed with Zed by default.

I also took some time to streamline our theme importing process.

The theme_importer depends on the theme2 crate that contains the definition of a theme, but theme2 also contains the themes that are emitted by the theme_importer. This creates a bit of a circular dependency, where compile errors in the emitted themes then cause the theme_importer to fail to compile. This was quite annoying to deal with when iterating on the theme_importer, so it was clear that we needed a better solution.

The solution I arrived at was using Cargo features to conditionally compile the generated themes. The theme2 crate defines an importing-themes feature and will only compile the themes module containing all of the themes when importing-features is disabled:

#[cfg(not(feature = "importing-themes"))]
mod themes;

Then in the Cargo.toml for the theme_importer we set the importing-themes feature to indicate that we're going to be importing themes:

theme = { package = "theme2", path = "../theme2", features = ["importing-themes"] }

This allows us to continue to run the theme_importer even when the themes module is in an uncompilable state.