← Back to DevBytes

Zed Extensions/Plugins: Complete Guide

Introduction to Zed Extensions

Zed is a high-performance, multiplayer code editor built in Rust by the creators of Atom. One of its most powerful features is the extension system, which allows developers to extend the editor's functionality with language support, themes, slash commands, and context servers. Unlike traditional plugin systems that can slow down an editor, Zed extensions are designed to be lightweight, sandboxed, and efficient.

In this complete guide, we'll walk through everything you need to know about Zed extensions — from understanding the architecture to building, publishing, and maintaining your own extension.

What Are Zed Extensions?

Zed extensions are modular packages that add capabilities to the Zed editor. They are written primarily in WebAssembly (Wasm) using Rust, which means they run in a sandboxed environment and cannot crash the main editor process. Extensions can provide several types of functionality:

Why Extensions Matter

Zed ships with excellent built-in support for many popular languages, but the ecosystem of programming is vast. Extensions allow the community to fill gaps without waiting for core releases. Because extensions are sandboxed in Wasm, they provide a safe way to distribute third-party code — a malicious or buggy extension cannot access your filesystem arbitrarily or crash the editor.

For developers, writing an extension is also an opportunity to deeply customize your workflow. Whether you want first-class support for a niche language, a custom theme matching your brand, or a slash command that queries your internal documentation, extensions make it possible.

Prerequisites and Setup

Before you can build Zed extensions, you need the following tools installed on your system:

Verify your setup by running:

rustc --version
zed extension --help

If the zed extension command is not available, make sure Zed is installed and its CLI is on your PATH. On macOS, you may need to install the CLI from within Zed via the command palette (zed: install cli).

Extension Project Structure

A typical Zed extension project has the following structure:

my-zed-extension/
├── Cargo.toml
├── extension.toml
├── README.md
├── src/
│   └── lib.rs
├── languages/
│   └── mylang/
│       ├── config.toml
│       ├── highlights.scm
│       ├── grammar.js
│       └── queries/
└── themes/
    └── my-theme.json

The two most important files are extension.toml (the extension manifest) and Cargo.toml (the Rust package manifest). The src/lib.rs file contains the Wasm entry point if your extension includes slash commands or context servers.

The Extension Manifest

The extension.toml file is the heart of your extension. It declares what the extension provides, its metadata, and how it should be built. Here is a complete example:

# extension.toml
id = "my-zed-extension"
name = "My Zed Extension"
version = "0.1.0"
schema_version = 1
description = "Adds language support and a custom theme for MyLang"
authors = ["Your Name <you@example.com>"]
repository = "https://github.com/you/my-zed-extension"

[language_servers.mylang-lsp]
name = "MyLang Language Server"
language = "MyLang"
languages = ["MyLang"]

[grammars.mylang]
repository = "https://github.com/tree-sitter/tree-sitter-mylang"
commit = "a1b2c3d4e5f6..."

[language_servers.mylang-lsp.languages.MyLang]
grammar = "mylang"
language_server = { start_command = "mylang-lsp", stop_command = null }

Key fields explained:

Adding Language Support

Tree-sitter Grammars

Zed uses Tree-sitter for syntax highlighting and code navigation. To add a language, you reference an existing Tree-sitter grammar repository in your extension.toml:

[grammars.mylang]
repository = "https://github.com/tree-sitter/tree-sitter-mylang"
commit = "main"

You then create a language configuration file at languages/mylang/config.toml:

