Patterns / Media
Syntax
Markdown apps/gallery/src/patterns/syntax.rs
Nothing on this page is library code. It wraps a sample in a fence and hands the result to markdown::render. The call underneath is the crate's whole surface:
syntax::highlight(code, "rs") // -> Option<Vec<(Range<usize>, HighlightKind)>>Spans in document order, in bytes, and everything outside them is plain text. No colour and no rendering here — kinds become colours through SyntaxPalette, and a capture name with no slot in the bezel vocabulary degrades to Variable, which paints as body text. A tag naming no grammar returns None and the block renders plain. There is no injection machinery either: the fence already names the grammar, so a block is one parse with one query.
Eight languages ship, each answering to its fence aliases — rust/rs, python/py, typescript/ts, tsx/jsx/javascript/js, json/jsonc, go/golang, bash/sh/shell/zsh/console, toml. JavaScript rides the TSX grammar, because TSX parses JS and a second grammar would buy only the <-ambiguity cases a highlighted sample does not hinge on.
One feature per language, all on by default. A grammar is a C compile, so an app that only ever shows Rust should only ever build one:
syntax = { version = "0.0.2", default-features = false, features = ["rust"] }That is seven grammars down to one, and a measured 12.4s of clean build down to 3.5s. The typescript feature carries both the TypeScript and TSX rows, since they are one grammar crate. The language table is a slice and not a fixed-size array precisely so its length can follow the features; with none of them on it is empty, every tag resolves to None, and every block paints plain.
markdown names no highlighter. It does not depend on syntax at all — the two meet at one function pointer, installed at boot like the theme palette:
fn spans(language: &str, code: &str) -> Option<Vec<(Range<usize>, HighlightKind)>> {
syntax::highlight(code, language)
}
markdown::set_highlighter(cx, spans);Note the argument order flips: Highlighter takes the language first, syntax::highlight takes the source first. Both are &str, so a swap compiles and silently colours nothing.
syntax is a peer crate, not part of the bezel facade. You name it yourself, and an app that highlights nothing never compiles a grammar — the facade carried it once, which cost every consumer seven C grammar builds and made bezel unbuildable for wasm32-unknown-unknown outright.
A language the table does not carry is a static of your own. Lang::new is const, so it sits beside the built-in rows and reaches the same highlight method — the query cache, the capture-name filter and the HighlightKind vocabulary all come with it, and none of it has to be rebuilt:
use syntax::lang::Lang;
static ZIG: Lang = Lang::new(
"zig",
&["zig"],
tree_sitter_zig::LANGUAGE,
include_str!("../queries/zig.scm"),
);
ZIG.highlight(code)Take the grammar's LanguageFn through syntax::tree_sitter_language rather than declaring your own tree-sitter. Two versions in one graph are two unrelated types with the same name, and Lang::new will reject the stranger — the same hazard bezel::gpui exists to prevent, one layer down.
That extension point and the seam above it are different tools. Lang is for another tree-sitter grammar; the function pointer is for another engine, and nothing about it is tree-sitter at all — Range<usize> and HighlightKind are the entire vocabulary:
fn spans(language: &str, code: &str) -> Option<Vec<(Range<usize>, HighlightKind)>> {
match language {
"zig" => ZIG.highlight(code),
_ => syntax::highlight(code, language),
}
}Swapping the engine wholesale — syntect, a regex pass, nothing at all — is the same function with a different body. This is why the grammar table stays private to syntax: opening it would put tree-sitter's own types in the public API, and an app carrying its own tree-sitter would then have two.
A browser cannot run any of it. tree-sitter is C, and wasm32-unknown-unknown has no libc to compile it against, so a web build would carry a dependency it can never link — which is the reason markdown names no highlighter in the first place. A build script runs on the host whatever the target is, so the gallery highlights its samples ahead of time and the wasm build looks the answer up by (tag, source); a block the build script never saw paints plain, which is what an unknown language does anyway.
Highlighting recolours runs and never moves layout: the block is laid out line by line, so a build with no highlighter installed paints the same shape in one plain run.
The source is at apps/gallery/src/patterns/syntax.rs, and the highlighter it installs at apps/gallery/src/highlight.rs. Copy the file.