How Zed's Snippet System Works: A Complete Guide to Adding Custom Snippets

Zed implements snippets as JSON-formatted template files stored in ~/.config/zed/snippets that the core snippet crate parses into structured templates with interactive tabstops, supporting LSP-style ${1:placeholder} syntax and choice lists for dynamic code expansion.

The zed-industries/zed repository provides a sophisticated snippet architecture that bridges user-defined JSON configurations with runtime code expansion. Understanding how Zed's snippet system works enables developers to create reusable code templates that integrate seamlessly with the editor's completion engine and language server protocol support.

Storage Architecture and File Discovery

Snippets reside in ~/.config/zed/snippets (resolved via paths::snippets_dir()). The system distinguishes between global snippets and language-specific snippets through a scope-based file naming convention implemented in crates/snippets_ui/src/snippets_ui.rs.

Global snippets live in snippets.json (defined by the constant GLOBAL_SCOPE_FILE_NAME), while language-specific snippets use <language-id>.json where the ID corresponds to the language's LSP identifier (language.lsp_id()). When you trigger the Snippets → Configure Snippets command (actions ConfigureSnippets and OpenFolder in snippets_ui.rs), the UI scans the directory to build existing_scopes and presents them in a fuzzy picker via ScopeSelectorDelegate::new (lines 60-74).

The ScopeName to filename conversion happens through the From<ScopeName> for ScopeFileName implementation (lines 37-44), which appends .json via ScopeFileName::with_extension (lines 28-32).

Parsing Engine and Tabstop Resolution

When completion triggers, the snippet crate (specifically crates/snippet/src/snippet.rs) processes raw snippet strings through Snippet::parse (lines 17-41). The parser implements a state machine that:

  1. Collects raw source and iterates characters in parse_snippet
  2. Detects tabstops when encountering $, delegating to parse_tabstop
  3. Handles escapes for \, $, and } per LSP specifications (lines 58-66)
  4. Processes choice lists via parse_choices (lines 45-78) for syntax like ${1|a,b,c|}
  5. Manages nested placeholders and final tabstop $0 insertion

The resulting Snippet struct contains text (the expanded code with placeholders removed) and an ordered tabstops vector defining cursor positions and choice lists. The editor uses this to position the caret, link mirrored placeholders, and render choice dropdowns.

How to Add Custom Snippets in Zed

Creating Global Snippets

To add a global snippet available across all file types:

  1. Open the snippets folder via the command palette (Snippets → Open Folder) or action snippets::OpenFolder
  2. Create or edit snippets.json:
{
  "Print Debug": {
    "prefix": "dbg",
    "body": ["println!(\"Debug: ${1:msg}\");", "$0"],
    "description": "Quick debug print"
  }
}

The prefix triggers completion, $1 defines the first tabstop with default text "msg", and $0 marks the final cursor position.

Adding Language-Specific Snippets

For TypeScript or JavaScript snippets, create javascript.json (which also covers JSX):

{
  "React Component": {
    "prefix": "rcomp",
    "body": [
      "export const ${1|Component,Container|} = () => {",
      "  return <div>$0</div>;",
      "};"
    ],
    "description": "Skeleton for a functional React component"
  }
}

The ${1|Component,Container|} syntax creates a dropdown choice list parsed by parse_choices, allowing users to select between alternatives at expansion time.

Programmatic Snippet Parsing

For developers extending Zed's functionality, the snippet parser exposes a clean Rust API:

use snippet::Snippet;

fn expand_snippet(source: &str) -> anyhow::Result<String> {
    // Parse the raw snippet definition
    let parsed = Snippet::parse(source)?;
    // `parsed.text` is the snippet with placeholders removed
    Ok(parsed.text)
}

// Example
let result = expand_snippet(r#"console.log("${1:msg}")${0}"#)?;
assert_eq!(result, r#"console.log("")"#);

This matches the internal implementation used when the completion engine receives snippet-type completions from language servers or Zed's simple-completion-language-server.

Advanced Snippet Syntax

Zed supports the full LSP snippet specification implemented in the parsing functions:

  • Tabstops: $1, $2 define cursor positions
  • Placeholders: ${1:default} provide default text
  • Choice lists: ${1|opt1,opt2|} render UI dropdowns
  • Final tabstop: $0 or implicit end-of-snippet positioning (lines 24-40 in snippet.rs)
  • Escaping: \\$ and \\} render literal characters

The parser validates nested structures and automatically inserts an implicit final tabstop if $0 is omitted.

Summary

  • Storage: Snippets live in ~/.config/zed/snippets as JSON files, with global snippets in snippets.json and language-specific ones in <lsp_id>.json
  • UI Layer: The snippets_ui crate provides ConfigureSnippets and OpenFolder actions for file discovery and editing, mapping scopes to filenames via ScopeFileName::with_extension
  • Parsing: The snippet crate's Snippet::parse function processes LSP-style syntax including tabstops, placeholders, and choice lists into structured TabStop data
  • Expansion: Parsed snippets yield text and tabstops vectors that drive cursor positioning, linked placeholder updates, and interactive choice selection in the editor

Frequently Asked Questions

Where does Zed store custom snippets?

Zed stores snippets in ~/.config/zed/snippets (resolved via paths::snippets_dir()). Global snippets use snippets.json, while language-specific files follow the naming convention <language-lsp-id>.json as determined by the language server's identifier and the ScopeFileName mapping in snippets_ui.rs.

What snippet syntax does Zed support?

Zed supports standard LSP snippet syntax including numbered tabstops ($1), placeholders with defaults (${1:value}), choice lists (${1|a,b|}), and the final tabstop ($0). The parser in crates/snippet/src/snippet.rs handles escaping for special characters like $ and } according to LSP specifications.

How do I create a snippet with multiple choice options?

Use the choice list syntax ${1|option1,option2,option3|} in the snippet body. When expanded, Zed renders a dropdown at that tabstop position allowing selection between alternatives. This is parsed by the parse_choices function (lines 45-78) in the snippet crate.

How do snippet tabstops work during editing?

When you insert a snippet, Zed places the cursor at the first tabstop ($1). Pressing Tab advances to $2, and so on, until reaching $0 (the final position). Linked placeholders with the same index update simultaneously, and choice lists display a dropdown for selection. This behavior is driven by the TabStop ranges vector generated during Snippet::parse in crates/snippet/src/snippet.rs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →