@codincod/codemirror-lang-groovy
v0.1.0
Published
Groovy language support for the CodeMirror code editor
Maintainers
Readme
@codincod/codemirror-lang-groovy 
[ CHANGELOG ]
This package implements Groovy language support for the CodeMirror code editor, using a Lezer grammar written for this package.
CodeMirror 6 has never shipped Groovy, and no Lezer grammar for it existed
before this one. What gets used instead is the stream mode ported from
CodeMirror 5, which colours a list of keywords and builds no tree, so nothing
above it can fold a closure, indent a switch or ask what the word under the
cursor is. It also has nothing to say about the parts of Groovy that are not
Java: a build file's parenthesis-free calls, a slashy regular expression, a
${} inside a string.
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 {groovy} from "@codincod/codemirror-lang-groovy"
const view = new EditorView({
parent: document.body,
doc: `class Greeter {
def name
String greet() {
"Hello, $name!"
}
}
`,
extensions: [basicSetup, groovy()]
})Coverage
Groovy 4, and with it the Java that Groovy is a superset of. Classes,
interfaces, traits, enums, records and annotation types, generics with
wildcards and bounds, closures and lambdas, def in every position it is
allowed, multiple assignment, command expressions with named arguments and
trailing closures, the Elvis, spread, safe navigation, attribute and method
pointer operators, ranges, ==~ and =~, Spock's string method names and
labels, switch as a statement and as an expression, and every way Groovy writes
a string: single and triple quoted, GStrings with $name and ${} holes,
slashy, and dollar slashy.
Measured against 20 662 Groovy and Gradle files from six open source projects (Apache Groovy, Gradle, Spock, Grails, Ratpack and Geb), which is 84 MiB of code, of which 98.48% parse with no error node.
What the grammar cannot say
Five things about Groovy are settled in src/tokens.ts rather than in the
grammar, because no context free rule can decide them.
A newline ends a statement, except when it does not. Groovy's semicolons
are optional, so a\nb is two statements and a +\nb is one. The tokenizer
emits a zero width insertSemi when a line break stands between two things
that could each begin a statement, and holds it back when the next line opens
with something that can only continue the one before: a dot in any of Groovy's
five spellings, a comma, a closing bracket, &&, ||, ?:, an arrow, or one
of extends, implements and throws, which a wrapped signature puts at the
head of the next line.
A brace opens a block or a closure. { is the same character either way,
and only the arrow a closure's parameters end with tells them apart. Where a
statement may begin the grammar settles it with a cut, because a block is the
only thing wanted there. Everywhere else the tokenizer looks ahead for the
arrow, giving up at the first character a parameter list cannot hold, so that
{ foo(a -> b) } is not mistaken for a closure that takes foo.
A parenthesis opens a call or a lambda. a.b() and (a, b) -> a + b are
the same shape until the arrow after the closing bracket, and a call is written
far too often for the parser to be left weighing the two. The tokenizer counts
the depth along the line and emits LambdaParen only when the bracket that
closes the group is followed by an arrow.
A < is a comparison or a type argument list. a < b and Map<K, V> are
the same two characters, and Groovy makes it harder than Java does: a command
expression means the parser has a comparison available everywhere a type could
stand. The tokenizer reads the whole angle bracketed group, then reads the
name after it, and decides on what follows that name. A name being declared
is followed by its value, by another name, by its parameters, by the colon or
in of a for-each, or by the end of the line; a name being compared is
followed by an operator.
A slash opens a string or divides. Groovy's slashy string needs no escaping
and is what regular expressions are written in, and it starts on the same
character as division. The tokenizer looks at what stands in front: after a
name, a number, a string or a closing bracket there is a value to divide, and
everywhere else there is not. Groovy's own lexer reads the character this way,
so len / (w + 1) divides and m ==~ /\d+/ matches, here as in the language.
Development
npm install
npm test # build, then the parse specs and the editor tests
npm run build # regenerate src/parser.js and dist/The corpus scripts want lezer-survey checked out beside this package, and a corpus of real Groovy to point it at.
