zed-editor · v1.0.0 · 2026-07-08 · sha256 b34d61316c9b49bb

zed-editor v1.0.0B

Immutable. This exact content is served forever at /api/v1/blob/b34d61316c9b49bb.

---
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](https://zed.dev) 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](https://zed.dev/download))
- `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**:
- [Zed Extension API Documentation](https://zed.dev/docs/extensions)
- [Publishing Extensions to Marketplace](https://github.com/zed-industries/extensions#publishing-your-extension)
- [Zed Extension API Schema](https://github.com/zed-industries/zed/blob/main/crates/zed_extension_api/src/extension.toml.md)
- [Zed GitHub Repository](https://github.com/zed-industries/zed)
- [crates.io: zed_extension_api](https://crates.io/crates/zed_extension_api)


---

## 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)

```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"

# 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"]
```

---

## Rust Extension Setup
#O|
### Cargo.toml

```toml
[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_api` version from [crates.io](https://crates.io/crates/zed_extension_api). Check [compatible Zed versions](https://github.com/zed-industries/zed/blob/main/crates/extension_api#compatible-zed-versions).

### src/lib.rs — Basic Extension

```rust
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

```toml
[language_servers.my-language-server]
name = "My Language LSP"
languages = ["My Language"]
```

### Implementation

```rust
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(),
        })
    }
}
```

### Downloading Language Server from GitHub

```rust
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

```rust
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

```rust
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

```rust
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`:

```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 = ["-", "."]
```

### Tree-sitter Query Files

All `.scm` files go in `languages/my-language/`.

#### highlights.scm — Syntax Highlighting

```scheme
(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:
```scheme
(type_identifier) @type @variable
```
Zed resolves right-to-left: tries `@variable` first, falls back to `@type`.

#### brackets.scm — Bracket Matching

```scheme
("{" @open "}" @close)
("[" @open "]" @close)
("(" @open ")" @close)
("\"" @open "\"" @close) (#set! rainbow.exclude)  ; Exclude from rainbow brackets
```

#### outline.scm — Code Outline

```scheme
(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

```scheme
(array "]" @end) @indent
(object "}" @end) @indent
(function_definition body: (block "{" @indent))
```

#### injections.scm — Language Injections

```scheme
(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

```scheme
(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

```scheme
(pair value: (string) @redact)
(pair value: (number) @redact)
(password_field) @redact
```

#### runnables.scm — Runnable Code Detection

```scheme
(
    (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

```toml
[context_servers.my-mcp]
```

### Implementation

```rust
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

```toml
[[slash_commands.my-command]]
description = "Does something useful"
requires_argument = true
```

### Implementation

```rust
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

```toml
[debug_adapters.my-dap]
schema_path = "debug_adapter_schemas/my-dap.json"

[debug_locators.my-locator]
```

### Implementation

```rust
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

```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](https://zed.dev/theme-builder) to visually design themes.

---

## Snippet Extensions

### Snippet JSON Format

Place in `snippets/my-language.json`:

```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:

```json
{
    "granted_extension_capabilities": [
        { "kind": "process:exec", "command": "*", "args": ["**"] },
        { "kind": "download_file", "host": "github.com", "path": ["**"] },
        { "kind": "npm:install", "package": "*" }
    ]
}
```

### Restricting Capabilities

```json
{
    "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](https://www.rust-lang.org/tools/install) (not Homebrew or system packages)
- Node.js (for some extensions using npm packages)

### Install Dev Extension

1. Open Zed → Extensions page → **Install Dev Extension**
2. Select your extension directory
3. Dev extension overrides any published version

### Debugging

```bash
# 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
```

### Test Workflow

```bash
# 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.
```

---

## 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

1. **Fork** [zed-industries/extensions](https://github.com/zed-industries/extensions)
   > Fork to a personal account (not org) so Zed staff can push changes to your PR.

2. **Add submodule**:
```bash
git submodule add https://github.com/you/my-zed-extension.git extensions/my-extension
git add extensions/my-extension
```

3. **Add to extensions.toml**:
```toml
[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"
```

4. **Sort entries**:
```bash
pnpm sort-extensions
```

5. **Open PR** to `zed-industries/extensions`

### Updating an Extension

```bash
# 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
```

### 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

```rust
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

```rust
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(),
    })
}
```

## Best Practices

### Extension Structure

```rust
// ✅ 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
```

### Performance

```rust
// 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(|| /* ... */)
}
```

### Testing

```rust
// Test extension loads correctly
#[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](https://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:**

- **Zed Editor Website**: https://zed.dev
- **Zed Extension API Docs**: https://zed.dev/docs/extensions
- **Extension Manifest Schema**: https://github.com/zed-industries/zed/blob/main/crates/zed_extension_api/src/extension.toml.md
- **Publishing Guide**: https://github.com/zed-industries/extensions
- **Zed GitHub Repository**: https://github.com/zed-industries/zed
- **crates.io: zed_extension_api**: https://crates.io/crates/zed_extension_api

**Protocols & Standards:**

- **Language Server Protocol (LSP)**: https://microsoft.github.io/language-server-protocol/
- **Model Context Protocol (MCP)**: https://modelcontextprotocol.io/
- **Debug Adapter Protocol (DAP)**: https://microsoft.github.io/debug-adapter-protocol/

**Tools & Utilities:**

- **Tree-sitter CLI**: https://tree-sitter.github.io/tree-sitter/
- **cargo install**: https://doc.rust-lang.org/cargo/commands/cargo-install.html
- **npm CLI**: https://docs.npmjs.com/cli
- **GitHub Releases API**: https://docs.github.com/en/rest/releases/releases

**Additional Documentation:**

- **Zed Theme Builder**: https://zed.dev/theme-builder
- **Zed Extension API Rust Docs**: https://docs.rs/zed_extension_api/latest/zed_extension_api/
- **Extension Naming Guide**: https://github.com/zed-industries/extensions#naming


---

**End of Document**