This Week at Zed Industries: #16

Nathan Sobo

Nathan Sobo

August 18th, 2023

Hey everyone!

Before the solo updates, I wanted to share an update for the company about our progress toward open source.

Earlier this summer we announced Zed's intent to go open source. How is that going? Today, we began using Zed's new channels feature internally. Once we open source, we'll use channels to stream our development and host office hours. We plan to have a channel for each of the various projects we're currently pursuing. When you tune to a channel, you can send messages, talk over audio, share your screen, all while collaboratively editing any Zed project you share into the channel.

In parallel with the channels effort, we've been fixing some pain points in our original approach to layout and styling in GPUI. We want to make building UI for Zed more approachable than its current state before we spend more time documenting it. Once that's done, we have a lot of deferred documentation to write to enable people to engage effectively with our codebase.

Now, on with your regularly scheduled weekly update:

Kyle

This was a big week for me learning gpui and understanding Zed's UI a lot better as we finished the updated Search UI. We are hopeful this offers an improvement over our current search experience, and I'm happy we got it over the line.

Beyond the Search UI, there was alot of experimentation on the AI side. First, we took a look at running cross-encoders locally on our machine with GPU acceleration on Mac. Its great to see how far performance locally has come in the space, and I think we are at a point where we will start to see local LLMs making an appearance locally on device in more applications. Along with this analysis, there was alot of exploration done in the Semantic Search engine, working to identify performance and optimization opportunities to improve the experience for our users when working on large projects. These are really challenging problems, but we've got a few creative ideas which we hope will make an impact over the coming weeks.

Antonio

Short week for me as I came back from vacation on Thursday. This week, I've been mainly thinking about how to provide first-class support for Prettier in Zed. My main thought is to provide two new options for the formatter setting: auto and prettier.

The auto option would become the default, and means that Zed will automatically try to infer how to best format your code: if a prettier config can be found, then it will use prettier, and fall back to a language server otherwise.

Users can also manually opt into always formatting things with prettier using the prettier option. This can be configured globally, or on a per-project basis, and can be different for each language.

Nathan

My efforts on GPUI have accelerated. My goal is to make style and layout approachable to designers with front-end coding experience. This is prompting explorations into using taffy, which I mentioned last week. Previously, we attempted to perform layout in a single recursive pass over the tree. Delegating to an external engine simplifies element code, and provides anyone with web experience a familiar styling interface. In the current design, you can even style elements by chaining methods named after Tailwind CSS classes.

Mikayla

This week, I've been experimenting with a new, composable primitive in GPUI: Components. Right now every piece of UI is written individualy from the Elements that GPUI provides for us. This is great for flexibility and we didn't want to commit to an incorrect abstraction before we'd gotten a feel for things. But now Zed has grown to the point that it's become one our largest friction when building UI. Components solve this by having a far simpler contract than either Views or Elements, and by allowing late-binding of styling information, all done in a type-safe way with the Typestate pattern.

So let's dive into an example. Note that the rest of this entry assumes a familiarity with Rust's generics and in particular it's associated types.

Here's the core of the new Component trait:

pub trait Component {
    fn render(self) -> Element;
}

It's a simple trait that's designed around the builder pattern, note that this takes an owned self, this keeps the lifetimes and declarations simple. You declare your state, build it up with chained method calls, and then consume the whole thing and turn it into a regular GPUI Element. Here's a simple Button component:

pub struct Button {
    text: String,
    border_color: Color,
}

impl Button {
    fn new(text: String) -> Self {
        Self {
            text,
            border_color: Color::new(0, 0, 0, 0),
        }
    }

    fn with_border_color(self, color: Color) -> Self {
        self.border_color = color;
        self
    }
}

impl Component for Button {
    fn render(self) -> Element {
        Label::new(self.text)
            .contained()
            .with_border_color(self.border_color)

    }
}

And here's how you'd use it:

fn render() -> Element {
    Flex::new()
        .with_child(
            Button::new("first button".to_string())
                .with_border_color(Color::new(255, 0, 0, 1))
                .render()
        )
        .with_child(
            Button::new("second button".to_string())
                .with_border_color(Color::new(0, 255, 0, 1))
                .render()
        )
}

