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:
- Language support — syntax highlighting via Tree-sitter grammars, Language Server Protocol (LSP) integration, and language-specific settings.
- Themes — custom color schemes for the editor UI and syntax highlighting.
- Slash commands — custom commands that can be invoked in the assistant panel, prefixed with a slash (e.g.,
/docs). - Context servers — providers that supply contextual information to Zed's AI assistant.
- Indexed language providers — tools that index codebases for enhanced AI context.
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:
- Rust — the Rust toolchain, installable via rustup.
- Node.js — required for some build tooling and for compiling Tree-sitter grammars.
- Zed — the editor itself, available from zed.dev.
- Extension CLI — Zed includes a built-in extension CLI accessible via
zed extension(orzed: extensionin the command palette).
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:
id— a unique, lowercase, hyphenated identifier for your extension.schema_version— always1for current extensions.[grammars.*]— declares Tree-sitter grammars to compile and bundle.[language_servers.*]— declares language servers that the extension configures.
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:
- Check the Zed logs — open the log file via the command palette (
zed: open log). Extension errors are logged here. - Validate your manifest — run
zed extension buildand read any warnings carefully. The CLI catches many common mistakes. - Test grammars in isolation — use the
tree-sitterCLI to test your grammar and queries before bundling them into an extension. - Use
println!in Wasm — output fromprintln!in your extension code appears in the Zed log, which is useful for debugging slash commands and context servers.
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.