Skip to content

ADR 0103: The mainstream languages

Status

Accepted

Context

ADR 0086 split the two gates and claimed the result: "LSP — config-driven. Name a command, it works, for any language."

It did not. ase_config_language_for_path knew eight suffixes, all C and C++, and returned NULL for everything else — which startLspClientIfConfigured turned into NotApplicable, a completely blank indicator. A .py file with lang.python.lsp correctly set looked exactly like a .txt file. Nothing said that a second line, filetype.py = python, was also required.

So the promise was true only for languages the editor already knew, of which there were two.

Decision

The two halves cost wildly different amounts

Knowing a language costs a row in a table. Data, no binary. Highlighting one costs a grammar, and ADR 0085 measured that at 3.3 MB for C++ alone. Treating them as one decision is what made this look expensive.

Every mainstream language is now known

The built-in table covers the languages people run servers for, named with the LSP spec's own ids — a server keys off python, never py, and off javascriptreact for .jsx. With that, lang.python.lsp = pylsp is genuinely all a Python file needs.

An unconfigured .py now reads no lsp rather than showing nothing, which is the difference between "you could configure this" and "this is not code". That distinction is the whole point: the blank state was indistinguishable from success.

Five grammars, measured before they were chosen

Predicted from the two existing grammars — parser.c ÷ 5.5 ≈ library size (C: 3.87 MB → 621 KB; C++: 17.3 MB → 3378 KB) — then checked:

language library
python 516 KB
javascript 381 KB .jsx comes with it
css 106 KB
lua 64 KB
html 35 KB
total 1.08 MB predicted 1.26 MB

Binary 5.45 MB → 6.43 MB, +18%. Startup unchanged at 62 ms: the grammars are linked but only one is instantiated per buffer.

Vimscript was measured and dropped. Its parser.c is 4.57 MB — larger than Python's — for an estimated 830 KB, which would have been 40% of the total spend on the least-read file type of the six. It is a row in the filetype table, so lang.vim.lsp works; there are just no colours.

Every node name was checked before a query was written

ADR 0085's rule: ts_query_new rejects the whole query on one unknown name, syntax_create returns NULL, and the editor shows a language with no colours and no error — indistinguishable from "not supported yet".

So each candidate name was checked against that grammar's own node-types.json before being written down, and a test now asserts all seven languages compile their query, capture something, and highlight a known keyword. That test caught its own first version: it expected int to be a C keyword when the query captures it as a type.

Consequences

Five languages have colours; every mainstream language can have a server from one config line.

HTML, CSS and JavaScript do not see into each other. A <script> block in an HTML file stays grey, because that needs Tree-sitter injections — a real feature, not a query line, and deliberately separate from this.

Adding the next language is one FetchContent_Declare, one name in each of two foreach lists, one enum value, one case, one .scm, and one row in the grammar table. No architecture changes; the wiring was already a loop.

The queries stay deliberately thin — keyword, comment, string, number, type — because that is all the theme has (ADR 0048). CSS bends it slightly: property names read as keywords and selectors as types, which is the mapping that makes a stylesheet legible with only those five.