@codincod/codemirror-lang-swift
v0.1.1
Published
Swift language support for the CodeMirror code editor
Maintainers
Readme
@codincod/codemirror-lang-swift 
[ CHANGELOG ]
This package implements Swift language support for the CodeMirror code editor, using a Lezer grammar written for this package.
CodeMirror 6 has never shipped Swift. What it offers is the stream mode ported from CodeMirror 5, which colours a list of keywords and leaves no tree behind, so nothing above it can fold a function, indent a switch or ask what the word under the cursor is. No Lezer grammar for Swift 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 {swift} from "@codincod/codemirror-lang-swift"
const view = new EditorView({
parent: document.body,
doc: `struct Greeter {
let name: String
func greet() {
print("Hello, \\(name)!")
}
}
`,
extensions: [basicSetup, swift()]
})Coverage
Swift 5.9. Async and await, actors, structured concurrency, opaque and
existential types (some and any), property wrappers, result builders,
macros, parameter packs, pattern matching, generics with where clauses, key
paths, multi-line and raw strings, operator and precedence group declarations.
The Swift 1 and 2 spellings a decade of published code still carries are in
too: inout in front of a parameter name, var parameters, curried functions,
and constraints written inside the angle brackets.
Measured against 724 Swift files from Rosetta Code, 94.34% parse with no error
node. Most of the rest is the C-style for loop that Swift 3 removed, and a
handful of files that are C rather than Swift.
What a tokenizer has to settle
Almost all of Swift can be written down as a grammar. Five things cannot, and
they are the whole of tokens.ts.
Whitespace decides what an operator is. Swift's operator set is open, so
a - b, a -b and a-b cannot be told apart by spelling. The language settles
it by spacing: an operator with space on both sides or neither is infix, one
with space only in front is prefix, and one with space only behind is postfix.
The same rule separates x! from !x, and a ?? b from x?.y.
An angle bracket is two different things. Array<Int> opens a generic
argument list and a < b compares. Where a type is expected the parser has
already ruled the comparison out. Where an expression is expected, the tokenizer
reads ahead for the closing bracket and takes the call or member access that
follows it as the proof.
A brace is two different things. if x { } is a condition and a body;
xs.map { } is a call and a closure. Swift tells them apart by where the brace
stands, so the tokenizer asks the parser whether a body could stand here and
lets the answer decide. A closure that opens with a signature ({ x in ... })
is a third case, found by reading ahead for the in over nothing but what a
signature may hold.
Block comments nest. A comment opened inside a comment has to be closed twice, which no regular token can count.
A string holds code. "total: \(a + b)" is a string with an expression
inside it, and that expression may hold another string. The tokenizer cuts the
string into pieces and the grammar parses what lies between them.
What the tree does not say
Two things are deliberately shallower than the language reference.
Operator precedence is flat. Swift lets anybody declare an operator and give
it a precedence group, so no fixed table here could be right about a program it
has not seen. Infix operators are one left-associative level, which is wrong
about the shape of a + b * c and right about every operator this grammar has
never heard of. A highlighter reads tokens rather than shapes.
Patterns are expressions. case .some(let x) holds a binding that only
appears in a pattern. Rather than carry a second grammar for patterns, let x
is accepted as an expression and the tree says BindingPattern where a pattern
was written.
Raw strings (#"..."#) are one token rather than a parsed string, because the
delimiter's pound count would have to be carried from the opening token to the
closing one. Nothing interpolates inside them anyway.
