ワンクリックで
zed-editor
Zed Editor extensions - Rust/Wasm plugins, LSP servers, Tree-sitter grammars, themes, MCP servers, slash commands, debug adapters
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Zed Editor extensions - Rust/Wasm plugins, LSP servers, Tree-sitter grammars, themes, MCP servers, slash commands, debug adapters
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Develop C++ plugins for Kate text editor using KTextEditor Framework, CMake with ECM, Qt threading, and KDE plugin architecture
Develop plugins, tools, and extensions for OpenCode AI coding agent with MCP, LSP integration, custom tools, and SDK usage
Django security - CSRF protection, authentication, sessions, login/logout, password handling, middleware, protected views
Python testing framework with powerful fixtures, parametrization, extensive plugin ecosystem, and support for async, Django, Flask testing
Python SQL toolkit and ORM with expressive query API, relationship mapping, async support, and Alembic migrations
Common Rust development pitfalls: frequent compiler errors, struct constructor patterns, test organization, and coverage enforcement for reliable codebases.
| 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"]} |
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.
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:
cargo install zed_extension_api (latest version from crates.io)See also:
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
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"
# Tree-sitter grammars
[grammars.my-language]
repository = "https://github.com/tree-sitter/tree-sitter-my-language"
rev = "abc123def456"
#S|
# For local development, use file:// URL
# [grammars.my-language]
# repository = "file:///path/to/tree-sitter-my-language"
#N|
# Language servers
[language_servers.my-lsp]
name = "My Language Server"
languages = ["My Language"]
# Multi-language server with LSP ID mapping
# [language_servers.my-lsp]
# name = "Whatever LSP"
# languages = ["JavaScript", "HTML", "CSS"]
#
# [language_servers.my-lsp.language_ids]
# "JavaScript" = "javascript"
# "TSX" = "typescriptreact"
# "HTML" = "html"
# Debug adapters
[debug_adapters.my-dap]
schema_path = "debug_adapter_schemas/my-dap.json"
# MCP context servers
[context_servers.my-mcp]
#Y|
# Agent servers
[agent_servers.my-agent]
name = "My AI Agent"
icon = "icon/agent.svg"
#X|
[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..."
#S|
[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"]
#O|
[package]
name = "my-extension"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0" # Use latest from crates.io
serde = "1.0"
serde_json = "1.0"
Important: Use the latest
zed_extension_apiversion from crates.io. Check compatible Zed versions.
use zed_extension_api as zed;
struct MyExtension;
impl zed::Extension for MyExtension {
fn new() -> Self {
Self
}
}
zed::register_extension!(MyExtension);
[language_servers.my-language-server]
name = "My Language LSP"
languages = ["My Language"]
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> {
// Option 1: Use binary from PATH
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(),
})
}
}
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(),
})
}
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(),
})
}
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 }
}
})))
}
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![],
})
}
Place in languages/my-language/config.toml:
name = "My Language"
grammar = "my-language" # Must match grammar name in extension.toml
path_suffixes = ["myl", "mylang"] # File extensions
line_comments = ["// ", "# "] # Line comment prefixes
block_comments = [{ start = "/*", end = "*/" }]
tab_size = 4
hard_tabs = false
first_line_pattern = "^#!.*myl" # Shebang detection
word_characters = ["#", "$", "-"] # Non-alpha chars that are part of words
# Bracket auto-closing configuration
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"] },
]
# Scope-specific overrides
[overrides.string]
completion_query_characters = ["-", "."]
All .scm files go in languages/my-language/.
(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.
("{" @open "}" @close)
("[" @open "]" @close)
("(" @open ")" @close)
("\"" @open "\"" @close) (#set! rainbow.exclude) ; Exclude from rainbow brackets
(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).
(array "]" @end) @indent
(object "}" @end) @indent
(function_definition body: (block "{" @indent))
(fenced_code_block
(info_string (language) @injection.language)
(code_fence_content) @injection.content)
((string_content) @injection.content
(#set! injection.language "sql"))
(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.
(pair value: (string) @redact)
(pair value: (number) @redact)
(password_field) @redact
(
(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.
[context_servers.my-mcp]
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.my-command]]
description = "Does something useful"
requires_argument = true
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,
},
])
}
[debug_adapters.my-dap]
schema_path = "debug_adapter_schemas/my-dap.json"
[debug_locators.my-locator]
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,
})
}
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.
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}.
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": "*" }
]
}
{
"granted_extension_capabilities": [
{ "kind": "process:exec", "command": "cargo", "args": ["**"] },
{ "kind": "download_file", "host": "github.com", "path": ["owner", "repo", "**"] }
]
}
| 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": []
# Run Zed in foreground for verbose logs
zed --foreground
# View logs in Zed: open command palette → "zed: open log"
# Log file locations:
# Linux: ~/.config/zed/zed.log
# macOS: ~/Library/Application Support/zed/zed.log
# stdout/stderr from extensions is forwarded to Zed process
# Use println!/dbg! in Rust code, visible with --foreground
# 1. Make changes to extension code
# 2. In Zed: Extensions → click "Install Dev Extension" again to rebuild
# 3. Check Zed.log for errors
# 4. Test language features, LSP, themes, etc.
Extension repositories must include a license file at the root. Accepted:
-theme-snippetsrust, python)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
[my-extension]
submodule = "extensions/my-extension"
version = "0.1.0"
# If extension is in a subdirectory:
# [my-extension]
# submodule = "extensions-my-extension"
# path = "packages/zed"
# version = "0.1.0"
pnpm sort-extensions
zed-industries/extensions# Update submodule to latest commit
git submodule update --remote extensions/my-extension
# Update version in extensions.toml to match extension.toml
# Open PR with the changes
| 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 |
| 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 |
| 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 |
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)
}
}
fn language_server_command(
&mut self,
_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<zed::Command> {
// Prefer local install, fall back to global
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(),
})
}
// ✅ GOOD: Clear module organization
src/
├── lib.rs // Main entry, register languages/themes
├── language.rs // Language server implementation
├── theme.rs // Color definitions
└── snippets.rs // Snippet collections
// ❌ BAD: Everything in one file
// Cache expensive operations
fn expensive_computation(&self) -> Result<Value> {
if let Some(cached) = &self.cached {
return Ok(cached.clone());
}
// ... compute ...
}
// Lazy initialization
fn get_language(&self) -> &Language {
self.language.get_or_init(|| /* ... */)
}
// Test extension loads correctly
#[test]
fn test_extension_loads() {
let ext = MyExtension::new();
assert!(ext.activate().is_ok());
}
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
cargo check after dependency changescargo binstall — Zed requires native cargo installcargo binstall for package installationcargo install command for installing language servers and dependencies--stdio will fail--stdio (stdin/stdout) protocol~/.cache/ needs manual clearing{worktree_root}/.cache/ persist across Zed sessionsrm -rf ~/.cache/zed/{extension_name}/Core Resources:
Protocols & Standards:
Tools & Utilities:
Additional Documentation:
End of Document