# languages/mylang/config.toml
name = "MyLang"
grammar = "mylang"
path_suffix = ["myl"]
line_comments = ["# "]
block_comments = ["/* ", " */"]
autoclose_before = ";:.,=}])>"
brackets = [
  { start = "{", end = "}", close = true, newline = true },
  { start = "[", end = "]", close = true, newline = true },
  { start = "(", end = ")", close = true, newline = true },
  { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
]

Highlight Queries

Tree-sitter highlighting queries map syntax nodes to highlight scopes. Place them in languages/mylang/highlights.scm:

; highlights.scm
(function_definition
  name: (identifier) @function)

(keyword) @keyword
(string) @string
(comment) @comment
(number) @constant.numeric
(type_identifier) @type

Zed supports a rich set of highlight scopes that map to theme colors, such as @keyword, @string, @function, @type, @constant.numeric, and many more.

Language Server Configuration

If your language has an LSP server, you can configure it in the manifest. Zed will launch the server process and communicate with it over stdio:

[language_servers.mylang-lsp]
name = "MyLang LSP"
language = "MyLang"
languages = ["MyLang"]

[language_servers.mylang-lsp.languages.MyLang]
grammar = "mylang"

You also need to tell Zed how to start the server. This is done in the language's config.toml or via user settings. The simplest approach is to ensure the server binary is on the user's PATH and reference it by name.

Creating Themes

Themes are one of the simplest extension types to create. A theme extension only needs a JSON file describing the colors and an entry in extension.toml. Here is a minimal theme:

{
  "$schema": "https://zed.dev/schema/themes/v1",
  "name": "My Theme",
  "author": "Your Name",
  "themes": [
    {
      "name": "My Theme Dark",
      "appearance": "dark",
      "style": {
        "background": "#1e1e2e",
        "foreground": "#cdd6f4",
        "syntax": {
          "keyword": "#cba6f7",
          "function": "#89b4fa",
          "string": "#a6e3a1",
          "comment": "#6c7086",
          "constant.numeric": "#fab387",
          "type": "#f9e2af"
        },
        "editor.background": "#1e1e2e",
        "editor.foreground": "#cdd6f4",
        "editor.gutter.background": "#181825",
        "editor.active_line.background": "#313244",
        "editor.highlighted_line.background": "#313244"
      }
    }
  ]
}

Reference the theme file in your extension.toml:

[themes]
"themes/my-theme.json" = "My Theme"

Building Slash Commands

Slash commands are custom commands available in Zed's assistant panel. They are implemented in Rust and compiled to Wasm. Here is a complete example of a slash command that fetches documentation for a given symbol.

First, update your Cargo.toml:

[package]
name = "my-zed-extension"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
zed_extension_api = "0.1.0"

Then implement the command in src/lib.rs:

use zed_extension_api::{
    register_slash_command, slash_command::SlashCommandOutput,
    slash_command::SlashCommandRequest, Result,
};

struct MyExtension;

impl zed_extension_api::Extension for MyExtension {
    fn activate(&mut self) {
        register_slash_command("docs");
    }

    fn complete_slash_command(
        &mut self,
        _req: zed_extension_api::slash_command::SlashCommandComplete,
    ) -> Result<Vec<zed_extension_api::slash_command::SlashCommandCompletion>> {
        Ok(vec![])
    }

    fn run_slash_command(
        &mut self,
        req: SlashCommandRequest,
        _worktree: Option<zed_extension_api::Worktree>,
    ) -> Result<SlashCommandOutput> {
        let symbol = req.arguments.first().cloned().unwrap_or_default();
        let text = format!("Documentation for `{}`:\n\nThis is a placeholder. "
            "Replace with real lookup logic.", symbol);
        Ok(SlashCommandOutput { text, ranges: vec![] })
    }
}

zed_extension_api::impl_extension!(MyExtension);

The impl_extension! macro generates the Wasm entry points that Zed calls. When a user types /docs HashMap in the assistant panel, Zed will invoke run_slash_command with HashMap as the first argument.

Context Servers

Context servers provide contextual data to Zed's AI assistant. They are similar to slash commands but are designed to supply background context rather than respond to explicit invocations. A context server implements the ContextServer trait:

use zed_extension_api::{
    register_context_server, ContextServerId, ContextServerCommand,
    ContextServerHandler, Result,
};

struct MyContextServer;

impl ContextServerHandler for MyContextServer {
    fn start(
        &mut self,
        _id: ContextServerId,
        command: ContextServerCommand,
    ) -> Result<()> {
        // Launch the context server process and communicate via stdio
        Ok(())
    }
}

struct MyExtension;

impl zed_extension_api::Extension for MyExtension {
    fn activate(&mut self) {
        register_context_server("my-context-server", MyContextServer);
    }
}

zed_extension_api::impl_extension!(MyExtension);

Building and Testing Locally

To build your extension, navigate to the project directory and run:

zed extension build

This produces a .tar.gz file containing the compiled Wasm binary, grammars, themes, and metadata. To install it locally for testing:

zed extension install ./my-zed-extension-0.1.0.tar.gz

After installation, restart Zed (or reload via the command palette) and verify that your language, theme, or slash command appears. You can also use the zed: extensions command in Zed to browse and manage installed extensions.

For iterative development, you can use the --dev flag to install the extension in development mode, which makes it easier to rebuild and reload:

zed extension install ./my-zed-extension-0.1.0.tar.gz --dev

Publishing to the Extension Registry

Once your extension is ready, you can publish it to Zed's public extension registry. First, ensure your extension.toml has accurate metadata, including a valid repository URL pointing to a public Git repository.

Authenticate with the registry:

zed extension login

This will open a browser to complete OAuth authentication. Once logged in, publish with:

zed extension publish

The CLI will validate your extension, upload it to the registry, and make it available to all Zed users. Subsequent publishes with an incremented version field will push updates.

Best Practices

Pin Grammar Commits

Always pin Tree-sitter grammar repositories to a specific commit hash rather than a branch name. This ensures reproducible builds and prevents breakage when upstream grammars change:

[grammars.mylang]
repository = "https://github.com/tree-sitter/tree-sitter-mylang"
commit = "a1b2c3d4e5f6789..."

Keep Extensions Focused

Each extension should serve a clear, single purpose. Avoid bundling unrelated languages or themes into one extension. This makes it easier for users to find and install exactly what they need, and simplifies maintenance.

Write a Clear README

Your README.md should explain what the extension does, how to install any external dependencies (like language servers), and provide usage examples. Users often discover extensions through the registry, so a good first impression matters.

Test Across Platforms

Zed runs on macOS, Linux, and Windows. If your extension depends on external binaries (such as an LSP server), document the installation process for each platform and consider providing fallback behavior when the binary is missing.

Version Semantically

Follow semantic versioning. Bump the major version when you introduce breaking changes (such as removing a slash command or changing a theme name), the minor version for new features, and the patch version for fixes.

Handle Errors Gracefully

In your Rust code, return meaningful Result errors rather than panicking. Since extensions run in Wasm, a panic will surface as an opaque error to the user. Use descriptive error messages:

fn run_slash_command(
    &mut self,
    req: SlashCommandRequest,
    _worktree: Option<zed_extension_api::Worktree>,
) -> Result<SlashCommandOutput> {
    let symbol = req.arguments.first()
        .ok_or_else(|| "Usage: /docs <symbol>".to_string())?;
    // ... lookup logic
    Ok(SlashCommandOutput { text, ranges: vec![] })
}

Debugging Tips

When things go wrong, here are some strategies:

Conclusion

Zed's extension system strikes a thoughtful balance between power and safety. By leveraging WebAssembly and Tree-sitter, it allows developers to add rich language support, custom themes, and AI-integrated commands without compromising editor stability. Whether you are adding support for an obscure language, crafting a personal theme, or building a slash command that connects the assistant to your team's internal knowledge base, the process is well-defined and approachable. Start small — perhaps with a theme or a simple grammar — and iterate from there. With the manifest, Rust API, and CLI covered in this guide, you have everything you need to build, test, and publish your first Zed extension.

— Ad —

Google AdSense will appear here after approval

← Back to all articles