Our Button struct has now encapsulated the definition of a button from the rest of the codebase. Maybe buttons can have icons, maybe there's a gradient background, no one else has to worry about exactly how it's implemented they just need to provide the correct information to create and style it.

But just having some text with a border does not a button make. Buttons often change colors on mouse hover, some are toggleable, some are disabled. We can't just keep adding methods to our Button struct to handle all of these cases, as we'd have to recreate these fields for any other interactive or toggleable element. What we'd really like is a way to wrap a button with another component which can select the correct styles for it. To do so, we'll need a way to get at a component's styling information somehow. Let's a make a new trait to expose this information:

pub trait StylableComponent: Component {
    // Note that this is an associated type to create a 1:1 mapping
    // between a component and its style
    type Style;

    fn with_style(self, style: Self::Style) -> Self;
}

With this trait, we can now talk about a specific component's styling in a type safe way. here's a simple Hoverable component we can make with this:

pub struct HoverStyle<S> {
    default: S,
    hovered: S,
}

pub struct Hover<C: StylableComponent> {
    child: C,
    style: HoverStyle<C::Style>
}

impl<C: StylableComponent> Hover<C> {
    fn new(child: C, style: HoverStyle<C::Style>) -> Self {
        Self {
            child,
            style,
        }
    }
}

impl<C: StylableComponent> Component for Hover<C> {
    fn render(self) -> Element {
        if app_context::is_hovered() {
            self.child.with_style(self.style.hovered).render()
        } else {
            self.child.with_style(self.style.default).render()
        }

    }
}

Theres a few generics flying around, but the behavior is quite simple. We have a Hover component, which wraps some other component C, and applies that component's style (C::Style) to it's own HoverStyle<C::Style>. Let's implement this new StylableComponent trait for our Button:

impl StylableComponent for Button {
    type Style = Color;

    fn with_style(self, color: Color) -> Self {
        self.with_border_color(color)
    }
}

// Add a builder method to make the example cooler
impl Button {
    fn hoverable(self, hover_style: HoverStyle<Color>) {
        Hover::new(self, hover_style)
    }
}

And now we can use it in our main method like so:

fn render() -> Element {
    Flex::new()
        .with_child(
            Button::new("first hoverable button".to_string())
                .hoverable(
                    HoverStyle {
                        default: Color::new(0, 255, 0, 1),
                        hovered: Color::new(0, 0, 255, 1)
                    }
                )
                .render()
        )
        .with_child(
            Button::new("second hoverable button".to_string())
                .hoverable(
                    HoverStyle {
                        default: Color::new(0, 255, 0, 1),
                        hovered: Color::new(0, 0, 255, 1)
                    }
                )
                .render()
        )
}

Composability achieved! The implementation of the Button is completely independent from the implementation of the Hover, the Hover can be applied to anything else that might be hoverable(), and even better the implementation of Hover is exactly as simple as it should be: a single if.

But, we're not done yet. The button might be composable with different states, but what about the Hover itself? What if we wanted to add a toggle state to it? Well let's try it. Here's the definition of Toggle, similar to Hover:

pub struct ToggleStyle<S> {
    active: S,
    inactive: S,
}

pub struct Toggle<C: StylableComponent> {
    child: C,
    is_active: bool,
    style: ToggleStyle<C::Style>
}

impl<C: StylableComponent> Toggle<C> {
    pub fn new(child: C, is_active: bool, style: ToggleStyle<C::Style>) -> Self {
        Self {
            child
            is_active,
            style,
        }
    }
}

impl<C: StylableComponent> Component for Toggle<C> {
    fn render(self) -> Element {
        if self.is_active {
            self.child.with_style(self.style.hovered).render()
        } else {
            self.child.with_style(self.style.default).render()
        }

    }
}

And let's add our StylableComponent implementation and builder method:

impl<C: StylableComponent> StylableComponent for Hover<C> {
    type Style = HoverStyle<C::Style>;

    fn with_style(self, hover_style: Self::Style) -> Self {
        self.style = hover_style;
        self
    }
}

// We could do a default trait implementation for this, but that's out of scope
impl<C: StylableComponent> Hover<C> {
    fn toggleable(self, style: ToggleStyle<HoverStyle<C::Style>>) {
        Toggle::new(self, hover_style)
    }
}

And finally, let's try to use it:

