| name | zed-editor |
| description | Zed Editor extensions - Rust/Wasm plugins, LSP servers, Tree-sitter grammars, themes, MCP servers, slash commands, debug adapters |
| metadata | {"author":"mte90","version":"1.0.0","tags":["zed","editor","extension","rust","wasm","tree-sitter","lsp","plugin"]} |
Overview
Build extensions for the Zed editor — high-performance, multiplayer code editor built in Rust.
Extensions are Rust crates compiled to WebAssembly that run in a sandboxed Wasmtime environment. They can provide languages, themes, debuggers, snippets, MCP servers, and AI slash commands.
Top-Level Metadata
What: This skill covers creating Zed Editor extensions — Rust/Wasm plugins for language support, themes, debuggers, MCP servers, slash commands, and more. Includes extension manifest structure, language server integration, tree-sitter grammar development, and publishing workflows.
Why: Extend Zed to add custom language support, debugging capabilities, AI-powered features, and personalization through themes and snippets. Zed's sandboxed architecture allows safe extension development without compromising core editor stability.
When: Use this skill when developing extensions for Zed, whether for production use, personal productivity tools, or contributing to the Zed ecosystem. Ideal for language developers, editor enthusiasts, and Rust developers building Wasm-based extensions.
Requirements:
- Rust 1.75+ with rustup installed
- Zed Dev edition (installed locally or via AppImage)
cargo install zed_extension_api (latest version from crates.io)
- Tree-sitter CLI (for grammar compilation)
- Node.js 18+ (for npm-based language server installations)
- GitHub account (for publishing extensions)
See also:
Directory Structure
my-extension/
├── extension.toml # Required manifest
├── Cargo.toml # Required for Rust extensions (cdylib)
├── src/
│ └── lib.rs # Extension implementation
├── languages/
│ └── my-language/
│ ├── config.toml # Language metadata
│ ├── highlights.scm # Syntax highlighting
│ ├── brackets.scm # Bracket matching
│ ├── outline.scm # Code outline
│ ├── indents.scm # Auto-indentation
│ ├── injections.scm # Language injections
│ ├── overrides.scm # Editor behavior overrides
│ ├── textobjects.scm # Text objects (Vim)
│ ├── redactions.scm # Screen share redaction
│ ├── runnables.scm # Runnable code detection
│ └── semantic_token_rules.json # LSP semantic tokens
├── themes/
│ └── my-theme.json # Theme definitions
├── icon-themes/
│ └── my-icons.json # Icon theme definitions
└── snippets/
└── snippets.json # Snippet definitions
Extension Manifest (extension.toml)
id = "my-extension"
name = "My Extension"
version = "0.1.0"
schema_version = 1
authors = ["Your Name <you@example.com>"]
description = "Provides support for My Language"
repository = "https://github.com/you/my-zed-extension"
[grammars.my-language]
repository = "https://github.com/tree-sitter/tree-sitter-my-language"
rev = "abc123def456"
[language_servers.my-lsp]
name = "My Language Server"
languages = ["My Language"]
[debug_adapters.my-dap]
schema_path = "debug_adapter_schemas/my-dap.json"
[context_servers.my-mcp]
[agent_servers.my-agent]
name = "My AI Agent"
icon = "icon/agent.svg"
[agent_servers.my-agent.env]
AGENT_LOG_LEVEL = "info"
[agent_servers.my-agent.targets.darwin-aarch64]
archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.tar.gz"
cmd = "./agent"
args = ["--serve"]
sha256 = "abc123..."
[agent_servers.my-agent.targets.linux-x86_64]
archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-linux-x64.tar.gz"
cmd = "./agent"
args = ["--serve"]
Rust Extension Setup
#O|
Cargo.toml
[package]
name = "my-extension"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"
serde = "1.0"
serde_json = "1.0"
Important: Use the latest zed_extension_api version from crates.io. Check compatible Zed versions.
src/lib.rs — Basic Extension
use zed_extension_api as zed;
struct MyExtension;
impl zed::Extension for MyExtension {
fn new() -> Self {
Self
}
}
zed::register_extension!(MyExtension);
Language Server (LSP) Integration
Configuration in extension.toml
[language_servers.my-language-server]
name = "My Language LSP"
languages = ["My Language"]
Implementation
use zed_extension_api as zed;
struct MyExtension {
cached_binary_path: Option<String>,
}
impl zed::Extension for MyExtension {
fn new() -> Self {
Self { cached_binary_path: None }
}
fn language_server_command(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<zed::Command> {
let path = worktree
.which("my-language-server")
.ok_or_else(|| "my-language-server not found in PATH".to_string())?;
Ok(zed::Command {
command: path,
args: vec!["--stdio".to_string()],
env: worktree.shell_env(),
})
}
}
Downloading Language Server from GitHub
fn language_server_command(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<zed::Command> {
let binary_path = format!(
"{}/my-language-server-{}",
worktree.root_path(),
std::env::consts::OS
);
if !std::path::Path::new(&binary_path).exists() {
let release = zed::latest_github_release(
"owner/my-language-server",
zed::GithubReleaseOptions { require_assets: true },
)?;
let (os, arch) = zed::current_platform();
let asset_name = format!("server-{}-{}.tar.gz", os, arch);
let asset = release.assets
.iter()
.find(|a| a.name.contains(&asset_name))
.ok_or_else(|| format!("No release asset matching '{}'", asset_name))?;
zed::download_file(&asset.browser_download_url, &binary_path)?;
zed::make_file_executable(&binary_path)?;
}
Ok(zed::Command {
command: binary_path,
args: vec!["--stdio".to_string()],
env: worktree.shell_env(),
})
}
Using npm Packages
fn language_server_command(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<zed::Command> {
if zed::npm_package_installed_version("my-language-server").is_none() {
zed::npm_install_package("my-language-server")?;
}
Ok(zed::Command {
command: format!("{}/node_modules/.bin/my-language-server",
zed::node_binary_path()),
args: vec!["--stdio".to_string()],
env: worktree.shell_env(),
})
}
LSP Initialization Options
fn language_server_initialization_options(
&mut self,
_language_server_id: &zed::LanguageServerId,
_worktree: &zed::Worktree,
) -> zed::Result<Option<serde_json::Value>> {
Ok(Some(serde_json::json!({
"settings": {
"enableFormatting": true,
"lint": { "enable": true }
}
})))
}
Custom Completion Labels
fn label_for_completion(
&self,
_language_server_id: &zed::LanguageServerId,
completion: zed::lsp::Completion,
) -> Option<zed::CodeLabel> {
Some(zed::CodeLabel {
text: completion.label.clone(),
filter_range: 0..completion.label.len(),
display_range: 0..completion.label.len(),
syntax_highlights: vec![],
})
}
Language Support (Tree-sitter)
Language config.toml
Place in languages/my-language/config.toml:
name = "My Language"
grammar = "my-language"
path_suffixes = ["myl", "mylang"]
line_comments = ["// ", "# "]
block_comments = [{ start = "/*", end = "*/" }]
tab_size = 4
hard_tabs = false
first_line_pattern = "^#!.*myl"
word_characters = ["#", "$", "-"]
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"] },
]
[overrides.string]
completion_query_characters = ["-", "."]
Tree-sitter Query Files
All .scm files go in languages/my-language/.
highlights.scm — Syntax Highlighting
(string) @string
(comment) @comment
(number) @number
(keyword) @keyword
(function name: (identifier) @function)
(type_identifier) @type
(identifier) @variable
(property_identifier) @property
(operator) @operator
(constant) @constant
(boolean) @boolean
Supported captures:
| Capture | Description |
|---|
@string | String literals |
@string.escape | Escaped characters |
@string.regex | Regular expressions |
@string.special | Special strings |
@comment | Comments |
@comment.doc | Doc comments |
@keyword | Keywords |
@number | Numeric values |
@boolean | Boolean values |
@function | Functions |
@type | Types |
@type.builtin | Built-in types |
@variable | Variables |
@variable.special | Special variables |
@variable.parameter | Parameters |
@property | Properties |
@operator | Operators |
@constant | Constants |
@constant.builtin | Built-in constants |
@constructor | Constructors |
@attribute | Attributes |
@tag | Tags |
@label | Labels |
@punctuation | Punctuation |
@punctuation.bracket | Brackets |
@punctuation.delimiter | Delimiters |
@preproc | Preprocessor directives |
@embedded | Embedded content |
@enum | Enumerations |
@variant | Variants |
Fallback captures: Multiple captures on same node define fallback highlights:
(type_identifier) @type @variable
Zed resolves right-to-left: tries @variable first, falls back to @type.
brackets.scm — Bracket Matching
("{" @open "}" @close)
("[" @open "]" @close)
("(" @open ")" @close)
("\"" @open "\"" @close) (#set! rainbow.exclude) ; Exclude from rainbow brackets
outline.scm — Code Outline
(function_definition name: (identifier) @name) @item
(class_definition name: (identifier) @name) @item
(method_definition name: (identifier) @name) @item
Captures: @name (item name), @item (entire item), @context (context info), @annotation (decorators, doc comments).
indents.scm — Auto-Indentation
(array "]" @end) @indent
(object "}" @end) @indent
(function_definition body: (block "{" @indent))
injections.scm — Language Injections
(fenced_code_block
(info_string (language) @injection.language)
(code_fence_content) @injection.content)
((string_content) @injection.content
(#set! injection.language "sql"))
textobjects.scm — Vim Text Objects
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(class_definition
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(comment)+ @comment.around
Captures: @function.around, @function.inside, @class.around, @class.inside, @comment.around, @comment.inside.
redactions.scm — Screen Share Privacy
(pair value: (string) @redact)
(pair value: (number) @redact)
(password_field) @redact
runnables.scm — Runnable Code Detection
(
(document
(object
(pair
key: (string (string_content) @_name
(#eq? @_name "scripts"))
value: (object
(pair
key: (string (string_content) @run))
)
)
)
)
)
)
Extra captures (except _ prefixed) become ZED_CUSTOM_<capture_name> env vars.
MCP Server Extensions
Registration
[context_servers.my-mcp]
Implementation
fn context_server_command(
&mut self,
_context_server_id: &zed::ContextServerId,
_project: &zed::Project,
) -> zed::Result<zed::Command> {
Ok(zed::Command {
command: "my-mcp-server".to_string(),
args: vec!["--stdio".to_string()],
env: std::env::vars().collect(),
})
}
Slash Commands (AI Assistant)
Registration in extension.toml
[[slash_commands.my-command]]
description = "Does something useful"
requires_argument = true
Implementation
fn run_slash_command(
&self,
command: zed::SlashCommand,
args: Vec<String>,
worktree: Option<&zed::Worktree>,
) -> zed::Result<zed::SlashCommandOutput, String> {
match command.name.as_str() {
"my-command" => {
let result = do_something(&args)?;
Ok(zed::SlashCommandOutput {
text: result,
sections: vec![],
attachments: vec![],
})
}
_ => Err(format!("Unknown command: {}", command.name)),
}
}
fn complete_slash_command_argument(
&self,
command: zed::SlashCommand,
_args: Vec<String>,
) -> zed::Result<Vec<zed::SlashCommandArgumentCompletion>> {
Ok(vec![
zed::SlashCommandArgumentCompletion {
label: "option-1".to_string(),
new_text: "option-1".to_string(),
run_command_in_query: false,
},
])
}
Debugger Extensions (DAP)
Registration
[debug_adapters.my-dap]
schema_path = "debug_adapter_schemas/my-dap.json"
[debug_locators.my-locator]
Implementation
fn get_dap_binary(
&mut self,
adapter_name: String,
_config: zed::DebugTaskDefinition,
_user_provided_debug_adapter_path: Option<String>,
worktree: &zed::Worktree,
) -> zed::Result<zed::DebugAdapterBinary, String> {
let path = worktree
.which(&adapter_name)
.ok_or_else(|| format!("{} not found", adapter_name))?;
Ok(zed::DebugAdapterBinary {
command: Some(path),
args: vec![],
env: worktree.shell_env(),
connection: None,
use_tcp: false,
})
}
Theme Extensions
Theme JSON Structure
Place in themes/my-theme.json. Follow schema: https://zed.dev/schema/themes/v0.2.0.json
{
"name": "My Theme Family",
"author": "Your Name",
"themes": [
{
"name": "My Dark Theme",
"appearance": "dark",
"style": {
"background": "#1a1b26",
"foreground": "#a9b1d6",
"accent": "#7aa2f7",
"border": "#292e42",
"border.variant": "#1f2335",
"surface.background": "#1a1b26",
"title_bar.background": "#1a1b26",
"toolbar.background": "#1a1b26",
"editor.background": "#1a1b26",
"editor.foreground": "#a9b1d6",
"editor.gutter.background": "#1a1b26",
"editor.active_line.background": "#292e42",
"editor.line_number": "#3b4261",
"editor.active_line_number": "#a9b1d6",
"terminal.background": "#1a1b26",
"terminal.foreground": "#a9b1d6",
"terminal.ansi.black": "#24283b",
"terminal.ansi.red": "#f7768e",
"terminal.ansi.green": "#9ece6a",
"terminal.ansi.yellow": "#e0af68",
"terminal.ansi.blue": "#7aa2f7",
"terminal.ansi.magenta": "#ad8ee6",
"terminal.ansi.cyan": "#7dcfff",
"terminal.ansi.white": "#a9b1d6",
"syntax": {
"keyword": { "color": "#bb9af7" },
"keyword.control": { "color": "#bb9af7" },
"string": { "color": "#9ece6a" },
"function": { "color": "#7aa2f7" },
"type": { "color": "#7dcfff" },
"comment": { "color": "#565f89", "font_style": "italic" },
"variable": { "color": "#a9b1d6" },
"number": { "color": "#ff9e64" },
"operator": { "color": "#89ddff" },
"property": { "color": "#73daca" },
"constant": { "color": "#ff9e64" },
"tag": { "color": "#f7768e" }
},
"players": [
{ "cursor": "#7aa2f7", "selection": "#283457" }
]
}
},
{
"name": "My Light Theme",
"appearance": "light",
"style": {
"background": "#e1e2e7",
"foreground": "#3760bf",
"editor.background": "#e1e2e7",
"editor.foreground": "#3760bf"
}
}
]
}
Use the Theme Builder to visually design themes.
Snippet Extensions
Snippet JSON Format
Place in snippets/my-language.json:
{
"function": {
"prefix": "fn",
"body": [
"fn ${1:function_name}(${2:args}) -> ${3:ReturnType} {",
" ${4:// TODO: implement}",
"}",
"$0"
],
"description": "Create a new function"
},
"test": {
"prefix": "test",
"body": [
"#[test]",
"fn ${1:test_name}() {",
" ${2:// TODO: write test}",
"}"
],
"description": "Create a test function"
},
"struct": {
"prefix": "struct",
"body": [
"struct ${1:Name} {",
" ${2:field}: ${3:Type},",
"}"
],
"description": "Create a struct"
}
}
Tabstop syntax: $0 (final cursor), ${1:placeholder}, ${2:default_value}.
Extension Capabilities (Security)
Extensions run sandboxed. Users grant capabilities in settings:
{
"granted_extension_capabilities": [
{ "kind": "process:exec", "command": "*", "args": ["**"] },
{ "kind": "download_file", "host": "github.com", "path": ["**"] },
{ "kind": "npm:install", "package": "*" }
]
}
Restricting Capabilities
{
"granted_extension_capabilities": [
{ "kind": "process:exec", "command": "cargo", "args": ["**"] },
{ "kind": "download_file", "host": "github.com", "path": ["owner", "repo", "**"] }
]
}
Capability Kinds
| Capability | Controls |
|---|
process:exec | Execute external commands |
download_file | Download files from URLs |
npm:install | Install npm packages |
To disable all capabilities: "granted_extension_capabilities": []
Development Workflow
Prerequisites
- Rust installed via rustup (not Homebrew or system packages)
- Node.js (for some extensions using npm packages)
Install Dev Extension
- Open Zed → Extensions page → Install Dev Extension
- Select your extension directory
- Dev extension overrides any published version
Debugging
zed --foreground
Test Workflow
Publishing to Zed Marketplace
License Requirements
Extension repositories must include a license file at the root. Accepted:
- Apache 2.0, MIT, BSD 2-Clause, BSD 3-Clause
- CC BY 4.0, GPLv3, LGPLv3, Unlicense, zlib
Naming Rules
- Extension IDs must be unique
- Cannot contain "zed", "Zed", or "extension"
- Theme extensions: suffix with
-theme
- Snippet extensions: suffix with
-snippets
- Language extensions: use the language name (e.g.,
rust, python)
Publishing Process
-
Fork zed-industries/extensions
Fork to a personal account (not org) so Zed staff can push changes to your PR.
-
Add submodule:
git submodule add https://github.com/you/my-zed-extension.git extensions/my-extension
git add extensions/my-extension
- Add to extensions.toml:
[my-extension]
submodule = "extensions/my-extension"
version = "0.1.0"
- Sort entries:
pnpm sort-extensions
- Open PR to
zed-industries/extensions
Updating an Extension
git submodule update --remote extensions/my-extension
Additional Publishing Rules
- Language/debugger extensions must NOT ship binaries — they should download or detect them
- Theme and icon theme extensions must be published separately from language extensions
- If an existing extension has issues, fix it upstream first before creating a new one
Extension API Reference
Key Types
| Type | Description |
|---|
Command | Process command with args and env |
Worktree | Project workspace (read files, find binaries, get env) |
Project | Zed project context |
LanguageServerId | LSP server identifier |
ContextServerId | MCP server identifier |
CodeLabel | Syntax-highlighted text label |
SlashCommand | AI assistant slash command definition |
SlashCommandOutput | Command response with text, sections, attachments |
DebugAdapterBinary | Debug adapter process configuration |
DebugTaskDefinition | Debug launch configuration |
KeyValueStore | Persistent key-value storage |
Key Global Functions
| Function | Description |
|---|
download_file(url, path) | Download file (requires capability) |
latest_github_release(repo, opts) | Get latest GitHub release |
github_release_by_tag_name(repo, tag) | Get specific release by tag |
npm_install_package(pkg) | Install npm package |
npm_package_latest_version(pkg) | Get latest npm version |
npm_package_installed_version(pkg) | Check installed version |
node_binary_path() | Get Node.js binary path |
make_file_executable(path) | Set executable permission |
current_platform() | Get (OS, Architecture) tuple |
Worktree Methods
| Method | Description |
|---|
id() | Worktree identifier |
root_path() | Project root path |
read_text_file(path) | Read file content |
which(binary_name) | Find binary in PATH |
shell_env() | Get shell environment variables |
Common Patterns
Download + Cache Language Server
use zed_extension_api as zed;
use std::path::Path;
struct MyExtension {
cached_path: Option<String>,
}
impl zed::Extension for MyExtension {
fn new() -> Self {
Self { cached_path: None }
}
fn language_server_command(
&mut self,
_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<zed::Command> {
if self.cached_path.is_none() {
let path = self.ensure_server_binary(worktree)?;
self.cached_path = Some(path);
}
let path = self.cached_path.as_ref().unwrap().clone();
Ok(zed::Command {
command: path,
args: vec!["--stdio".to_string()],
env: worktree.shell_env(),
})
}
}
impl MyExtension {
fn ensure_server_binary(&self, worktree: &zed::Worktree) -> zed::Result<String> {
let (os, arch) = zed::current_platform();
let binary_name = format!("my-lsp-{}-{}", os, arch);
let binary_path = format!("{}/.cache/{}", worktree.root_path(), binary_name);
if Path::new(&binary_path).exists() {
return Ok(binary_path);
}
let release = zed::latest_github_release(
"owner/my-lsp",
zed::GithubReleaseOptions { require_assets: true },
)?;
let asset = release.assets.iter()
.find(|a| a.name.contains(&binary_name))
.ok_or_else(|| "No matching release asset".to_string())?;
zed::download_file(&asset.browser_download_url, &binary_path)?;
zed::make_file_executable(&binary_path)?;
Ok(binary_path)
}
}
Check External Tool Availability
fn language_server_command(
&mut self,
_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<zed::Command> {
let local_path = format!("{}/node_modules/.bin/typescript-language-server",
worktree.root_path());
let command = if Path::new(&local_path).exists() {
local_path
} else {
worktree.which("typescript-language-server")
.ok_or_else(|| "typescript-language-server not found. Install with: npm i -g typescript-language-server typescript".to_string())?
};
Ok(zed::Command {
command,
args: vec!["--stdio".to_string()],
env: worktree.shell_env(),
})
}
Best Practices
Extension Structure
src/
├── lib.rs
├── language.rs
├── theme.rs
└── snippets.rs
Performance
fn expensive_computation(&self) -> Result<Value> {
if let Some(cached) = &self.cached {
return Ok(cached.clone());
}
}
fn get_language(&self) -> &Language {
self.language.get_or_init(|| )
}
Testing
#[test]
fn test_extension_loads() {
let ext = MyExtension::new();
assert!(ext.activate().is_ok());
}
Do:
-
Keep extension size under 1MB
-
Use async for I/O operations
-
Test on multiple Zed versions
-
Always verify crate versions on crates.io before adding dependencies — Rust's strict semver means a wrong version can cascade into hundreds of compile errors. Check zed_extension_api and any third-party crate versions explicitly
-
Inform the user when including libraries from third-party repositories and let them check versions. Highlight files and lines where you include such dependencies so the user can double-check
-
When generating Cargo.toml dependencies, mark version numbers as approximate and advise the user to run cargo check immediately to verify compatibility
-
Verify language server binary compatibility — ensure LSP supports --stdio protocol for seamless integration
Don't:
- Block the main thread
- Use heavy dependencies
- Hardcode paths (use API methods)
- Assume a crate version compiles without verifying — always recommend the user runs
cargo check after dependency changes
Known Issues / Pitfalls
No cargo binstall — Zed requires native cargo install
- Zed's extension API does not support
cargo binstall for package installation
- Must use standard
cargo install command for installing language servers and dependencies
- Pre-built binaries from binstall packages may not work correctly
Binary download errors — LSP servers not supporting --stdio will fail
- Language servers must support the
--stdio (stdin/stdout) protocol
- Some LSP servers require TCP socket connection instead
- Binary downloads from GitHub may fail if asset naming doesn't match expected patterns
- Always verify LSP documentation before implementing download logic
Cache invalidation — binary cache at ~/.cache/ needs manual clearing
- Cached LSP binaries stored at
{worktree_root}/.cache/ persist across Zed sessions
- Old/broken cached binaries cause silent failures
- Manual cache clearing required:
rm -rf ~/.cache/zed/{extension_name}/
- Consider implementing cache age checks in production code
No GUI access — extensions cannot create custom panels or modify Zed's UI
- Extensions run in sandboxed Wasmtime environment without GUI context
- Cannot create custom panels, dialogs, or windows
- Cannot access or modify Zed's internal UI components
- All output must be text-based or passed through LSP/Slash Command channels
References
Core Resources:
Protocols & Standards:
Tools & Utilities:
Additional Documentation:
End of Document