← 返回博客

Zed Industries 本周动态:#16

2023 年 8 月 18 日


大家好!

在个人更新之前,我想分享一下公司关于开源进展的更新。

今年夏天早些时候,我们宣布了 Zed 打算开源。进展如何? 今天,我们开始在内部使用 Zed 的新频道功能。 一旦我们开源,我们将使用频道来直播我们的开发过程并主持办公时间。 我们计划为我们目前正在进行的每个项目都设置一个频道。 当您调整到频道时,您可以发送消息、进行音频通话、共享屏幕,同时协同编辑您共享到频道中的任何 Zed 项目。

在进行频道工作的同时,我们一直在修复 GPUI 中布局和样式设计的原始方法中的一些痛点。 我们希望在花更多时间编写文档之前,让构建 Zed 的 UI 比当前状态更容易上手。 完成后,我们将编写大量延迟文档,以便人们能够有效地参与我们的代码库。

现在,继续您定期安排的每周更新

Kyle

这对我来说是学习 gpui 并且更好地了解 Zed 的 UI 的重要一周,因为我们完成了更新后的搜索 UI。 我们希望这能比我们当前的搜索体验有所改进,我很高兴我们完成了它。

除了搜索 UI 之外,在 AI 方面还进行了大量的实验。 首先,我们研究了在 Mac 上使用 GPU 加速在本地机器上运行交叉编码器。 很高兴看到本地性能在该领域取得了如此大的进步,我认为我们正处于在更多应用程序中看到本地 LLM 在本地设备上出现的地步。 除了这个分析之外,还对语义搜索引擎进行了大量的探索,努力识别性能和优化机会,以改善用户在大型项目上的体验。 这些是非常具有挑战性的问题,但我们有一些有创意的想法,我们希望这些想法在未来几周内能够产生影响。

Antonio

对我来说是短暂的一周,因为我周四才从假期回来。 本周,我主要考虑如何在 Zed 中为 Prettier 提供一流的支持。 我的主要想法是为 formatter 设置提供两个新选项:autoprettier

auto 选项将成为默认值,这意味着 Zed 将自动尝试推断如何以最佳方式格式化您的代码:如果可以找到 prettier 配置,那么它将使用 prettier,否则将回退到语言服务器。

用户还可以使用 prettier 选项手动选择始终使用 prettier 格式化内容。 这可以在全局范围内配置,也可以在每个项目的基础上配置,并且对于每种语言都可以不同。

Nathan

我在 GPUI 上的努力有所加速。 我的目标是让具有前端编码经验的设计师更容易上手风格和布局。 这促使探索使用 taffy,这是我上周提到的。 以前,我们尝试对树进行单次递归传递来执行布局。 委托给外部引擎简化了元素代码,并为任何具有 Web 经验的人提供了熟悉的样式界面。 在当前设计中,您甚至可以通过链接以 Tailwind CSS 类命名的样式方法来设置元素样式。

Mikayla

本周,我一直在尝试 GPUI 中的一个新的、可组合的基元:组件。 目前,每个 UI 部分都是从 GPUI 为我们提供的元素单独编写的。 这对于灵活性来说非常棒,而且在我们了解情况之前,我们不想承诺采用不正确的抽象。 但现在 Zed 已经发展到成为构建 UI 时最大的摩擦之一。 组件通过拥有比视图或元素更简单的契约来解决这个问题,并通过允许样式信息的后期绑定来解决这个问题,所有这些都是通过类型安全的方式使用类型状态模式完成的。

让我们深入研究一个例子。 请注意,本条目的其余部分假设熟悉 Rust 的 泛型,特别是 关联类型

这是新组件 trait 的核心

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

这是一个围绕构建器模式设计的简单 trait,请注意,这需要一个拥有的 self,这使生命周期和声明保持简单。 您声明您的状态,使用链式方法调用构建它,然后消耗整个状态并将其转换为常规 GPUI 元素。 这是一个简单的 Button 组件

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)
 
    }
}

这是它的使用方法

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()
        )
}

我们的 Button 结构现在封装了代码库其余部分中的按钮定义。 也许按钮可以有图标,也许有一个渐变背景,没有其他人需要担心它究竟是如何实现的,他们只需要提供正确的信息来创建和设置样式。

但仅仅有一些带边框的文本并不能构成一个按钮。 按钮通常会在鼠标悬停时更改颜色,有些是可切换的,有些是禁用的。 我们不能只是继续向我们的 Button 结构添加方法来处理所有这些情况,因为我们必须为任何其他交互式或可切换元素重新创建这些字段。 我们真正想要的是一种用另一个组件包装按钮的方法,该组件可以选择正确的样式。 为此,我们需要一种以某种方式获取组件样式信息的方法。 让我们创建一个新的 trait 来公开此信息

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;
}

有了这个 trait,我们现在可以用类型安全的方式谈论特定组件的样式。 这是一个简单的 Hoverable 组件,我们可以用它来制作

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()
        }
 
    }
}

有一些泛型,但行为很简单。我们有一个 Hover 组件,它包装了另一个组件 C,并将该组件的样式 (C::Style) 应用于它自己的 HoverStyle<C::Style>。让我们为我们的 Button 实现这个新的 StylableComponent trait

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)
    }
}

现在我们可以在主方法中使用它,如下所示

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()
        )
}

可组合性实现了!Button 的实现完全独立于 Hover 的实现,Hover 可以应用于任何其他可能 hoverable() 的东西,而且更好的是,Hover 的实现应该尽可能简单:只有一个 if

但是,我们还没有完成。按钮可能与其他状态组合,但 Hover 本身呢?如果我们想向它添加一个切换状态呢?让我们试试看。这是 Toggle 的定义,类似于 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()
        }
 
    }
}

让我们添加我们的 StylableComponent 实现和构建器方法

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)
    }
}

最后,让我们尝试使用它

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()
        )
}

等等,我们在 '?????' 里放什么?我们不能放一个 HoverStyle,因为我们还不知道我们正在处理哪一个。但是我们仍然必须在那里放 *一些东西*,因为编译器需要知道在 Hover 组件的 style 字段上放什么类型。我们可以把它包装在一个 Option<C::Style> 中,但这样我们要么必须 panic(破坏类型安全),要么在我们的渲染代码中添加许多令人困惑的分支。我们可以添加一个 : Default trait bound 到我们的可样式组件中,类似于 Button::new() 将其 border_color 设置为透明黑色。但这不支持复杂的样式属性,例如 Font,它可能被包装在一个智能指针中,或者无法在编译时定义。但并非所有希望都破灭了,因为 rust 为我们提供了一种用于这种情况的类型:()

(),或 Unit,是一种特殊的类型,它只有一个值,()。如果我们使用样式对我们的元素进行参数化,我们可以使用 () 作为样式的默认值,然后更新我们的 StylableComponent trait 以模拟从 需要样式已提供样式 的状态转换。让我们这样做

pub trait StylableComponent {
    type Style;
    type Output: Component;
 
    fn with_style(self, style: Self::Style) -> Self::Output;
}

请注意这里的两个重大变化:我们从 trait 定义中删除了 : Component bound,并将其移动到新的 Output 关联类型中。这意味着 StylableComponents 在 *样式化之后* 之前不需要能够渲染自己。这是它在 Button 组件上的样子

 
// 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)
 
    }
}
 

还有一些泛型,以及更多的复杂性,但现在我们是类型安全的,而且我们不必添加一个 : Default bound。让我们将相同的转换应用于我们的 Hover 组件

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()
        }
 
    }
}

同样,除了泛型之外,与我们之前的内容没有太大区别。如果我们将相同的转换应用于 Toggle,并重做我们的构建器助手,我们现在可以像这样编写我们的渲染函数

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()
        )
}

现在我们只需要绑定一次我们的样式,只要我们知道组件可以处于的状态,并且我们不会意外地忘记绑定样式,或者绑定错误的样式。并且每个这些组件的实现仍然应该尽可能简单。

也就是说,如果你忘记绑定样式,错误消息可能会有点棘手

A very long compile message from rustc, saying that we didn't implement Component
来自 rustc 的一个非常长的编译消息,说我们没有实现 Component

但这就是结对编程的目的,对吧? :)

Conrad

我专注于改进 Vim 模拟。本周的主要新功能是 Visual Block Mode!感谢 Zed 出色的多光标支持,我们在原始版本上略有改进(因此您不仅限于插入新文本)。为了使它能够很好地工作,我们完全修改了 Vim 选择的工作方式,并在此过程中修复了现有可视化模式中的许多其他小错误。

Kirill

本周同时发生两件大事,而且非常不同

  • tailwind 语言服务器实验

我很高兴能与 Julia 合作完成补全和新的语言服务器,在一个新的 JavaScript 项目上测试 Zed,并且对语言服务器的实现方式如此不同感到非常有趣,尽管使用了相同的协议。我们通常可以使用补全,但细节决定成败,目前细节很多,让我们看看我们什么时候能克服这些。

  • 嵌入提示悬停

我已经编写了代码来确定哪个嵌入提示段被悬停,这需要考虑编辑器中文本的各种坐标空间,并且是对初始嵌入提示实现期间编写的内容的有趣回顾。现在我正在进行解析部分的工作,这大概是把我所有东西粘合在一起并使其工作的最后一件大事。

在处理这两个项目之间,我设法修复了几个关于终端快捷方式的小问题和其他一些小东西。

Piotr

本周我继续研究搜索实现和新的搜索 UI。很快您将体验到更流畅的搜索体验。与此同时,我也开始回顾 PHP 实现,它有很多粗糙的地方。下周我将继续与 Kyle 合作开发语义搜索引擎。

Joseph

我正在继续开发一个 Python 工具,它可以将来自多个来源的反馈收集到一个应用程序中。理想情况下,它能够从 GitHub Issues、PostgreSQL(我们的应用内反馈)、电子邮件以及我们想要的任何其他来源获取数据。我正在编写后端代码。想法是有一个 DataStoreManager 和各种类型的 DataStore,您可以配置并将它们注册到其中。这些 store 实现自己的方法,从各自的来源提取最新数据。管理器将这些方法作为任务运行。完成后,数据将被序列化并存储在 SQLite 数据库中,这样我们就不必每次运行它都提取相同的数据。一旦我完成了这项工作,我将开始研究前端,并尝试引入某种 AI 来生成报告。我打算将它作为一个 TUI 应用程序使用;我正在使用 Textual。我还有很多工作要做,但是再次编写 🐍 代码很有趣。

Nate

我一直在支持频道的内部发布,并帮助解决在使用 GPUI 构建 UI 时使用主题的一些复杂性。正如 Nathan 在过去几周的更新中所述,很多东西在不久的将来可能会发生变化。我很高兴开始在内部使用频道,解决一些痛点,然后将它们发布给全世界。

Julia

本周我花了很多时间与他人协作和协调,主要是为了让 Tailwind 在 Zed 中启动并运行。Kirill 已经详细介绍了一些信息,所以可以说我很高兴能将这个经常被要求的功能交到我们的用户手中。

Max

本周,我们的团队开始使用我们新的频道功能的初始版本,这使得设置和加入 Zed 通话变得更加容易,而无需邀请每位参与者。进入不同的频道与不同的队友进行快速对话已经很有趣了。