fn render() -> Element {
    let is_active = true;

    Flex::new()
        .with_child(
            Button::new("hoverable button".to_string())
                .hoverable(
                    HoverStyle {
                        default: Color::new(0, 255, 0, 1),
                        hovered: Color::new(0, 0, 255, 1)
                    }
                )
                .render()
        )
        .with_child(
            Button::new("toggleable and hoverable button".to_string())
                .hoverable(
                    ??????????????????? // <- What do we put here?
                )
                .toggleable(
                    is_active,
                    ToggleStyle {
                        active: HoverStyle {
                            default: Color::new(0, 0, 0, 1),
                            hovered: Color::new(255, 255, 255, 1)
                        },
                        inactive: HoverStyle {
                            default: Color::new(255, 255, 255, 1),
                            hovered: Color::new(0, 0, 0, 1)
                    }
                })
                .render()
        )
}

Wait, what do we put in the '?????'? We can't put a HoverStyle because we don't know which one we're dealing with yet. But we still have to put something in there because the compiler needs to know what type to put on the Hover component's style field. We could wrap this in an Option<C::Style>, but then we'd either have to panic (wrecking type safety) or add lots of confusing branches into our rendering code. We could add a : Default trait bound into our stylable component, analogous to the way Button::new() sets it's border_color to transparent black. But this wouldn't support complex styling properties like Font, which might be wrapped in a smart pointer or be impossible to define at compile time. But all hope is not lost, because rust provides us with a type for exactly this situation: ().

(), or Unit, is a special type which has exactly one value, (). If we parameterize our elements over their style, we can use () as the default value for the style, and then update our StylableComponent trait to model the state transition from Needs a style to A style has been provided. Let's do that:

pub trait StylableComponent {
    type Style;
    type Output: Component;

    fn with_style(self, style: Self::Style) -> Self::Output;
}

Note two big changes here: we took the : Component bound off of the trait definition and moved it into the new Output associated type. This means that StylableComponents don't need to be able to render themselves until after they've been styled. Here's what it looks like on the Button component:


// Note the new generic parameter
pub struct Button<S> {
    text: String,
    border_color: S,
}

// Note that this is implementing `new()` for a *specific kind* of button,
// buttons whose styling is `()` (Unit). Because of module privacy, the
// only way to create a button is with it's style 'unbound' (bound to `()`).
impl Button<()> {
    pub fn new(text: String) -> Self {
        Self {
            text,
            border_color: (),
        }
    }
}


impl StylableComponent for Button<()> {
    type Style = Color;
    type Output = Button<Self::Style>;

    // Note that we now return a *different* kind of button, one whose style
    // type is now bound to `Color`.
    fn with_style(self, color: Color) -> Self {
        Button {
            text: self.text,
            border_color: color,
        }
    }
}


// Note that this is only implemented for buttons with their style bound to
// `Color`. Trying to render a button whose style is `()` will result in a
// compile error.
impl Component for Button<Color> {
    fn render(self) -> Element {
        Label::new(self.text)
            .contained()
            .with_border_color(self.border_color)

    }
}

There's a few more generics, and a little bit more complexity, but now we are type safe, and we don't have to add a : Default bound. Let's apply the same transformation to our Hover component:

pub struct HoverStyle<S> {
    default: S,
    hovered: S,
}

pub struct Hover<C, S> {
    child: C,
    style: HoverStyle<S>
}

// Same as before, you can only create an unstyled Hover
// component.
impl<C: StylableComponent> Hover<C, ()> {
    fn new(child: C) -> Self {
        Self {
            child
            style: HoverStyle {
                default: (),
                hovered: (),
            },
        }
    }
}

// Like last time, note that this is only implemented for
// unstyled Hover components.
impl<C: StylableComponent> StylableComponent for Hover<C, ()> {
    type Style = HoverStyle<C::Style>;
    type Output = Hover<C, Self::Style>;

    fn with_style(self, style: Self::Style) -> Self {
        Hover {
            child: self.child,
            style
        }
    }
}


// Also like last time, this is only implemented for Hover
// components whose style is bound to the correct type.
impl<C: StylableComponent> Component for Hover<C, HoverStyle<C::Style>> {
    fn render(self) -> Element {
        if app_context::is_hovered() {
            // Since our subcomponent is also a `StylableComponent`, we _must_
            // make sure to style it before the compiler will let us render it.
            self.child.with_style(self.style.hovered).render()
        } else {
            self.child.with_style(self.style.default).render()
        }

    }
}

