Languages
What stet gives every language, and which get diagnostics and code intelligence.
Language support is layered. Named file associations select syntax, icons, and LSP profiles independently. Every recognized file gets syntax highlighting, a file-type icon, and structural folding; language servers add diagnostics and code intelligence for the profiles the table marks. You can change any layer through your config.
| Language | Highlighting | Folding | Diagnostics | Code intelligence |
|---|---|---|---|---|
| TypeScript & JavaScript | yes | indent | types + lint | yes |
| Rust | yes | indent | types + cargo | yes |
| Python | yes | indent | types + lint | yes |
| Go | yes | indent | types + vet | yes |
| JSON & JSONC | yes | indent | schema + lint | hover, outline |
| YAML | yes | indent | schema | hover, outline, definition |
| CSS | yes | indent | lint | |
| GraphQL | yes | indent | lint | |
| Markdown | yes | by heading | ||
| 200+ others | yes | indent | via config |
Syntax highlighting
Shiki bundles grammars for more than 200 languages. stet
picks the grammar from the file's name, including exact names such as
Dockerfile, and loads it on demand. The diff, full-file view, search results,
hover cards, and reference previews share the same highlighter and theme. An
unrecognized file type falls back to plain text.
Config dotfiles carry no extension to pick a grammar from, so they resolve by
name: .npmrc highlights as INI and .prettierrc as JSONC, so a comment in it
reads as a comment. Their extension variants, such as .prettierrc.json and
.prettierrc.yaml, already resolve on the extension.
Folding
Press z to fold the block at the caret: code folds by indentation in every
language, markdown folds by heading section. See
reading files and diffs.
File icons
Recognized file types use restrained colored Nerd Font icons; structural and
generic icons remain muted. Recognizable brands keep their identity, while
unbranded formats reuse stet's palette. The same named icon and theme color
appears in every file-list surface. Matching follows exact filename, glob,
extension, and dotfile specificity; --no-icons keeps every layout stable while
hiding the glyphs if your terminal font lacks them.
Diagnostics
Servers overlap deliberately: every server that claims a file runs, and their
findings merge per file, each tagged with its own source label. A linter that
competes with an always-on default activates only where the repo opted into
it, the way an editor extension would. Where a language has competing type
checkers, stet runs the one the repo opted into and only that one, so the same
code is never type-checked twice. Both rules are written as data in the same
grammar your config uses: a
server's when says which repos it runs in, and a firstOf group picks exactly one of several
candidates. A language profile can reuse those defaults or override a candidate with an
unconditional { "server": "name" } or its own when.
TypeScript & JavaScript
.ts, .tsx, .js, .jsx, and the .mts/.cts/.mjs/.cjs variants run
through three servers: typescript-language-server reports type errors (label
tsc), oxlint reports lint findings (label oxc), and Biome joins them (label
biome) when the repo has a biome.json or biome.jsonc.
Rust
.rs files get rust-analyzer's own analysis (label rust-analyzer) plus the
findings cargo check produces through it (label rustc). stet downloads the
server automatically the first time a Rust file needs it.
Python
.py and .pyi files get a type checker plus ruff's lint findings (label
Ruff). Python has more than one type checker, so stet runs the one your
project runs: ty (label ty) in a repo that
opted into it, basedpyright (label basedpyright) everywhere else, never both.
So the warnings you see are the ones your CI gates on.
A repo counts as a ty repo when it has a ty.toml or .ty.toml, a [tool.ty]
table in pyproject.toml, or ty among its declared dependencies ([project]
dependencies, a dependency group, an optional-dependencies extra, [tool.uv] dev-dependencies, or a Poetry dependency table). That
rule is not private machinery: it is ty's when gate, and the one-checker rule
is a firstOf group, both written in the exact grammar
your config uses, so you can
reproduce, re-gate, or replace the whole arrangement. Bare names retain their server's default
gate; use the object form when you want to force a checker regardless of repository signals:
{
"languages": {
"python": { "servers": [{ "server": "basedpyright" }, "ruff"] },
},
}ty answers hover, go to definition, references, call hierarchy, and the symbol
outline. It has no go-to-implementation yet, so in a ty repo Shift+I says
implementations are unsupported for the file type rather than claiming there
are none.
Servers download automatically the first time a Python file needs them, basedpyright from npm, ty and ruff as sha256-verified GitHub release binaries.
Go
.go files get gopls, Go's
official language server: type and build errors plus the go vet analyzers it
runs by default (label compiler, and the analyzer's name for a vet finding).
go.mod and go.work get their module diagnostics through the same server.
gopls ships no prebuilt binaries and stet never builds it, so unlike the other
built-in servers stet never downloads it. Install it yourself with
go install golang.org/x/tools/gopls@latest; stet finds it on your PATH.
Without it, Go files show as unavailable rather than falsely clean.
JSON & JSONC
vscode-json-language-server validates well-known files against their schemas:
package.json, tsconfig.json, tsconfig.*.json, and jsconfig.json out of
the box, plus any file you associate through the
schemas config. Biome adds its lint
where configured, and the same server answers hover and the symbol outline. The
built-in associations point at SchemaStore URLs
fetched over the network, so an offline repo falls back to syntax checking.
YAML
yaml-language-server validates .yaml and .yml against schemas it
discovers through SchemaStore (GitHub
workflows, and so on), and answers go to definition, hover, and the symbol
outline. It fetches those schemas over the network, so an offline repo falls
back to syntax checking.
CSS & GraphQL
Biome covers both, only in repos with a biome config. Without one these files show as unavailable rather than falsely clean.
Code intelligence
Go to definition, find references, implementations, call hierarchy, hover, and the symbol outline work for the TypeScript & JavaScript family, for Rust, for Python, and for Go, through the same servers that produce their diagnostics (a ty repo has everything but go to implementation, which ty does not yet answer). JSON and YAML get hover and the symbol outline (YAML also go to definition); ruff adds hover on Python. See code intelligence for the keys and flows.
How servers are found
The default discovery strategy checks the repo's own node_modules/.bin, then
your PATH. For provisionable built-in servers only, stet then checks for an
existing pinned copy in its LSP cache. Python discovery first checks the active
$VIRTUAL_ENV/bin, then the repo's .venv/bin, before using the same
node_modules/.bin and PATH lookup. Opting a user-defined server into Python
discovery changes only those local lookup locations; it does not enable cache
use or provisioning. User-defined servers are never provisioned. The cache is
$XDG_CACHE_HOME/stet/lsp when XDG_CACHE_HOME is set, and
~/.cache/stet/lsp otherwise. If a provisionable built-in remains missing,
stet downloads it into that cache: packages distributed through npm (this
path needs npm on your PATH), or a SHA-256-verified GitHub release binary
for the servers it doesn't (rust-analyzer, ruff, ty). Set
"diagnostics": { "download": false } in your config to turn downloads off, or
pass --no-lsp-download (or set STET_NO_LSP_DOWNLOAD) for one run.
gopls is the exception among built-ins: it ships no prebuilt binaries and stet
never builds from source, so stet never downloads it. Install it yourself
(go install golang.org/x/tools/gopls@latest) and stet finds it on your PATH;
without it, Go files show as unavailable rather than falsely clean.
A download that cannot complete (offline, air-gapped, locked-down CI) times out, and its files show as unavailable instead of waiting forever.
Installing dependencies while stet is open
Agents install dependencies constantly, and a language server that never hears
about it keeps reporting imports it can no longer resolve. stet tells each
server about the on-disk changes it asked to hear about, as they happen,
including inside gitignored directories like .venv/.
In Python, that means running uv add fastapi while stet is open clears the
Import "fastapi" could not be resolved errors on its own: basedpyright has no
file watcher of its own, so this is the only way it learns a package arrived.
You do not need to restart, and you do not need to press r.
Servers that watch the filesystem themselves, like rust-analyzer and tsserver, keep doing so and
pick up cargo add or npm install through their own watchers.
If a server does get stuck, R restarts the language servers for the repo. r only re-reads what
they already believe, which is why it cannot clear a stale cache on its own. R is also the
fallback where filesystem watching is unavailable (some sandboxes and containers): installs go
unnoticed there, so press R after installing.
Add your own languages
Custom support connects a named server, language profile, and file
association. The registries stay separate so several associations can reuse one
LSP profile and several profiles can reuse one server:
{
"diagnostics": {
"servers": {
"lua": { "command": ["lua-language-server"] },
},
},
"languages": {
"lua": { "languageId": "lua", "servers": ["lua"] },
},
"files": {
"lua": { "extensions": ["lua"], "syntax": "lua", "language": "lua" },
},
}Server conditions and firstOf groups let the same profile select a different
server by repository. stet resolves user-configured commands locally and never
runs a command declared by the inspected repository. See
configuration for matching,
icons, gates, server options, and overriding or disabling built-ins.