docs/development/adding-languages.md
Fresh supports a language through two independent systems — syntax
highlighting and auto-indentation. You can add either on its own. Both are
connected through the grammar catalog (GrammarRegistry), which maps a file to a
language; detection (extension, filename, glob, shebang, configured default)
lives in crates/fresh-editor/src/primitives/detected_language.rs.
| System | What powers it |
|---|---|
| Highlighting | a syntect (TextMate/Sublime) grammar, or a language pack |
| Auto-indent | the regex indent-rules tier (language families) |
Pick only the pieces you need.
.sublime-syntax file under
crates/fresh-editor/src/grammars/ and register it with the catalog
(primitives/grammar/loader.rs).indent_rules.rs.indent_rules.rs.[languages.<id>] config block.[lsp.<id>] (or a
pack's fresh.lsp).When you press Enter or type a closing bracket, Fresh chooses an indent through a tiered fallback:
primitives/indent_rules.rs).primitives/indent.rs).primitives/indent_pattern.rs).The rules tier applies scope masking: before matching, it blanks out comment and string spans (reusing the highlighter's existing output), so a bracket or keyword inside a string or comment doesn't trigger an indent.
A family is a shared set of indent rules describing one class of block
syntax. Most languages can be pointed at an existing family rather than needing
bespoke logic. The families (defined in indent_rules.rs) are:
{ } [ ] ( ) block structure (C, Rust, JS/TS, Go, JSON, CSS…).: opens a block, indentation is the structure.def…end, do…end, with midblock keywords.function…end, if…then…end, for…do…end.if…then…fi, for…do…done, case…esac.begin…end, case…of…end.A family captures the usual signals: what opens a deeper level, what closes one,
one-shot indent/dedent for things like a braceless if or a Python return, and
a "self-close" rule so one-liners (def f; end) don't over-indent. The exact
patterns are data in indent_rules.rs; the user-facing equivalents are
documented in Configuration → Customize Auto-Indentation.
Fresh leans toward the syntect and indent-rules paths above rather than tree-sitter. Each tree-sitter grammar adds a sizable parse table to the binary (around 18 MB was reclaimed by dropping the ones that weren't essential), so it's reserved for languages syntect can't render, and a new language usually doesn't need a tree-sitter indent query — the rules tier covers it.
| Concern | Location |
|---|---|
| Indent families & rules | crates/fresh-editor/src/primitives/indent_rules.rs |
| Generic bracket fallback | crates/fresh-editor/src/primitives/indent_pattern.rs |
| Tree-sitter indent | crates/fresh-editor/src/primitives/indent.rs |
| Syntect grammars | crates/fresh-editor/src/grammars/ + primitives/grammar/loader.rs |
| Language detection / catalog | crates/fresh-editor/src/primitives/detected_language.rs, primitives/grammar/ |
| User-facing indent config | Configuration guide |
| Language packs (no recompile) | Language Packs |
The design rationale and the per-language support matrix live in the repo's
internal docs:
indentation-rules-design.md
and
language-support-review.md.