>_EXECUTIVE SUMMARY
vscode-zsh is the VS Code / VSCodium extension for zshrs — the first-ever Rust rewrite of zsh, a compiled, JIT'd, massively parallel shell. It ships a declarative language contribution (filetype, grammar, editor config), an LSP client that launches zshrs --lsp, a TextMate grammar that is generated from the zshrs binary's own reflection tables, and — since 0.2.0 — execution and full debugging driven by zshrs's own debug adapter, zshrs --dap.
Design principle: the grammar owns its own zshrs language id and source.zshrs scope — not a shellscript reskin, not a hand-picked subset. scripts/gen_grammar.sh dumps zshrs --dump-reflection (.builtins, .extensions, .special_vars, .keywords) from the live binary and emits a single source.zshrs grammar carrying the real shell surface. The zshrs world-first extensions get their own scope (support.function.extension.zshrs). Regenerate after a zshrs upgrade and it stays in sync.
~COMPONENTS
| File | Responsibility |
|---|---|
package.json | Extension manifest — contributes.languages (id zshrs, zsh extensions + dotfile filenames, shebang firstLine), contributes.grammars (source.zshrs), contributes.configuration (zshrs.path, zshrs.lsp.enabled, zshrs.lsp.args), contributes.commands (zshrs.run, zshrs.debug) with their keybinding and editor-title / palette menus, contributes.breakpoints, contributes.debuggers (type zshrs, its launch attributes, initial configuration and snippet), and the vscode-languageclient dependency. |
language-configuration.json | Line comment #, brackets, auto-closing / surrounding pairs, word pattern, and brace-based indentationRules. |
extension.js | Activates on onLanguage:zshrs (and on debug resolve) and starts a LanguageClient running zshrs --lsp over stdio. Missing binary → one non-fatal warning; highlighting still works. Also registers the zshrs.run and zshrs.debug commands, the debug configuration provider that fills in the active file when there is no launch.json, and the debug adapter descriptor factory. |
lib/resolveBinary.js | Resolves zshrs to an absolute path from $PATH plus ~/.cargo/bin, /opt/homebrew/bin, /usr/local/bin and ~/.local/bin. A GUI-launched editor inherits no shell $PATH, so a bare zshrs fails to spawn and takes the language server and the debugger with it. vscode-free, so it is unit-tested headlessly. |
lib/dapBridge.js | The connect-back bridge. zshrs --dap HOST:PORT is a TCP client — it dials into a listener the IDE provides, as it already does in JetBrains — while VS Code wants to be the client connecting to a DebugAdapterServer. This runs a rendezvous server both ends connect to and pipes the two sockets together. Also vscode-free. |
syntaxes/zshrs.tmLanguage.json | Generated TextMate grammar — comments, shebang, strings / here-docs, numbers, sigil variables, command substitution, control + declaration keywords, 138 builtins, 112 extensions, 245 special vars, and operators / pipes / redirects. |
scripts/gen_grammar.sh | Regenerates the grammar from zshrs --dump-reflection. Builtins exclude keyword and extension names; names are emitted longest-first so the alternation never matches a prefix of a longer name. |
scripts/tokenize_test.js | Loads the grammar under vscode-textmate + vscode-oniguruma (the engine VS Code itself uses) and asserts the scope of 7 sample tokens. |
scripts/resolver_test.js | Binary resolution against a temporary PATH: an executable is found, a non-executable file is not, and the fallback directories are searched. |
scripts/activate_test.js | Drives extension.js against a stub vscode module and asserts the runtime wiring: the LSP is spawned as bare zshrs --lsp with no --stdio, a missing binary never constructs the LanguageClient, run and debug are both registered under the zshrs debug type, and the configuration provider fills in the active file. |
scripts/dapbridge_test.js | Pipes a DAP initialize through the bridge to a real zshrs --dap and asserts the response comes back. Skips when the binary is unavailable, so the CI Linux runner still passes. |
$SCOPE MAP
| Token group | TextMate scope | Sample |
|---|---|---|
| Control flow | keyword.control.zshrs | if then fi for while case esac function return |
| Declarations | storage.modifier.zshrs | typeset local export declare readonly integer float |
| Builtins (138) | support.function.builtin.zshrs | bindkey autoload zstyle compadd setopt zle alias |
| Extensions (112) | support.function.extension.zshrs | base64 async await barrier clone arch |
| Special variables (245) | variable.language.zshrs | PATH HOME PWD RANDOM |
| Sigil variables | variable.other.zshrs | $foo ${bar} $1 $? $@ $# $$ |
| Operators / pipes / redirects | keyword.operator.zshrs | | || && ;; > >> << >& =~ |
| Literals & vars | string.* / constant.numeric / variable.* | strings, here-docs, command substitution, sigils, numbers |
&RUN & DEBUG
Run is the simple half: zshrs: Run File saves the active script and executes zshrs <file> in an integrated terminal, reachable from Ctrl+F5, the editor-title ▶, or the command palette. Debug is the interesting half, because zshrs's debug adapter does not speak stdio: zshrs --dap HOST:PORT is a TCP client that dials into a listener the IDE provides — the model it already uses under JetBrains. VS Code expects the opposite, connecting outward to a DebugAdapterServer. lib/dapBridge.js reconciles the two with a rendezvous server: both VS Code and zshrs connect to it and their sockets are piped together, so DAP traffic flows transparently and the session behaves like any other VS Code debug session.
| Surface | Contribution | Detail |
|---|---|---|
| Run | zshrs.run | Ctrl+F5 (editorLangId == zshrs), editor-title navigation@1, command palette |
| Debug | zshrs.debug | F5, editor-title navigation@2, command palette |
| Breakpoints | contributes.breakpoints | Gutter breakpoints enabled for the zshrs language |
| Debug type | zshrs | Launch attributes program, args, cwd, stopOnEntry, zshrsPath |
No launch.json | DebugConfigurationProvider | An empty configuration is resolved to the active file, so F5 works unconfigured |
| Binary | lib/resolveBinary.js | Absolute-path resolution from $PATH + the common install dirs, for GUI-launched editors |
#VERIFICATION
The generated grammar is loaded under the exact engine VS Code uses at runtime — vscode-textmate driving vscode-oniguruma — and a sample is tokenized with the resulting scopes asserted. This proves the builtin / extension / special-var alternations compile under oniguruma and classify tokens correctly, with extensions matched ahead of plain builtins so a name such as base64 resolves to support.function.extension.zshrs.
| Sample token | Asserted scope |
|---|---|
typeset | storage.modifier.zshrs |
for | keyword.control.zshrs |
bindkey | support.function.builtin.zshrs |
base64 (extension) | support.function.extension.zshrs |
"^R" (double string) | string.quoted.double.zshrs |
$out (interpolated var) | variable.other.interpolated.zshrs |
Three further suites run headless alongside it, all under node --test: resolver_test.js resolves the binary against a temporary PATH, activate_test.js drives extension.js against a stub vscode module to pin the runtime contract (bare zshrs --lsp, no client on a missing binary, run + debug registered under the zshrs type, active file filled in for an empty configuration), and dapbridge_test.js pipes a DAP initialize through the bridge to a real zshrs --dap, skipping when the binary is unavailable. CI additionally validates the JSON manifests, shellchecks the scripts, and packages the .vsix to confirm the runtime dependency is bundled.