← 返回博客

Zed Industries 本周动态:#16

2023 年 8 月 18 日

大家好!

在个人更新之前,我想向公司分享我们开源进展的最新消息。

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

在频道开发的同时,我们一直在修复 GPUI 中原始布局和样式方法的一些痛点。我们希望在投入更多时间记录之前,让 Zed 的 UI 构建比目前状态更易于使用。一旦完成,我们还有许多待编写的文档,以便人们能够有效地使用我们的代码库。

现在,继续您的常规每周更新。

凯尔

对我来说,本周是学习 GPUI 并更好地理解 Zed UI 的重要一周,我们完成了更新的搜索 UI。我们希望这能改善我们当前的搜索体验,我很高兴我们成功实现了它。

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

Antonio

由于我周四才从假期回来,所以这周我的工作时间很短。这周,我主要在考虑如何在 Zed 中为 Prettier 提供一流的支持。我的主要想法是为 formatter 设置提供两个新选项:autoprettier

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

用户还可以使用 prettier 选项手动选择始终使用 prettier 格式化。这可以全局配置,也可以按项目配置,并且可以根据不同的语言进行设置。

内森

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

米凯拉

本周,我一直在 GPUI 中试验一种新的、可组合的原语:组件(Components)。目前,每个 UI 都是独立于 GPUI 为我们提供的元素编写的。这对于灵活性来说是很好的,我们不想在对事物有感觉之前就提交一个不正确的抽象。但现在 Zed 已经发展到成为我们构建 UI 时最大的摩擦点之一。组件通过比视图(Views)或元素(Elements)更简单的契约来解决这个问题,并通过允许样式信息的后期绑定,所有这些都以类型安全的方式通过类型状态(Typestate)模式完成。

那么让我们深入一个例子。请注意,本文的其余部分假设您熟悉 Rust 的泛型,特别是它的关联类型

这是新的组件特性的核心

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

这是一个围绕构建器模式设计的简单特性,注意它接受一个拥有的 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 结构体添加方法来处理所有这些情况,因为我们必须为任何其他交互式或可切换元素重新创建这些字段。我们真正想要的是一种用另一个组件来包装按钮的方法,该组件可以为其选择正确的样式。为此,我们需要一种方法来获取组件的样式信息。让我们创建一个新特性来暴露这些信息。

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

有了这个特性,我们现在可以以类型安全的方式讨论特定组件的样式。这是一个我们可以用它创建的简单 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 特性。

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> 中,但这会导致恐慌(破坏类型安全)或在渲染代码中增加许多令人困惑的分支。我们可以在我们的可样式化组件中添加一个 : Default 特性边界,类似于 Button::new() 将其 border_color 设置为透明黑色。但这不支持复杂的样式属性,例如 Font,它可能被包装在智能指针中或无法在编译时定义。但并非所有希望都破灭了,因为 Rust 为这种情况提供了一种类型:()

(),或单元类型,是一种特殊的类型,它只有一个值,即 ()。如果我们根据其样式参数化我们的元素,我们可以使用 () 作为样式的默认值,然后更新我们的 StylableComponent 特性,以模拟从 Needs a styleA style has been provided 的状态转换。我们来做。

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

请注意这里的两个重大变化:我们从特性定义中删除了 : Component 绑定,并将其移到了新的 Output 关联类型中。这意味着 StylableComponent 只有在被样式化*之后*才需要能够渲染自身。以下是它在 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 约束。让我们将相同的转换应用到我们的 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。

但这正是结对编程的意义所在,对吧?:)

康拉德

我专注于改进 Vim 仿真。本周主要的新功能是可视化块模式!得益于 Zed 出色的多光标支持,我们对原始模式进行了略微改进(因此您不仅限于插入新文本)。为了使其正常工作,我们完全彻底改造了 Vim 选择的工作方式,并在此过程中修复了现有可视化模式中的许多其他小错误。

基里尔

本周我同时处理了两件非常不同的大事。

  • tailwind 语言服务器实验

我得到了一个很好的机会,与 Julia 合作完成了自动补全和一个新的语言服务器,在一个新的 JavaScript 项目上测试了 Zed,并且非常有趣地发现语言服务器的实现方式是多么不同,尽管它们使用了相同的协议。我们已经让自动补全功能正常工作,但魔鬼在于细节,而目前细节非常多,让我们看看什么时候能克服这些。

  • 内联提示悬停

我编写了代码来确定哪个内联提示片段被悬停,这需要考虑编辑器中文本的各种坐标空间,这是对初始内联提示实现期间所写内容的一个有趣的复习。现在我正在处理解析部分,这大概是最后一个大任务,然后我就可以将它们全部整合起来并使其工作。

在处理这两个项目的同时,我设法修复了一些关于终端快捷方式的零碎 bug 和一些其他小问题。

Piotr

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

约瑟夫

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

内特

我一直致力于支持内部频道发布,并协助处理在为 GPUI 构建 UI 时与主题相关的一些复杂性。根据 Nathan 过去几周的更新,很多情况在不久的将来可能会发生变化。我很高兴能开始在内部使用频道,解决一些痛点,然后将其推向世界。

茱莉亚 (Julia)

本周我大部分时间都在与他人协作和协调,主要集中在我们让 Tailwind 在 Zed 中运行起来的努力上。Kirill 已经深入讲解了一些细节,所以我就不多说了,但我很高兴能将这个备受请求的功能交付给我们的用户。

马克斯

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


正在寻找更好的编辑器吗?

您今天就可以在 macOS、Windows 或 Linux 上试用 Zed。立即下载


我们正在招聘!

如果您对我们博客中涵盖的主题充满热情,请考虑加入我们的团队,帮助我们实现软件开发的未来。