Again, not too different than what we had before, barring the generics. If we also apply this same transformation to Toggle, and rework our builder helpers, we can now write our render function like this:

fn render() -> Element {
    let is_active = true;

    Flex::new()
        .with_child(
            Button::new("hoverable button".to_string())
                .hoverable()
                .with_style(HoverStyle {
                    default: Color::new(0, 255, 0, 1),
                    hovered: Color::new(0, 0, 255, 1)
                })
                .render()
        )
        .with_child(
            Button::new("Toggleable and hoverable button".to_string())
                .hoverable()
                .toggleable(is_active)
                .with_style(
                    ToggleStyle {
                        active: HoverStyle {
                            default: Color::new(0, 0, 0, 1),
                            hovered: Color::new(64, 64, 64, 1)
                        },
                        inactive: HoverStyle {
                            default: Color::new(128, 128, 128, 1),
                            hovered: Color::new(255, 255, 255, 1)
                        }
                    }
                )
                .render()
        )
}

Now we only have to bind our styles once, as soon as we know the states a component can be in, and we can't accidentally forget to bind a style, or bind the wrong one. And the implementation of each of these components is still exactly as simple as it should be.

That said, the error messages can get a bit hairy if you do forget to bind a style:

A very long compile message from rustc, saying that we didn't implement Component
A very long compile message from rustc, saying that we didn't implement Component

But that's what pair programming is for, right? :)

Conrad

I am focused on improving Vim emulation. The major new feature this week is Visual Block Mode! We slightly improve on the original thanks to Zed's excellent multi-cursor support (so you're not just limited to inserting new text). To make this work well we completely overhauled the way Vim selections work, and along the way fixed a bunch of other small bugs in the existing visual modes.

Kirill

Two big and very different things happened concurrently for me this week:

  • tailwind language server experiments

I've got a great chance to pair with Julia on completions and a new language server, test Zed on a new JavaScript project and get very amused at how differently language servers can be implemented, albeit using the same protocol. We have the completions working in general, but the devil is in the details and those are abundant at the moment, let's see when we get over those.

  • inlay hint hover

I have the code to derermine which inlay hint segment is hovered, which required considering in various coordinate spaces of the text in the editor and was an interesting refresher of what was written during the initial inlay hint implementation. Now I work on the resolve part, presumably the last big task before I can glue it all together and make it work.

Between dealing with the two projects, I've managed to fix a couple papercut bugs with terminal shortcuts and a few other small things.

Piotr

This week I have continued poking at search implementation and new search UI. Soon you'll experience a smoother search experience. In the meantime I have also started looking back at the PHP implementation which has quite a few rough edges. In the next week I will continue working on Semantic Search engine with Kyle.

Joseph

I'm back to working on the Python tool that pulls all feedback from multiple sources into a single application. Ideally, it will be able to pull in data from GitHub Issues, PostgreSQL (our in-app feedback), email, and anything else we want. I'm in the middle of writing the backend code. The idea is that there's a DataStoreManager and various types of DataStores that you can configure and register with it. The stores implement their own methods to pull fresh data down from their respective sources. The manager runs these methods as tasks. After completion, the data is serialized and stored in a SQLite database, so we don't have to pull the same data each time we run it. Once I get this working, I'll start working on the frontend and trying introducing some sort of AI to make reports. I intend for this to be a TUI application; I'm using Textual. I have quite a bit of work ahead of me, but it's fun to be writing some ๐Ÿ again.

Nate

I've been supporting the internal launch of channels, and helping navigate some of the complexities of working with our theme when build UI for GPUI. A lot of this may change in the near future, as per Nathan's updates the past few weeks. I'm excited to start using channels internally, resolve soem of the pain points, and then get them out to the world.

Julia

Been spending a lot of my time this week collaborating and coordinating with others, primarily on our efforts to get Tailwind up and running in Zed. Kirill already went into some good detail so it'll suffice to say I'm excited to get this oft requested feature in the hands of our users.

Max

This week, our team started using an initial version of our new channels feature, which makes it easier to set up and join Zed calls, without having to invite every participant. It's already been fun to pop into different channels for quick conversations with different teammates.