Skip to content

Commit b0f4ab8

Browse files
committed
feat: add yaml enhanced mode with proper lazer grammar
1 parent 0550026 commit b0f4ab8

11 files changed

Lines changed: 316 additions & 6 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@lezer/common": "^1.5.2",
1717
"@lezer/highlight": "^1.2.3",
1818
"@lezer/lr": "^1.4.10",
19+
"@lezer/yaml": "^1.0.4",
1920
"@replit/codemirror-lang-svelte": "^6.0.0",
2021
"@viz-js/lang-dot": "^1.0.5",
2122
"acode-plugin-types": "^1.11.7-patch.2",

plugin.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"autohotkey",
1818
"zig",
1919
"svelte",
20-
"jsonc"
20+
"jsonc",
21+
"yaml"
2122
],
2223
"price": 0,
2324
"permissions": [],

plugin.zip

14.7 KB
Binary file not shown.

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Adds CodeMirror language support for languages that are not bundled with Acode.
1212
- Git attributes (`.gitattributes`, Git attributes-style files)
1313
- Git Commit Message (`^COMMIT_EDITMSG`, `^commit_editmsg`)
1414
- JSONC (`.jsonc`)
15+
- YAML Enhanced (`.yaml`, `.yml`), with structural parsing and core-schema scalar highlighting
1516
- BibTeX (`.bib`)
1617
- Elixir (`.ex`, `.exs`, `.eex`, `.heex`, `.leex`)
1718
- EJS (`.ejs`)

