Skip to content

Commit 66a5a70

Browse files
committed
feat: gleam language support
1 parent 62615d3 commit 66a5a70

9 files changed

Lines changed: 414 additions & 0 deletions

File tree

plugin.zip

5.97 KB
Binary file not shown.

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Adds CodeMirror language support for languages that are not bundled with Acode.
2323
- Pkl (`.pkl`)
2424
- Svelte (`.svelte`)
2525
- WGSL (`.wgsl`)
26+
- Gleam (`.gleam`)
2627

2728
Community modes are registered only when Acode does not already provide a mode
2829
with the same name. Languages already covered by Acode's

scripts/test-parser.mjs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,3 +402,89 @@ for (const sample of gitattributesSamples) {
402402
}
403403

404404
console.log(`Validated ${gitattributesSamples.length} GitAttributes parser fixtures.`);
405+
406+
// Test Gleam Parser
407+
const { parser: gleamParser } = await bundledRequire(
408+
"src/languages/gleam/parser.js",
409+
"acode-gleam-parser-test.cjs",
410+
);
411+
412+
const gleamSamples = [
413+
{
414+
name: "Gleam comments, imports, types and functions",
415+
source: `//// This is a module comment
416+
/// This is a doc comment
417+
// This is a line comment
418+
419+
@target(javascript)
420+
import wibble/wobble.{type MyType, my_value}
421+
422+
pub type MyResult(a) {
423+
Ok(a)
424+
Error(Nil)
425+
}
426+
427+
pub fn main() -> Nil {
428+
let x = 123
429+
let f = 3.14
430+
let s = "hello"
431+
let b = True
432+
let _discarded = False
433+
io.println(s)
434+
case x {
435+
_ -> Nil
436+
}
437+
let list = [1, 2]
438+
let tuple = #(1, 2)
439+
let bits = <<1, 2>>
440+
}
441+
`,
442+
nodes: [
443+
"ModuleComment",
444+
"DocComment",
445+
"LineComment",
446+
"Attribute",
447+
"TypeName",
448+
"Identifier",
449+
"String",
450+
"Integer",
451+
"Float",
452+
"True",
453+
"False",
454+
"Nil",
455+
"FunctionDefinition",
456+
"FunctionCall",
457+
"Block",
458+
"Parenthesized",
459+
"Bracketed",
460+
"BitArray",
461+
],
462+
},
463+
];
464+
465+
for (const sample of gleamSamples) {
466+
const tree = gleamParser.parse(sample.source);
467+
const names = new Set();
468+
const errors = [];
469+
470+
tree.iterate({
471+
enter(node) {
472+
names.add(node.name);
473+
if (node.type.isError) {
474+
errors.push([node.from, node.to]);
475+
}
476+
},
477+
});
478+
479+
// Print all parsed node names to inspect them
480+
console.log("Parsed Gleam nodes:", [...names]);
481+
482+
assert.equal(errors.length, 0, `${sample.name} produced parse errors`);
483+
assert.equal(tree.length, sample.source.length, `${sample.name} was not fully parsed`);
484+
for (const node of sample.nodes) {
485+
assert(names.has(node), `${sample.name} did not produce ${node}`);
486+
}
487+
}
488+
489+
console.log(`Validated ${gleamSamples.length} Gleam parser fixtures.`);
490+

scripts/test-plugin-runtime.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async function test() {
6969
"wgsl",
7070
"ejs",
7171
"gitattributes",
72+
"gleam",
7273
];
7374

7475
assert.deepEqual([...modes.keys()], expectedNames);
@@ -126,6 +127,7 @@ message:
126127
wgsl: "@vertex fn main() -> @builtin(position) vec4f { return vec4f(); }",
127128
ejs: "<% if (user) { %>\n<h2><%= user.name %></h2>\n<% } %>",
128129
gitattributes: "# comment\n*.txt text eol=lf\n",
130+
gleam: "pub fn main() { Nil }\n",
129131
};
130132

