@codincod/codemirror-lang-odin
v0.1.1
Published
Odin language support for the CodeMirror code editor
Downloads
263
Maintainers
Readme
@codincod/codemirror-lang-odin 
[ CHANGELOG ]
This package implements Odin language support for the CodeMirror code editor, using a Lezer grammar written for this package.
CodeMirror has never shipped Odin, and there is no stream mode from CodeMirror 5 to fall back on either, so an Odin file in a CodeMirror 6 editor has until now been plain text. No Lezer grammar for Odin existed anywhere before this one.
Written in part for CodinCod, a competitive coding platform, where it colours the editor people solve puzzles in.
This code is released under an MIT license.
Usage
import {EditorView, basicSetup} from "codemirror"
import {odin} from "@codincod/codemirror-lang-odin"
const view = new EditorView({
parent: document.body,
doc: `package main
import "core:fmt"
Point :: struct {
x, y: f32,
}
main :: proc() {
p := Point{x = 1, y = 2}
fmt.println(p)
}
`,
extensions: [basicSetup, odin()]
})Coverage
Odin dev-2025-04. Declarations of every kind (:: for procedures, types and
constants, := for variables), structs with tags and parapoly parameters,
unions, enums, bit sets, bit fields, matrices, #soa and #simd types,
procedure groups, polymorphic types ($T, $T/Spec) with where clauses,
foreign blocks and foreign imports, attributes, compile-time when, the whole
directive vocabulary, or_return and its family, both ternaries, type
assertions, ranges, labels, and the automatic semicolons that make almost all of
this readable without one.
Measured against the 1090 files of the standard library that ships with the
compiler, 99.91% parse with no error node. The one file left is base:builtin,
which describes the compiler's built-in procedures in a notation Odin's own
parser rejects as well (-> (A, B, C, ...)), and which the compiler never reads
as ordinary Odin.
The corpus does not ship with this package. It is BSD-3 and belongs to the Odin
project, so test/corpus.ts takes the path to your own copy of it.
What a tokenizer has to settle
Almost all of Odin can be written down as a grammar. Seven things cannot, and
they are the whole of tokens.ts.
A newline can end a statement. Odin inserts a semicolon at a line break when
the token in front of it could end one, which is why almost no Odin source
writes a semicolon. Whether the break ends the statement depends on the token
behind it and on what stands after it: a brace where a body could stand, an
else, or a where all take the semicolon back.
A brace is two different things. if x == Foo { opens a body and
x := Foo{} opens a compound literal, and the same character spells either.
Odin settles it by where the brace stands, so the tokenizer asks the parser
whether a body could stand here and lets the answer decide. That is Odin's own
rule: its parser refuses a compound literal in the head of an if, a for or a
switch unless it is parenthesised, and those heads are exactly the places
where a body could stand instead.
A name is not an expression. x, y := f() and x, y = f() are the same two
names and the same comma, and only the operator behind them differs. Odin's
parser reads the list and then looks for the colon; an LR parser cannot hold
both readings open that long, so the tokenizer does the same lookahead one word
earlier and hands the parser a different token for a name being declared. The
same lookahead separates loop: for {} from x: int.
:: says what is being declared. main :: proc() {} declares a procedure,
Point :: struct {} a type and MAX :: 100 a constant. The three read
identically until the value is over, which is far too late to name the node a
highlighter wants, so the tokenizer looks one word past the :: and hands the
parser three different tokens. That is what makes a procedure name look like a
procedure name before its body has been read.
in walks or asks. for x in xs walks a map or an array, and x in xs
asks whether a set holds something. Odin turns the operator off while it reads
the head of a for, and the tokenizer does the same by handing a range clause
an in of its own, where only a range clause could stand.
A number may end in its point. 1. is a float and 1..<n is a range from
- A token rule reads as far as it can and would take the range's first dot for
the point of the number, so numbers are scanned by hand, where the character
behind the dot can be looked at first. Odin's own scanner does the same, and the
one here is a reading of it, down to the letters that count as digits in
0zand0h.
Block comments nest. /* /* */ */ is one comment in Odin rather than two,
which no regular token can count.
What the tree does not say
Types are expressions. That is not a simplification: Odin's own AST has no
separate type node, because Point :: struct {} really is a value bound to a
name and [4]int really is an expression. So struct, []T, map[K]V and
proc() are alternatives of expression in the grammar, exactly as they are in
the compiler.
One field rule does three jobs. Struct fields, procedure parameters and
results are written the same way, down to the using in front and the string
tag behind, so one rule shape serves all three. It accepts a tag where only a
struct may carry one, which is the usual trade: a grammar that is wrong about
what the compiler will reject and right about what it will accept.
A do body on a procedure literal is left out. f :: proc() do g() is
legal and appears nowhere in the standard library. Letting an unbraced statement
end where a procedure literal may stand is what the parser generator runs out of
memory over, so the one form the grammar refuses is the one nobody writes.