scripts/test-parser.mjs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,129 @@ for (const sample of jsoncSamples) {
179179

180180
console.log(`Validated ${jsoncSamples.length} JSONC parser fixtures.`);
181181

182+
const yamlBehaviorBundle = path.join(os.tmpdir(), "acode-yaml-behavior-test-bundle.mjs");
183+
const yamlBehaviorSource = String.raw`
184+
import assert from "node:assert/strict";
185+
import { EditorState } from "@codemirror/state";
186+
import { foldable, getIndentation, indentUnit, syntaxTree } from "@codemirror/language";
187+
import { classHighlighter, highlightTree } from "@lezer/highlight";
188+
import { yaml, yamlLanguage } from "./src/languages/yaml/index.js";
189+
190+
const source = [
191+
"%YAML 1.2",
192+
"---",
193+
"defaults: &defaults",
194+
" enabled: true",
195+
" retries: 3",
196+
" ratio: -1.25e+2",
197+
" missing: null",
198+
" image: ghcr.io/acme/app:latest",
199+
"jobs:",
200+
" build:",
201+
" <<: *defaults",
202+
" runs-on: ubuntu-latest # runner label",
203+
" strategy: {matrix: {node: [18, 20]}}",
204+
" steps:",
205+
" - name: Test",
206+
" run: |-",
207+
" npm test # block scalar content",
208+
"tagged: !!str value",
209+
"...",
210+
"",
211+
].join("\n");
212+
213+
const tree = yamlLanguage.parser.parse(source);
214+
const names = new Set();
215+
const errors = [];
216+
tree.iterate({
217+
enter(node) {
218+
names.add(node.name);
219+
if (node.type.isError) errors.push([node.from, node.to]);
220+
},
221+
});
222+
223+
assert.equal(errors.length, 0, "YAML fixture produced parse errors");
224+
for (const name of [
225+
"Directive",
226+
"BlockMapping",
227+
"BlockSequence",
228+
"FlowMapping",
229+
"FlowSequence",
230+
"BlockLiteral",
231+
"Anchor",
232+
"Alias",
233+
"Tag",
234+
"Boolean",
235+
"Integer",
236+
"Float",
237+
"Null",
238+
"PlainString",
239+
]) {
240+
assert(names.has(name), "YAML fixture did not produce " + name);
241+
}
242+
243+
const spans = [];
244+
highlightTree(tree, classHighlighter, (from, to, cls) => {
245+
spans.push({ text: source.slice(from, to), cls });
246+
});
247+
248+
function isHighlighted(text, tokenClass) {
249+
return spans.some((span) => span.text === text && span.cls.includes(tokenClass));
250+
}
251+
252+
assert(isHighlighted("defaults", "tok-propertyName"));
253+
assert(isHighlighted("true", "tok-bool"));
254+
assert(isHighlighted("3", "tok-number"));
255+
assert(isHighlighted("-1.25e+2", "tok-number"));
256+
assert(isHighlighted("null", "tok-keyword"));
257+
assert(isHighlighted("ubuntu-latest", "tok-string"));
258+
assert(isHighlighted("&defaults", "tok-labelName"));
259+
assert(isHighlighted("*defaults", "tok-labelName"));
260+
assert(isHighlighted("!!str", "tok-typeName"));
261+
assert(isHighlighted("# runner label", "tok-comment"));
262+
assert(!isHighlighted("# block scalar content", "tok-comment"));
263+
264+
const indentSource = "jobs:\n build:\n steps:\n - name: Test\n run: npm test\n";
265+
const indentState = EditorState.create({
266+
doc: indentSource,
267+
extensions: [yaml(), indentUnit.of(" ")],
268+
});
269+
syntaxTree(indentState);
270+
assert.equal(getIndentation(indentState, indentState.doc.line(4).from), 4);
271+
assert.equal(getIndentation(indentState, indentState.doc.line(5).from), 8);
272+
assert(foldable(indentState, indentState.doc.line(1).from, indentState.doc.line(1).to));
273+
274+
const flowSource = "matrix: {\n node: [18, 20]\n}\n";
275+
const flowState = EditorState.create({
276+
doc: flowSource,
277+
extensions: [yaml(), indentUnit.of(" ")],
278+
});
279+
syntaxTree(flowState);
280+
assert.equal(getIndentation(flowState, flowState.doc.line(3).from), 0);
281+
282+
console.log("Validated YAML parsing, highlighting, indentation, and folding fixtures.");
283+
`;
284+
285+
await build({
286+
stdin: {
287+
contents: yamlBehaviorSource,
288+
resolveDir: process.cwd(),
289+
sourcefile: "yaml-behavior-test.mjs",
290+
loader: "js",
291+
},
292+
bundle: true,
293+
format: "esm",
294+
platform: "node",
295+
outfile: yamlBehaviorBundle,
296+
logLevel: "silent",
297+
});
298+
299+
try {
300+
await import(pathToFileURL(yamlBehaviorBundle).href + `?t=${Date.now()}`);
301+
} finally {
302+
fs.rmSync(yamlBehaviorBundle, { force: true });
303+
}
304+
182305
const { parser: ejsParser } = await bundledRequire(
183306
"src/languages/ejs/parser.js",
184307
"acode-ejs-parser-test.cjs",
@@ -487,4 +610,3 @@ for (const sample of gleamSamples) {
487610
}
488611

489612
console.log(`Validated ${gleamSamples.length} Gleam parser fixtures.`);
490-

scripts/test-plugin-runtime.cjs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const registered = [];
1919
const unregistered = [];
2020

2121
const editorLanguages = {
22-
get() {
23-
return null;
22+
get(name) {
23+
return name === "yaml" ? { name: "yaml" } : null;
2424
},
2525
register(name, extensions, caption, load) {
2626
registered.push({ name, extensions, caption, load });
@@ -56,6 +56,7 @@ async function test() {
5656
"zig",
5757
"gitignore",
5858
"jsonc",
59+
"yaml-enhanced",
5960
"bibtex",
6061
"elixir",
6162
"golfscript",
@@ -80,6 +81,8 @@ async function test() {
8081
assert(modes.get("zig").extensions.includes("zon"));
8182
assert(modes.get("gitignore").extensions.includes("gitignore"));
8283
assert(modes.get("jsonc").extensions.includes("jsonc"));
84+
assert(modes.get("yaml-enhanced").extensions.includes("yaml"));
85+
assert(modes.get("yaml-enhanced").extensions.includes("yml"));
8386
assert(modes.get('gitcommitmsg').extensions.includes("^COMMIT_EDITMSG"));
8487

8588
const samples = {
@@ -116,6 +119,15 @@ message:
116119
zig: 'const std = @import("std");',
117120
gitignore: "# build output\ndist/\n!important.log\n*.tmp\n",
118121
jsonc: '{\n // comment\n "foo": "bar",\n}',
122+
"yaml-enhanced": `defaults: &defaults
123+
enabled: true
124+
retries: 3
125+
jobs:
126+
build:
127+
<<: *defaults
128+
runs-on: ubuntu-latest
129+
matrix: {node: [18, 20]}
130+
`,
119131
bibtex: "@article{example, title={Example}}",
120132
elixir: "defmodule Example do\nend",
121133
golfscript: "1 2 +",
@@ -182,7 +194,7 @@ index 0000000..e69de29`
182194
});
183195
const tree = support.language.parser.parse(source);
184196
assert.equal(tree.length, source.length, `${name} did not parse the full fixture`);
185-
if (name === "asciidoc" || name === "assembly") {
197+
if (name === "asciidoc" || name === "assembly" || name === "yaml-enhanced") {
186198
const nodeNames = new Set();
187199
const errors = [];
188200
tree.iterate({
@@ -195,7 +207,9 @@ index 0000000..e69de29`
195207
const expectedNodes =
196208
name === "asciidoc"
197209
? ["Heading1", "Heading2", "AttributeLine", "Xref", "ListingBlock", "ListItem"]
198-
: ["DirectiveName", "Label", "Instruction", "Register", "String"];
210+
: name === "assembly"
211+
? ["DirectiveName", "Label", "Instruction", "Register", "String"]
212+
: ["BlockMapping", "FlowMapping", "FlowSequence", "Anchor", "Alias", "Boolean", "Integer"];
199213
for (const nodeName of expectedNodes) {
200214
assert(
201215
nodeNames.has(nodeName),

src/languages/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { communityLanguageModes } from "./community";
55
import { gitignoreMode } from "./gitignore";
66
import { zigMode } from "./zig";
77
import { jsoncMode } from "./jsonc";
8+
import { yamlMode } from "./yaml";
89

910
import { ejsMode } from "./ejs"
1011
import { gitattributesMode } from "./gitattributes";
@@ -23,6 +24,7 @@ export const languageModes = [
2324
zigMode,
2425
gitignoreMode,
2526
jsoncMode,
27+
yamlMode,
2628
...communityLanguageModes,
2729
ejsMode,
2830
gitattributesMode,

src/languages/yaml/index.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
delimitedIndent,
3+
foldInside,
4+
foldNodeProp,
5+
indentNodeProp,
6+
LanguageSupport,
7+
LRLanguage,
8+
} from "@codemirror/language";
9+
import { parseMixed } from "@lezer/common";
10+
import { styleTags, tags as t } from "@lezer/highlight";
11+
import { parser as yamlParser } from "@lezer/yaml";
12+
import { parser as scalarParser } from "./parser";
13+
14+
// @lezer/yaml provides an error-tolerant structural YAML parser. The nested
15+
// scalar parser retains that structure while distinguishing the core-schema
16+
// null, boolean, integer, float, and plain-string values described by the
17+
// tree-sitter YAML grammar.
18+
const configuredScalarParser = scalarParser.configure({
19+
props: [
20+
styleTags({
21+
Null: t.null,
22+
Boolean: t.bool,
23+
"Integer Float": t.number,
24+
PlainString: t.string,
25+
}),
26+
],
27+
});
28+
29+
const configuredParser = yamlParser.configure({
30+
wrap: parseMixed((node) => {
31+
if (node.name !== "Literal" || node.node.parent?.name === "Key") {
32+
return null;
33+
}
34+
return { parser: configuredScalarParser };
35+
}),
36+
props: [
37+
indentNodeProp.add({
38+
Stream: (context) => {
39+
for (
40+
let before = context.node.resolve(context.pos, -1);
41+
before && before.to >= context.pos;
42+
before = before.parent
43+
) {
44+
if (
45+
before.name === "BlockLiteralContent" &&
46+
before.from < before.to
47+
) {
48+
return context.baseIndentFor(before);
49+
}
50+
if (before.name === "BlockLiteral") {
51+
return context.baseIndentFor(before) + context.unit;
52+
}
53+
if (
54+
before.name === "BlockSequence" ||
55+
before.name === "BlockMapping"
56+
) {
57+
return context.column(before.firstChild.from, 1);
58+
}
59+
if (before.name === "QuotedLiteral") return null;
60+
if (before.name === "Literal") {
61+
const column = context.column(before.from, 1);
62+
if (column === context.lineIndent(before.from, 1)) return column;
63+
if (before.to > context.pos) return null;
64+
}
65+
}
66+
return null;
67+
},
68+
FlowMapping: delimitedIndent({ closing: "}" }),
69+
FlowSequence: delimitedIndent({ closing: "]" }),
70+
}),
71+
foldNodeProp.add({
72+
"FlowMapping FlowSequence": foldInside,
73+
"Item Pair BlockLiteral": (node, state) => ({
74+
from: state.doc.lineAt(node.from).to,
75+
to: node.to,
76+
}),
77+
}),
78+
],
79+
});
80+
81+
export const yamlLanguage = LRLanguage.define({
82+
name: "yaml",
83+
parser: configuredParser,
84+
languageData: {
85+
commentTokens: { line: "#" },
86+
closeBrackets: { brackets: ["[", "{", '"', "'"] },
87+
indentOnInput: /^\s*[\]\}]$/,
88+
wordChars: "-_",
89+
},
90+
});
91+
92+
export function yaml() {
93+
return new LanguageSupport(yamlLanguage);
94+
}
95+
96+
// Acode already owns the `yaml` registry key. Registering a distinct mode lets
97+
// this plugin coexist with it, while Acode's later-registration tie-breaker
98+
// selects this structured parser for .yaml and .yml files.
99+
export const yamlMode = {
100+
name: "yaml-enhanced",
101+
caption: "YAML (Enhanced)",
102+
extensions: ["yaml", "yml"],
103+
load: yaml,
104+
};

src/languages/yaml/parser.js

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

src/languages/yaml/parser.terms.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// This file was generated by lezer-generator. You probably shouldn't edit it.
2+
export const
3+
Scalar = 1,
4+
Null = 2,
5+
Boolean = 3,
6+
Float = 4,
7+
Integer = 5,
8+
PlainString = 6

0 commit comments

Comments
 (0)