131133
for (const [name, source] of Object.entries(samples)) {

src/languages/gleam/gleam.grammar

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
@top SourceFile { Element* }
2+
3+
@precedence {
4+
def,
5+
call,
6+
importAlias
7+
}
8+
9+
Element {
10+
ModuleComment |
11+
DocComment |
12+
LineComment |
13+
Attribute |
14+
String |
15+
Float |
16+
Integer |
17+
Discard |
18+
ImportStatement |
19+
FunctionDefinition |
20+
FunctionCall |
21+
As | Assert | Case | Const | Echo | Else | Fn | If |
22+
Let | Opaque | Panic | Pub | Todo | Type | Use |
23+
True | False | Nil |
24+
TypeName |
25+
Identifier |
26+
Operator |
27+
Punctuation |
28+
divOp |
29+
dotOp |
30+
Block |
31+
Parenthesized |
32+
Bracketed |
33+
BitArray
34+
}
35+
36+
Block { "{" Element* "}" }
37+
Parenthesized { "(" Element* ")" }
38+
Bracketed { "[" Element* "]" }
39+
BitArray { "<<" Element* ">>" }
40+
41+
Attribute {
42+
"@" Identifier !call "(" Element* ")" |
43+
"@" Identifier
44+
}
45+
46+
FunctionDefinition {
47+
Fn !def Identifier
48+
}
49+
50+
FunctionCall {
51+
Identifier !call Parenthesized
52+
}
53+
54+
ImportStatement {
55+
!importAlias Import ModulePath (dotOp UnqualifiedImports)? (As (Identifier | Discard))?
56+
}
57+
58+
ModulePath {
59+
Identifier (!call divOp Identifier)*
60+
}
61+
62+
UnqualifiedImports {
63+
"{" (UnqualifiedImport ("," UnqualifiedImport)* ","?)? "}"
64+
}
65+
66+
UnqualifiedImport {
67+
Type? TypeName (As TypeName)? |
68+
Identifier (As Identifier)?
69+
}
70+
71+
As { @specialize<Identifier, "as"> }
72+
Assert { @specialize<Identifier, "assert"> }
73+
Case { @specialize<Identifier, "case"> }
74+
Const { @specialize<Identifier, "const"> }
75+
Echo { @specialize<Identifier, "echo"> }
76+
Else { @specialize<Identifier, "else"> }
77+
Fn { @specialize<Identifier, "fn"> }
78+
If { @specialize<Identifier, "if"> }
79+
Import { @specialize<Identifier, "import"> }
80+
Let { @specialize<Identifier, "let"> }
81+
Opaque { @specialize<Identifier, "opaque"> }
82+
Panic { @specialize<Identifier, "panic"> }
83+
Pub { @specialize<Identifier, "pub"> }
84+
Todo { @specialize<Identifier, "todo"> }
85+
Type { @specialize<Identifier, "type"> }
86+
Use { @specialize<Identifier, "use"> }
87+
88+
True { @specialize<TypeName, "True"> }
89+
False { @specialize<TypeName, "False"> }
90+
Nil { @specialize<TypeName, "Nil"> }
91+
92+
divOp { @specialize<Operator, "/"> }
93+
dotOp { @specialize<Punctuation, "."> }
94+
95+
@tokens {
96+
spaces { $[ \t]+ }
97+
newline { "\r"? "\n" }
98+
99+
ModuleComment { "////" ![\n\r]* }
100+
DocComment { "///" ![\n\r]* }
101+
LineComment { "//" ![\n\r]* }
102+
103+
String {
104+
'"' ( "\\" _ | !["\\\n\r] )* '"'
105+
}
106+
107+
Float {
108+
$[0-9] $[0-9_]* "." $[0-9_]* ($[eE] $[+-]? $[0-9_]+)? |
109+
"-" $[0-9] $[0-9_]* "." $[0-9_]* ($[eE] $[+-]? $[0-9_]+)?
110+
}
111+
112+
Integer {
113+
("0x" | "0X") $[0-9a-fA-F_]+ |
114+
("0o" | "0O") $[0-7_]+ |
115+
("0b" | "0B") $[01_]+ |
116+
$[0-9] $[0-9_]* |
117+
"-" $[0-9] $[0-9_]*
118+
}
119+
120+
Discard { "_" $[a-zA-Z0-9_]* }
121+
122+
TypeName { $[A-Z] $[a-zA-Z0-9_]* }
123+
124+
Identifier { $[a-z_] $[a-zA-Z0-9_]* }
125+
126+
Operator {
127+
"<-" | "->" | "<=" | ">=" | "==" | "!=" |
128+
"<=." | ">=." | "<." | ">." |
129+
"+." | "-." | "*." | "/." |
130+
"+" | "-" | "*" | "/" | "%" |
131+
"&&" | "||" | "!" |
132+
"<>" | "|>" | "=" | "|" | ".." |
133+
"<" | ">"
134+
}
135+
136+
Punctuation {
137+
"," | "." | ":" | ";" | "#"
138+
}
139+
140+
"@" "{" "}" "(" ")" "[" "]" "<<" ">>"
141+
142+
@precedence {
143+
ModuleComment,
144+
DocComment,
145+
LineComment,
146+
String,
147+
Float,
148+
Integer,
149+
TypeName,
150+
Discard,
151+
Identifier,
152+
Operator,
153+
Punctuation
154+
}
155+
}
156+
157+
@skip { spaces | newline }
158+
159+
@detectDelim

src/languages/gleam/index.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { completeFromList } from "@codemirror/autocomplete";
2+
import {
3+
delimitedIndent,
4+
foldInside,
5+
foldNodeProp,
6+
indentNodeProp,
7+
LanguageSupport,
8+
LRLanguage,
9+
} from "@codemirror/language";
10+
import { styleTags, tags as t } from "@lezer/highlight";
11+
import { parser } from "./parser";
12+
13+
const configuredParser = parser.configure({
14+
props: [
15+
styleTags({
16+
ModuleComment: t.docComment,
17+
DocComment: t.docComment,
18+
LineComment: t.lineComment,
19+
String: t.string,
20+
Float: t.number,
21+
Integer: t.number,
22+
Discard: t.comment,
23+
"FunctionDefinition/Identifier": t.function(t.definition(t.variableName)),
24+
"FunctionCall/Identifier": t.function(t.variableName),
25+
TypeName: t.typeName,
26+
ModulePath: t.namespace,
27+
Identifier: t.variableName,
28+
"True False": t.bool,
29+
Nil: t.null,
30+
"As Assert Case Else If Import Let Pub Type Use Const Opaque": t.keyword,
31+
Fn: t.definitionKeyword,
32+
"Panic Todo Echo": t.controlKeyword,
33+
Operator: t.operator,
34+
Attribute: t.meta,
35+
"( )": t.paren,
36+
"[ ]": t.squareBracket,
37+
"{ }": t.brace,
38+
"<< >>": t.special(t.brace),
39+
Punctuation: t.punctuation,
40+
}),
41+
indentNodeProp.add({
42+
Block: delimitedIndent({ closing: "}" }),
43+
Bracketed: delimitedIndent({ closing: "]" }),
44+
Parenthesized: delimitedIndent({ closing: ")" }),
45+
BitArray: delimitedIndent({ closing: ">>" }),
46+
}),
47+
foldNodeProp.add({
48+
Block: foldInside,
49+
Bracketed: foldInside,
50+
Parenthesized: foldInside,
51+
BitArray: foldInside,
52+
}),
53+
],
54+
});
55+
56+
export const gleamLanguage = LRLanguage.define({
57+
name: "gleam",
58+
parser: configuredParser,
59+
languageData: {
60+
commentTokens: { line: "//" },
61+
closeBrackets: { brackets: ["(", "[", "{", '"', "<<"] },
62+
indentOnInput: /^\s*}$/,
63+
},
64+
});
65+
66+
const keywords = [
67+
"as", "assert", "case", "const", "echo", "else", "fn", "if", "import",
68+
"let", "opaque", "panic", "pub", "todo", "type", "use",
69+
];
70+
71+
const builtinTypes = [
72+
"Int", "Float", "String", "Bool", "Nil", "UtfCodepoint", "BitArray", "Result", "List",
73+
];
74+
75+
const gleamCompletion = gleamLanguage.data.of({
76+
autocomplete: completeFromList([
77+
...keywords.map((label) => ({ label, type: "keyword" })),
78+
...builtinTypes.map((label) => ({ label, type: "type" })),
79+
{ label: "True", type: "constant" },
80+
{ label: "False", type: "constant" },
81+
{ label: "Nil", type: "constant" },
82+
]),
83+
});
84+
85+
export function gleam() {
86+
return new LanguageSupport(gleamLanguage, [gleamCompletion]);
87+
}
88+
89+
export const gleamMode = {
90+
name: "gleam",
91+
caption: "Gleam",
92+
extensions: ["gleam"],
93+
load: gleam,
94+
};

src/languages/gleam/parser.js

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)