SuperAPI AI Directory

zed-editor

Coding & development

View repository

Updated 10-15 20:13

About this skill

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"

# 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

[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. 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> {
        // 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

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

(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

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

Debugging

# 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

# 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

    Fork to a personal account (not org) so Zed staff can push changes to your PR.

  2. Add submodule:

git submodule add https://github.com/you/my-zed-extension.git extensions/my-extension
git add extensions/my-extension
  1. Add to extensions.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"
  1. Sort entries:
pnpm sort-extensions
  1. Open PR to zed-industries/extensions

Updating an Extension

# 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

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> {
    // 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

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

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

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