mach-zed
Zed extension for the Mach programming language.
Features
- Syntax highlighting via mach-tree-sitter
- Auto-indentation for blocks, records, unions, parameter lists, and initializer lists
- Bracket matching and auto-closing for
{},[],(),"",'' - Comment toggling with
# - LSP integration with mach-lsp (when available on
$PATH)
Installation
From the Zed Extension Registry
Search for Mach in Zed's extension panel (zed: extensions) and click Install.
Development
Clone this repository:
git clone https://github.com/briar-systems/mach-zed.gitIn Zed, run zed: install dev extension from the command palette and select the mach-zed directory. Zed builds the grammar and the WASM component and loads the extension. A dev extension replaces the registry copy while it is installed. Building it needs Rust installed through rustup.
Language Server
The extension starts mls, the mach-lsp server. When you open a .mach file, Zed resolves it in this order:
- The binary path in your Zed settings (see Configuration below).
mlson your$PATH.- A prebuilt
mlsdownloaded from the latest mach-lsp release.
The download needs no setup. The extension picks the newest mach-lsp release whose compiler your project accepts (see Compiler Compatibility). It fetches that release's archive for your platform and keeps it in the extension's work directory as mls-<version>/. A newer mach-lsp release is picked up the next time Zed loads the extension. A copy no project has used for 30 days is removed. When GitHub cannot be reached, the extension chooses among the copies already downloaded. Prebuilt binaries exist for x86_64 and aarch64 Linux, x86_64 and aarch64 macOS, and x86_64 Windows. On any other platform, put mls on your $PATH or set its path in settings.
Building mach-lsp
To use your own build instead, build mach-lsp from source:
git clone https://github.com/briar-systems/mach-lsp.git
cd mach-lsp
mach dep pull
mach build .The binary will be at out/{target}/{profile}/bin/mls (e.g. out/linux-x86_64/debug/bin/mls for a default debug build on Linux). Put it on your $PATH or point the settings at it:
install -Dm755 out/linux-x86_64/debug/bin/mls ~/.local/bin/mlsBuilding the WASM Extension
If you're developing the extension locally and need to rebuild the WASM component:
cd mach-zed
cargo build --release --target wasm32-wasip2Note: You need the
wasm32-wasip2target installed:rustup target add wasm32-wasip2
The compiled .wasm file will be at target/wasm32-wasip2/release/mach_zed.wasm. Zed handles building and loading the WASM automatically when installing from the extension registry or through zed: install dev extension.
Configuration
Editor Settings
You can customize Mach-specific editor settings in your Zed settings.json:
{
"languages": {
"Mach": {
"tab_size": 4,
"hard_tabs": false,
"format_on_save": "off"
}
}
}Language Server Binary
To use a specific build of mls, set its path in your Zed settings.json:
{
"lsp": {
"mls": {
"binary": {
"path": "/absolute/path/to/mls"
}
}
}
}mls takes no arguments, so leave binary.arguments unset.
Language Server Options
mls reads its options once, when it starts. Set them under lsp.mls.initialization_options. Zed passes them to the server unchanged and restarts it when they change:
{
"lsp": {
"mls": {
"initialization_options": {
"trace": "messages",
"traceFile": "/home/me/mls.log"
}
}
}
}| key | value | environment fallback |
|---|---|---|
trace |
"off", "messages" (what each message is, no contents) or "bodies" (also message contents, which include your source code) |
MLS_TRACE |
traceFile |
an absolute path the trace is appended to. Without it or MLS_TRACE_FILE, the trace goes to /tmp/mach-lsp.log |
MLS_TRACE_FILE |
requestDeadlineMs |
how long a request may wait on analysis before the server gives up on it, at least 1000 |
MLS_REQUEST_DEADLINE_MS |
An option takes precedence over its environment variable, which takes precedence over the server's default. Nothing is traced unless trace or MLS_TRACE turns it on. An unknown key or an unusable value is ignored, noted in the trace, and never stops the server from starting.
Leave requestDeadlineMs unset unless you have a reason to change it. In mach-lsp 0.20.0, a deadline shorter than the time your project takes to load stops the language server altogether.
The server does not read lsp.mls.settings, so options placed there have no effect.
Compiler Compatibility
mls contains the Mach compiler, linked from one mach release. mls --version names it, for example mls 0.20.0 (mach 5.4.0).
A project states the compilers it builds with as [project].mach in its mach.toml:
[project]
mach = "^5.4"When the linked compiler is outside that range, or outside a range one of the project's dependencies states, mls does not load the project. It reports why as an error on mach.toml, naming the dependency chain, and shows it as a notification. A mach.toml without the key loads, with a warning that gives the line to add. Both are reported on mach.toml itself, so they appear in the project diagnostics panel and when you open that file, and they clear once you save the fix.
The extension reads these ranges before it installs mls. It collects [project].mach from the mach.toml at the root of your Zed project and from every dependency it can reach (dep/<id>/mach.toml for a git dependency, the declared directory for a path dependency). Then it installs the newest mach-lsp release whose compiler satisfies all of them. Each release publishes RELEASES.json, which maps every mach-lsp version to the compiler it links, so no other release needs downloading to decide. When no release satisfies the ranges, or a range does not parse, the extension installs the newest release, and mls reports the problem on mach.toml. The choice is made once per project when its language server starts, so restart the server (editor: restart language server) after changing a range. An mls from your settings or $PATH is used as it is, whatever it links.
Project Structure
mach-zed/
├── .github/workflows/ci.yml # CI (fmt, clippy, wasm build, query checks)
├── extension.toml # Extension manifest (grammars, LSP, metadata)
├── Cargo.toml # Rust WASM extension build configuration
├── src/
│ ├── lib.rs # WASM extension entry point (language_server_command)
│ ├── compat.rs # dependency closure ranges and mls release selection
│ ├── install.rs # mls release asset naming and install layout
│ └── semver.rs # mach version and range grammar
├── languages/
│ └── mach/
│ ├── config.toml # Language configuration (brackets, comments, etc.)
│ ├── brackets.scm # Bracket matching queries
│ ├── highlights.scm # Syntax highlighting queries
│ ├── indents.scm # Auto-indentation queries
│ └── outline.scm # Document outline queries
├── CHANGELOG.md
└── README.mdHow It Works
Zed extensions with language server support require a Rust WASM component that implements the Extension trait from zed_extension_api. The key method is language_server_command, which returns the command Zed should execute to start the LSP.
The extension resolves the mls binary in this order:
- User settings:
lsp.mls.binary.pathin Zed'ssettings.json - System PATH:
worktree.which("mls")searches$PATH - Release download: the newest mach-lsp release whose compiler satisfies the project's dependency closure, found through the latest release's
RELEASES.json(src/compat.rs, with ranges parsed bysrc/semver.rsto the grammar in mach'sdoc/language/manifest.md). Its asset for the current platform, named by mach-lsp's release asset contract, is extracted by Zed and installed atomically intomls-<version>/(src/install.rs). A latest release without a validRELEASES.jsonthat lists itself is reported as an error, not worked around
If none of these succeed, Zed shows the reason in the language server status.
Contributing
- Clone this repo alongside mach-tree-sitter.
- Edit queries in
languages/mach/and reload the Zed extension to test. - For grammar changes, update
mach-tree-sitterand bump therevin[grammars.mach]ofextension.toml. - For LSP integration changes, edit
src/lib.rsand rebuild the WASM component.
Highlight queries are coupled to the grammar. A query may only reference node types and tokens that the pinned
mach-tree-sitterrevision actually produces — referencing a node or token the grammar does not emit makes Zed drop the entire query file. So query updates for new syntax must land together with a grammar bump, never ahead of one. Validate every query file against the pinned grammar before committing (tree-sitter query <file>.scm sample.machmust exit cleanly for each).The grammar now matches the authoritative
doc/language/grammar.md, and the queries cover the full surface, including:
:~bit-reinterpret and::value casts —(cast_expression operator: _ @operator).fwdre-export declarations —"fwd" @keyword, plus aforward_declarationoutline item and alias highlight.$-prefixed comptime value parameters / fields (fun f($x: T),$tag: T;) —(parameter comptime: "$" ...)and(field_declaration comptime: "$" ...).- The
asm <isa> { ... }ISA-tag form —(asm_statement isa: (identifier))and(asm_statement body: (asm_body)); the raw body is oneasm_bodynode.#[attr]decorators (#[symbol("...")],#[inline],#[align(expr)],#[section(...)],#[library(...)]) —(decorator "#[" name: (identifier) @attribute); argument expressions keep their own highlights.- Comptime variadic packs —
(pack_parameter name: (identifier))forva: ..., the$each a in va { ... }unroll (comptime_each_statement,$/each/inkeywords), and theva...pack_spread_expressionellipsis.- Secret values:
^Tis asecret_typewith its marker highlighted as a type qualifier, whilevalue:^Tis asecret_strip_expressionwith its operator highlighted as an operator.#[attr]annotations: both#[and]arepunctuation.special. The decorator query follows the broad bracket captures so they do not overwrite the closing delimiter's annotation highlight.Note
pub/extare repeatablemodifierschildren of each declaration (there is nopublic_declarationorextern_declarationnode), and there is no method-receiver syntax.&is the bitwise-AND operator, so it highlights as an operator rather than a pointer sigil.