Compare commits

..
Author SHA1 Message Date
Mohamed Boudra 586b605429 fix(highlight): handle Unicode heredoc delimiters and repeated template markers
Heredoc delimiters only accepted ASCII, so a Unicode delimiter was recorded
as empty and the rest of the file rendered as a string. `$$${` consumed the
second marker before confirming `{` followed, which turned an escape into an
interpolation.
2026-11-08 14:22:56 +01:00
Mohamed Boudra b03695d787 feat(highlight): highlight Terraform and HCL files
The grammar is vendored rather than added as a dependency because it only
ships inside an editor extension, and vendoring keeps the highlight
package limited to Lezer primitives without changing the lockfile.

Block labels map to the string role: the upstream labelName tag has no
Paseo syntax role and would render labels uncoloured.
2026-11-08 13:05:22 +01:00
13 changed files with 683 additions and 288 deletions
@@ -46,6 +46,7 @@ const LANGUAGE_ALIASES: Record<string, string> = {
"objective-c": "m",
markdown: "md",
elixir: "ex",
terraform: "tf",
};
function fenceLanguageToExtension(info: string | null | undefined): string | null {
@@ -430,20 +430,6 @@ function agentPermission(id: string): AgentPermissionRequest {
return { id, provider: "codex", name: id, kind: "tool", title: id };
}
const relayPrimary: HostConnection = {
id: "relay:relay.paseo.sh:443",
type: "relay",
relayEndpoint: "relay.paseo.sh:443",
daemonPublicKeyB64: "pk_test",
};
const relayAlternate: HostConnection = {
id: "relay:relay-next.paseo.sh:443",
type: "relay",
relayEndpoint: "relay-next.paseo.sh:443",
daemonPublicKeyB64: "pk_test",
};
function makeHost(input?: Partial<HostProfile>): HostProfile {
const direct: HostConnection = {
id: "direct:lan:6767",
@@ -1580,233 +1566,6 @@ describe("HostRuntimeController", () => {
expect(switched).toBe(true);
});
it("keeps the live connection while one alternate sample is re-read at active ticks", async () => {
// Replays the recorded Android sequence: both relays probed at startup, the alternate
// measured once, then only the active connection is re-read every ten seconds.
useHostRuntimeClock();
const host = makeHost({
connections: [relayPrimary, relayAlternate],
preferredConnectionId: relayPrimary.id,
});
const clients: FakeDaemonClient[] = [];
const latencies: Record<string, number | Error> = {
[relayPrimary.id]: 1053,
[relayAlternate.id]: 251,
};
const controller = new HostRuntimeController({
host,
deps: makeDeps(latencies, clients),
});
await controller.start();
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
const clientsAfterStart = clients.length;
await vi.advanceTimersByTimeAsync(10_500);
activeClient.heartbeatReportsRtt(376);
await vi.advanceTimersByTimeAsync(30_000);
expect(clients.length).toBe(clientsAfterStart);
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
expect(controller.getSnapshot().client).toBe(activeClient);
expect(activeClient.isDisposed()).toBe(false);
await controller.stop();
});
it("does not hold the previous socket's RTT against a reconnected active connection", async () => {
// Replays the recorded foreground recovery: the active client reconnects on its own while an
// alternate probe is in flight. The recovered socket has no heartbeat RTT yet.
useHostRuntimeClock();
const host = makeHost({
connections: [relayPrimary, relayAlternate],
preferredConnectionId: relayPrimary.id,
});
const clients: FakeDaemonClient[] = [];
const latencies: Record<string, number | Error> = {
[relayPrimary.id]: 300,
[relayAlternate.id]: 392,
};
let connectGate: Promise<void> | null = null;
const baseDeps = makeDeps(latencies, clients);
const controller = new HostRuntimeController({
host,
deps: {
...baseDeps,
connectToDaemon: async (input) => {
if (connectGate) await connectGate;
return baseDeps.connectToDaemon(input);
},
},
});
await controller.start();
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
activeClient.heartbeatReportsRtt(403);
await vi.advanceTimersByTimeAsync(150_500);
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
activeClient.setConnectionState({ status: "disconnected", reason: "liveness_timeout" });
activeClient.setConnectionState({ status: "connecting", attempt: 2 });
activeClient.heartbeatReportsRtt(null);
latencies[relayAlternate.id] = 280;
const alternateConnect = createDeferred<void>();
connectGate = alternateConnect.promise;
await vi.advanceTimersByTimeAsync(2_800);
activeClient.setConnectionState({ status: "connected" });
await vi.advanceTimersByTimeAsync(500);
alternateConnect.resolve();
connectGate = null;
await vi.advanceTimersByTimeAsync(100);
expect(controller.getSnapshot().probeByConnectionId.get(relayAlternate.id)).toEqual({
status: "available",
latencyMs: 280,
});
const clientsAfterProbe = clients.length;
await vi.advanceTimersByTimeAsync(30_000);
expect(clients.length).toBe(clientsAfterProbe);
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
expect(controller.getSnapshot().client).toBe(activeClient);
expect(activeClient.isDisposed()).toBe(false);
await controller.stop();
});
it("starts the switch streak over when the active socket reconnects", async () => {
useHostRuntimeClock();
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
const clients: FakeDaemonClient[] = [];
const latencies: Record<string, number | Error> = {
"direct:lan:6767": 15,
"relay:relay.paseo.sh:443": 60,
};
const controller = new HostRuntimeController({
host,
deps: makeDeps(latencies, clients),
});
await controller.start({ autoProbe: false });
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
latencies["relay:relay.paseo.sh:443"] = 30;
activeClient.heartbeatReportsRtt(95);
for (let index = 0; index < 2; index += 1) {
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
}
activeClient.setConnectionState({ status: "disconnected", reason: "network changed" });
activeClient.setConnectionState({ status: "connected" });
activeClient.heartbeatReportsRtt(95);
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
for (let index = 0; index < 2; index += 1) {
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
}
expect(controller.getSnapshot().activeConnectionId).toBe("relay:relay.paseo.sh:443");
await controller.stop();
});
it("ends the switch streak when a fresh probe of the alternate fails", async () => {
useHostRuntimeClock();
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
const clients: FakeDaemonClient[] = [];
const latencies: Record<string, number | Error> = {
"direct:lan:6767": 15,
"relay:relay.paseo.sh:443": 60,
};
const controller = new HostRuntimeController({
host,
deps: makeDeps(latencies, clients),
});
await controller.start({ autoProbe: false });
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
activeClient.heartbeatReportsRtt(95);
latencies["relay:relay.paseo.sh:443"] = 30;
for (let index = 0; index < 2; index += 1) {
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
}
latencies["relay:relay.paseo.sh:443"] = new Error("relay unreachable");
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
expect(controller.getSnapshot().probeByConnectionId.get("relay:relay.paseo.sh:443")).toEqual({
status: "unavailable",
latencyMs: null,
});
latencies["relay:relay.paseo.sh:443"] = 30;
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
for (let index = 0; index < 2; index += 1) {
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
}
expect(controller.getSnapshot().activeConnectionId).toBe("relay:relay.paseo.sh:443");
await controller.stop();
});
it("restarts the streak when a different candidate becomes the fastest fresh sample", async () => {
useHostRuntimeClock();
const direct: HostConnection = {
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
};
const host = makeHost({
connections: [direct, relayPrimary, relayAlternate],
preferredConnectionId: direct.id,
});
const clients: FakeDaemonClient[] = [];
const latencies: Record<string, number | Error> = {
[direct.id]: 15,
[relayPrimary.id]: 60,
[relayAlternate.id]: 70,
};
const controller = new HostRuntimeController({
host,
deps: makeDeps(latencies, clients),
});
await controller.start({ autoProbe: false });
expect(controller.getSnapshot().activeConnectionId).toBe(direct.id);
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
activeClient.heartbeatReportsRtt(95);
latencies[relayPrimary.id] = 30;
for (let index = 0; index < 2; index += 1) {
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
expect(controller.getSnapshot().activeConnectionId).toBe(direct.id);
}
latencies[relayAlternate.id] = 20;
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
expect(controller.getSnapshot().activeConnectionId).toBe(direct.id);
for (let index = 0; index < 2; index += 1) {
await vi.advanceTimersByTimeAsync(120_000);
await controller.runProbeCycleNow();
}
expect(controller.getSnapshot().activeConnectionId).toBe(relayAlternate.id);
await controller.stop();
});
it("exposes one snapshot with active connection and status from same source", async () => {
const host = makeHost();
const clients: FakeDaemonClient[] = [];
+23 -46
View File
@@ -1063,46 +1063,37 @@ export class HostRuntimeController {
expectedProbeVersion: requestVersion,
});
}
this.resetSwitchCandidate();
this.switchCandidateConnectionId = null;
this.switchCandidateHitCount = 0;
return;
}
// Adaptive switching counts fresh comparisons only. The probe map keeps the last known
// sample per connection for display and failover; a streak hit needs a candidate measured
// in this cycle and a heartbeat RTT from the socket that is active right now. Re-reading
// the cached map at every ten-second active tick turned one idle-probe sample into three
// "consecutive" wins, and a reconnected socket inherited its predecessor's RTT, so a
// working relay connection was replaced within seconds of coming online.
const sampledCandidates = connectionsToProbe.filter(
(connection) => connection.id !== currentActiveConnectionId,
);
if (sampledCandidates.length === 0) {
// An active-only tick carries no evidence about any candidate.
return;
}
const fastest =
sampledCandidates
.flatMap((connection) => {
const probe = probeByConnectionId.get(connection.id);
return probe?.status === "available"
? [{ connectionId: connection.id, latencyMs: probe.latencyMs }]
: [];
})
.sort((left, right) => left.latencyMs - right.latencyMs)[0] ?? null;
if (!fastest) {
// Every candidate sampled this cycle failed: that breaks the streak.
this.resetSwitchCandidate();
if (!activeProbe || activeProbe.status !== "available") {
return;
}
const activeRttMs = this.currentActiveSocketRttMs();
if (activeRttMs === null) {
const available = Array.from(probeByConnectionId.entries())
.filter(
(entry): entry is [string, Extract<ConnectionProbeState, { status: "available" }>] =>
entry[1].status === "available",
)
.map(([connectionId, probe]) => ({
connectionId,
latencyMs: probe.latencyMs,
}))
.sort((left, right) => left.latencyMs - right.latencyMs);
const fastest = available[0] ?? null;
if (!fastest || fastest.connectionId === currentActiveConnectionId) {
this.switchCandidateConnectionId = null;
this.switchCandidateHitCount = 0;
return;
}
const improvement = activeRttMs - fastest.latencyMs;
const improvement = activeProbe.latencyMs - fastest.latencyMs;
if (improvement < ADAPTIVE_SWITCH_THRESHOLD_MS) {
this.resetSwitchCandidate();
this.switchCandidateConnectionId = null;
this.switchCandidateHitCount = 0;
return;
}
@@ -1114,7 +1105,8 @@ export class HostRuntimeController {
}
if (this.switchCandidateHitCount >= ADAPTIVE_SWITCH_CONSECUTIVE_PROBES) {
this.resetSwitchCandidate();
this.switchCandidateConnectionId = null;
this.switchCandidateHitCount = 0;
await this.switchToConnection({
connectionId: fastest.connectionId,
expectedProbeVersion: requestVersion,
@@ -1225,19 +1217,6 @@ export class HostRuntimeController {
});
}
// The client clears its heartbeat RTT when a socket connects, so a connected client's RTT
// describes the socket in use right now; a reconnecting client still carries the old one.
private currentActiveSocketRttMs(): number | null {
const client = this.snapshot.client;
if (!client || client.getConnectionState().status !== "connected") return null;
return client.getLastLivenessRttMs();
}
private resetSwitchCandidate(): void {
this.switchCandidateConnectionId = null;
this.switchCandidateHitCount = 0;
}
// The active connection is probed through its own client, which may still be
// reconnecting when the probe starts. A verdict taken then is stale if the client
// has since connected, so the decision uses the client's current state.
@@ -1527,8 +1506,6 @@ export class HostRuntimeController {
return;
}
if (state.status === "connected") this.learnRelaySecurity(client, connection.id);
// Switch evidence belongs to the socket it was measured against.
if (state.status !== "connected") this.resetSwitchCandidate();
this.applyConnectionEvent({ type: "client_state", state, lastError: client.lastError });
this.updateSnapshot({
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
+2 -1
View File
@@ -5,7 +5,8 @@
"files": [
"dist",
"!dist/**/*.map",
"src/astro/LICENSE"
"src/astro/LICENSE",
"src/hcl/LICENSE"
],
"type": "module",
"main": "./dist/index.js",
@@ -3,6 +3,7 @@ import { setImmediate } from "node:timers/promises";
import { highlightCodeWithBudget, HIGHLIGHT_MAX_SOURCE_BYTES } from "../budgeted-highlighter.js";
import type { HighlightToken } from "../types.js";
import { highlightCode, highlightCodeInSteps, highlightLine } from "../highlighter.js";
import { getParserForFile } from "../parsers.js";
describe("highlightCode", () => {
it("highlights JavaScript code with correct token styles", () => {
@@ -344,6 +345,137 @@ const title = "Hello";
});
});
describe("Terraform", () => {
function styleOf(line: HighlightToken[], text: string) {
return line.find((token) => token.text === text)?.style;
}
it("highlights blocks, labels, attributes, and values", () => {
const code = [
"# The web server",
'resource "aws_instance" "web" {',
" count = 2",
" enabled = true",
' tags = merge(var.labels, { Name = "web" })',
"}",
].join("\n");
const result = highlightCode(code, "main.tf");
expect(result[0][0]).toEqual({ text: "# The web server", style: "comment" });
expect(styleOf(result[1], "resource")).toBe("type");
expect(styleOf(result[1], '"aws_instance"')).toBe("string");
expect(styleOf(result[1], '"web"')).toBe("string");
expect(styleOf(result[2], "count")).toBe("definition");
expect(styleOf(result[2], "2")).toBe("number");
expect(styleOf(result[3], "true")).toBe("literal");
expect(styleOf(result[4], "merge")).toBe("function");
expect(styleOf(result[4], "var")).toBe("variable");
expect(styleOf(result[4], "labels")).toBe("property");
});
it("highlights expressions interpolated inside strings", () => {
const result = highlightCode('name = "app-${var.env}-web"', "main.tf");
expect(result[0].map((token) => [token.text, token.style])).toEqual([
["name", "definition"],
[" ", null],
["=", "operator"],
[" ", null],
['"app-', "string"],
["${", "punctuation"],
["var", "variable"],
[".", "operator"],
["env", "property"],
["}", "punctuation"],
['-web"', "string"],
]);
});
it("highlights heredoc bodies as strings and resumes after the terminator", () => {
const code = ["policy = <<-EOT", ' { "Version": "2012" }', " EOT", "retries = 3"].join("\n");
const result = highlightCode(code, "main.tf");
expect(styleOf(result[0], "EOT")).toBe("keyword");
expect(result[1].map((token) => token.style)).toEqual(["string"]);
expect(styleOf(result[3], "retries")).toBe("definition");
expect(styleOf(result[3], "3")).toBe("number");
});
it("ends a heredoc with a Unicode delimiter", () => {
const result = highlightCode("x = <<ÉOF\nfoo\nÉOF\ny = 1", "main.tf");
expect(styleOf(result[3], "y")).toBe("definition");
expect(styleOf(result[3], "1")).toBe("number");
});
it.each([
['x = "$$${var.name}"', "$"],
['x = "%%%{ if true }"', "%"],
])("keeps escaped template text in %s as a string", (code) => {
const line = highlightCode(code, "main.tf")[0];
expect(line[line.length - 1]).toEqual({ text: code.slice(4), style: "string" });
});
it("parses a real configuration without error nodes", () => {
const code = [
"terraform {",
' required_version = ">= 1.5"',
' backend "s3" {}',
"}",
"",
'variable "subnets" {',
" type = map(object({ cidr = string }))",
" default = {}",
"}",
"",
"locals {",
" names = [for key, subnet in var.subnets : upper(key) if subnet.cidr != null]",
' first = length(local.names) > 0 ? local.names[0] : "none"',
" arns = aws_instance.web[*].arn",
" /* block comment */",
" path = provider::time::rfc3339_parse(timestamp()) // trailing",
"}",
"",
'data "aws_iam_policy_document" "assume" {',
' dynamic "statement" {',
" for_each = var.subnets",
" content {",
' sid = "${statement.key}-%{ if statement.value.cidr != "" }set%{ else }unset%{ endif }"',
" }",
" }",
"}",
"",
'output "script" {',
" value = <<EOT",
"#!/bin/sh",
"echo ${local.first}",
"EOT",
"}",
].join("\n");
const parser = getParserForFile("main.tf");
if (!parser) throw new Error("Expected a parser for .tf files");
const errors: number[] = [];
parser.parse(code).iterate({
enter(node) {
if (node.type.isError) errors.push(node.from);
},
});
expect(errors.map((from) => code.slice(from, from + 30))).toEqual([]);
});
it("uses the same grammar for tfvars and hcl files", () => {
expect(highlightCode('region = "eu-west-1"', "prod.tfvars")).toEqual(
highlightCode('region = "eu-west-1"', "terragrunt.hcl"),
);
expect(styleOf(highlightLine('region = "eu-west-1"', "prod.tfvars"), "region")).toBe(
"definition",
);
});
});
describe("highlightCodeInSteps", () => {
it("splits a large file into many bounded steps with the same result", () => {
const code = Array.from(
@@ -22,6 +22,9 @@ describe("isLanguageSupported", () => {
expect(isLanguageSupported("test.dart")).toBe(true);
expect(isLanguageSupported("test.cs")).toBe(true);
expect(isLanguageSupported("test.nix")).toBe(true);
expect(isLanguageSupported("main.tf")).toBe(true);
expect(isLanguageSupported("prod.tfvars")).toBe(true);
expect(isLanguageSupported("terragrunt.hcl")).toBe(true);
expect(isLanguageSupported("test.ex")).toBe(true);
expect(isLanguageSupported("Counter.svelte")).toBe(true);
expect(isLanguageSupported("Page.astro")).toBe(true);
@@ -65,6 +68,9 @@ describe("getSupportedExtensions", () => {
expect(extensions).toContain("dart");
expect(extensions).toContain("cs");
expect(extensions).toContain("nix");
expect(extensions).toContain("tf");
expect(extensions).toContain("tfvars");
expect(extensions).toContain("hcl");
expect(extensions).toContain("json");
expect(extensions).toContain("svelte");
expect(extensions).toContain("astro");
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2021 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
import { styleTags, tags as t } from "@lezer/highlight";
export const hclHighlighting = styleTags({
"for in if else endfor endif": t.controlKeyword,
"Attribute/Identifier": t.definition(t.propertyName),
"ObjectKey/Expression/VariableExpr/Identifier": t.definition(t.propertyName),
"ObjectKey/Expression/LiteralValue/StringLit!": t.definition(t.propertyName),
"ForIntro/Identifier TemplateForStart/Identifier": t.definition(t.variableName),
"BlockType!": t.typeName,
// Paseo themes have no role for labelName; labels render as the strings they are.
"BlockLabel!": t.string,
"FunctionNamespace/Identifier": t.namespace,
"FunctionName/Identifier": t.function(t.variableName),
"VariableExpr/Identifier": t.variableName,
"GetAttr/Identifier": t.propertyName,
Identifier: t.name,
"QuotedTemplateStart QuotedTemplateEnd TemplateLiteral": t.string,
NumericLit: t.number,
"true false": t.bool,
null: t.null,
LineComment: t.lineComment,
BlockComment: t.blockComment,
"( )": t.paren,
"{ }": t.brace,
"[ ]": t.squareBracket,
ArithOp: t.arithmeticOperator,
LogicOp: t.logicOperator,
CompareOp: t.compareOperator,
ControlOp: t.controlOperator,
"ControlOp/Colon": t.controlOperator,
AssignOp: t.definitionOperator,
DerefOp: t.derefOperator,
HeredocIdentifier: t.special(t.controlKeyword),
HeredocStart: t.operator,
"TemplateInterpolationStart TemplateInterpolationEnd": t.special(t.brace),
"TemplateDirectiveStart TemplateDirectiveEnd": t.special(t.brace),
StripMarker: t.special(t.brace),
"AttrSplat FullSplat LegacyIndex": t.derefOperator,
"Arrow Ellipsis": t.punctuation,
"ObjectElem/Colon ForIntro/Colon Comma NamespaceSeparator": t.separator,
});
+61
View File
@@ -0,0 +1,61 @@
import {
delimitedIndent,
foldInside,
foldNodeProp,
indentNodeProp,
LRLanguage,
} from "@codemirror/language";
import type { SyntaxNode } from "@lezer/common";
import { parser } from "./parser.js";
export const hclLanguage = LRLanguage.define({
name: "HCL",
parser: parser.configure({
props: [
indentNodeProp.add({
Block: delimitedIndent({ closing: "}", align: false }),
}),
foldNodeProp.add({
"Object Tuple TemplateFor TemplateIf": foldInside,
BlockComment: (tree: SyntaxNode) => {
return { from: tree.from + 2, to: tree.to - 2 };
},
HeredocTemplate(tree: SyntaxNode) {
const start = tree.getChild("HeredocIdentifier");
if (!start) {
return null;
}
return {
from: start.to,
to: tree.to,
};
},
FunctionCall(tree: SyntaxNode) {
const start = tree.getChild("(");
const end = tree.getChild(")");
if (!start || !end) {
return null;
}
return {
from: start.from + 1,
to: end.to - 1,
};
},
Block(tree: SyntaxNode) {
const start = tree.getChild("{");
const end = tree.getChild("}");
if (!start || !end) {
return null;
}
return {
from: start.from + 1,
to: end.to - 1,
};
},
}),
],
}),
languageData: {
commentTokens: { line: "#", block: { open: "/*", close: "*/" } },
},
});
+64
View File
@@ -0,0 +1,64 @@
// Generated by lezer-generator, taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE).
// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here.
import { LRParser } from "@lezer/lr";
import { hclHighlighting } from "./highlight.js";
import {
scanQuotedTemplateStart,
scanQuotedTemplateEnd,
scanTemplateInterpolationStart,
scanTemplateInterpolationEnd,
scanTemplateDirectiveStart,
scanTemplateDirectiveEnd,
scanHeredocIdentifier,
scanTemplateLiteralChunk,
trackTemplate,
} from "./tokens.js";
const spec_Identifier = new Map<string, number>([
["true", 41],
["false", 43],
["null", 47],
["for", 66],
["in", 70],
["endfor", 74],
["if", 80],
["else", 84],
["endif", 88],
]);
export const parser = LRParser.deserialize({
version: 14,
states:
"GxQ]Q&jOOPeO&jOOOmQ&kO'#DcO!kQ&kO'#E_O!yQ&kO'#E^OOQO'#Ea'#EaO#UQ&jO'#ChQOQ&jOOPOOO'#Cd'#CdPOOO)C@`)C@`OOQy'#Co'#CoOOQy'#Cr'#CrO#aQ)vO'#CwOOQy'#Cm'#CmOOQ!b'#D['#D[O#oQ'|O'#DZOOQy'#Cv'#CvO$rQ&kO'#D`O$yQ&jO'#DeO%UQ&jO'#DdO&XQ&kO'#EqO&cQ&kO'#DcOOQy,59},59}O&jQ&jO,59}OOQy'#D]'#D]O(YQ'dO'#DgOOQO'#Ee'#EeO)PQ&jO'#DiO)UQ&jO'#DiOOQy'#Dp'#DpOOQP'#Dx'#DxOOQP'#Dy'#DyO%^Q&kO'#DwO*nQ'dO'#ClOOQy'#Dv'#DvOOQy'#El'#ElO%^Q&kO'#ElOOQy'#Cl'#ClO%^Q&kO,59TO#jQ)`O'#CtOOQP'#E`'#E`OOQP'#Eg'#EgO+eQ&kO,5:xO+pQ&jO,5:xOOQO-E8_-E8_O+xQ+YO'#CuO,ZQ)vO,59`O,iQ&sO'#CxO,sQ)tO'#C{O-OQ)tO'#DSOOQ%n'#Cz'#CzOOQ%n'#Eb'#EbO-ZQ)vO,59cOOQy,59c,59cO-iQ&jO'#DTO-tQ+WO,59uO.SQ&jO'#EpOOQy,59z,59zO._Q&jO,59zO%^Q&kO,5:]O.dQ&jO'#DrOOQP'#E['#E[O%^Q&kO,5:uO.iQ&kO,5:OOOQP'#Ed'#EdO/dQ&kO,5;]O/kQ&kO,5;]O/uQ&kO,5:`OOQy1G/i1G/iOOQO'#Dk'#DkOOQO,5:U,5:UO0pQ&jO'#DlOOQO-E8c-E8cO0xQ&jO,5:TO0}Q&kO,5:TOOQy,5:c,5:cOOQP'#D{'#D{OOQP'#D|'#D|OOQP'#D}'#D}OOQP'#EO'#EOOOQP'#EP'#EPOOQP'#EQ'#EQO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO'#ESOOQy'#ER'#ERO1UQ&jO'#EUOOQy'#EX'#EXOOQy'#EY'#EYOOQy'#EW'#EWOOQy,5;W,5;WO1ZQ&jO,5;WO1cQ&jO1G.oO1qQ&lO,59`OOQP-E8e-E8eO1vQ&jO1G0dOOQO1G0d1G0dO1yQ&jO1G0dO+xQ+YO'#EmOOQ%n,59a,59aOOQy1G.z1G.zOOQ%n,59d,59dO2OQ&sO,59dO2YQ&rO,59dOOQ%n,59g,59gO,sQ)tO,59gO2eQ&jO'#DQOOQ%n,59n,59nO2sQ)tO,59nO-OQ)tO,59nO3OQ&jO'#DVOOQ%n-E8`-E8`OOQy1G.}1G.}O3aQ&jO,59hO3fQ&jO,59oO%^Q&kO,59oO3nQ+WO1G/aOOQy1G/a1G/aO3|Q&kO,5;[O4TQ&jO,5;[OOQy1G/f1G/fO4]Q&jO1G/wO4hQ&jO,5:^O4pQ&jO1G0aO4xQ&kO1G/jOOQP,5;O,5;OO5|Q&kO1G0wOOQP-E8b-E8bO7eQ,UO'#DgO7oQ,UO1G/zO9RQ,UO'#ClP9]Q&jO'#DjO9bQ&kO1G/oO9iQ&jO'#DnOOQy1G/o1G/oO9wQ&jO1G/oOOQy1G0Q1G0QO;|Q'dO1G0QO<WQ'dO1G0QO>bQ'dO1G0QO>lQ'dO1G0QO@^Q'dO1G0QOAQQ&jO,5:nOOQy,5:p,5:pPOQy'#Ef'#EfPOQy,5:s,5:sPOQy,5:t,5:tOOQy1G0r1G0rOOQO7+&O7+&OOAYQ&jO7+&OOOQ%n,5;X,5;XOOQ%n1G/O1G/OOA_Q&rO1G/OOAdQ&rO1G/OOOQ%n1G/R1G/ROAoQ&jO,59lOAzQ'[O,59lOOQ%n1G/Y1G/YO2sQ)tO1G/YOBSQ&jO'#DXO2sQ)tO1G/YOBbQ&jO,59qOBpQ'[O,59qOBxQ'[O,59sOCQQ&jO1G/SOCYQ&jO1G/SO%^Q&kO1G/ZOC_Q'[O1G/ZOOQy7+${7+${OCjQ&jO,5:}OC{Q&kO1G0vOOQO-E8a-E8aOOQy7+%c7+%cODSQ&jO7+%cO%^Q&kO'#DsO%^Q&kO1G/xODXQ&jO1G/xOOQP'#E]'#E]O%^Q&kO7+%{P%^Q&kO'#EdO%^Q&kO7+%fOOQy7+%Z7+%ZOD^Q&jO7+%ZODcQ&kO,5:YODjQ&jO,5:YOOQO,5:Y,5:YOOQy1G0Y1G0YPOQy-E8d-E8dOOQO<<Ij<<IjOOQ%n7+$j7+$jODuQ&rO7+$jODzQ'[O1G/WOOQ%n1G/W1G/WOESQ'[O1G/WOOQ%n7+$t7+$tOEXQ&jO,59sO2sQ)tO7+$tOEdQ'[O1G/]OElQ'[O1G/_OOQ$Y1G/]1G/]OEtQ'[O1G/]OOQ%n1G/_1G/_OEyQ'[O1G/_O%^Q&kO7+$nOFOQ&jO7+$nOFTQ&jO7+$nOF]Q'[O7+$uOOQ$Y7+$u7+$uOFhQ'[O7+$uP%^Q&kO'#EcOOQy<<H}<<H}OFmQ&jO,5:_OFxQ&jO7+%dOGQQ&jO7+%dOOQy<<Ig<<IgOGVQ&jO<<IQOHrQ,UO1G0QOJWQ,UO1G0QOKlQ,UO1G0QOMQQ,UO1G0QOMXQ,UO1G0QOOQy<<Hu<<HuOM`Q&kO1G/tOOQO1G/t1G/tOOQ%n<<HU<<HUOOQ%n7+$r7+$rOMgQ'[O7+$rOOQ%n<<H`<<H`OOQ$Y7+$w7+$wOMlQ'[O7+$wOOQ%n7+$y7+$yOMqQ'[O7+$yOMvQ'[O<<HYONRQ&jO<<HYO%^Q&kO<<HYONWQ&jO<<HYOOQ$Y<<Ha<<HaON]Q'[O<<HaOOQP<<IO<<IOO%^Q&kO<<IOOOQyAN>lAN>lONbQ&jOAN>lONgQ&jOAN>lOOQ%n<<H^<<H^OOQ$Y<<Hc<<HcOOQ%n<<He<<HeOOQ$YAN=tAN=tONoQ'[OAN=tO%^Q&kOAN=tONtQ'[OAN=tO! PQ&jOAN=tOOQ$YAN={AN={O! UQ&jOAN>jOOQyG24WG24WO! ^Q&jOG24WOOQ$YG23`G23`O! cQ'[OG23`O! nQ'[OG23`O%^Q&kOG23`OOQPG24UG24UOOQyLD)rLD)rOOQ$YLD(zLD(zO! sQ'[OLD(zO! xQ'[OLD(zOOQ$Y!$'Lf!$'LfO!!TQ'[O!$'LfOOQ$Y!)9BQ!)9BQO!!YQ&kO'#DgO.iQ&kO'#DwO/uQ&kO'#DwO!!|Q&kO'#ClO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO!#sQ&kO1G0QO!$pQ&kO1G0QO!%mQ&kO1G0QO!(mQ&kO1G0QO!(tQ&kO1G0QO.iQ&kO7+%{O/uQ&kO7+%{O4pQ&jO1G0aO4pQ&jO1G0aO%^Q&kO,5:uO%^Q&kO,5:u",
stateData:
"!)T~O#_OSXPQYPQ~O^RO!UQO~OXWOYWO~OP[O^iOb]OdYOeYOgZO!RaO!TfO!UeO!atO#b^O#c^O#gnO#hoO~O_vOP#RX^#RX!U#RX~OPwO^xO!U{O~O^RO#[[X!T[X~OQ!VOR!POT!WO#]}O~OV!XO~OP[O^iOb]OdYOeYOgZOq!^O!RaO!UeO!atO#b^O#c^O#gnO#hoO~O!Q!ZO~P#tO#w!_O_!XX!Y!XX~O_!aO!Y!aO~OP[O^iOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~Or!cO!T#eX~P%^Oq!^O~PmO!T!fO~O#f!gO!R!ZX!w!ZX!y!ZX#g!ZX#i!ZX#j!ZX#k!ZX#l!ZX#m!ZX#n!ZX#o!ZX#p!ZX#q!ZX#r!ZX#s!ZX#t!ZX#u!ZX#v!ZX#w!ZXr!ZX^!ZX!T!ZX~O_!ZX!Y!ZX!a!`X!Q!ZX![!ZX#[!ZXS!ZXm!ZXx!ZX!c!ZXU!ZX~P&oO^!iO~O!a!lO~O!w!{O!y!|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO#t!sO#u!}O#v#OO#w`Xr`X^`X!T`X~O!R!zO_`X!Y`X!Q`X![`X#[`XS`Xm`Xx`X!c`XU`X~P)ZOPwO^xO!U#VO~O^RO!T#WO~O#]#YOQ#aPR#aPT#aPV#aP~OQ#[OR#UXT#UX#]#UX~OS#]Om#^O~P%^OR!POT#bO#]}O~OR!POT#fO#]}O~OQ#hOR!POT!WO#]}O~Om#jOq#iOx#kO~OR!POT!WOV#mO#]}O~Or#nO#w!_O!Q#dX~O!Q#pO~O^#rO~OP[O^&yOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~O!T#ea~P%^Or#vO!T#ea~P%^OP[O^#xOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~O#f!gO!a!`X~O!a#|O~O![$OO~P%^O^$XO~O![$]O#w!_O~O#w!_O^]i#[]i!T]i~OQ#[O~O^RO!T$^O~OS$aOm$bO~P%^OS$aOm$bO#w!_O~Om$eOq#iOu$fOx#kO~OR!POT$iO#]}O~Om$kOq#iOx#kOz$lO|$mO~O^$nO~Oq$oOx$pO~OR!POT!WOV$rO#]}O~O!Q#da~P%^Or$tO!Q#da~Ox$xO!Q$vO#w!_O~Or$zOs$yO~O!Y${O#w!_O~O#w!_OP!Wi^!Wib!Wid!Wie!Wig!Wir!Wi!R!Wi!T!Wi!U!Wi!a!Wi#b!Wi#c!Wi#g!Wi#h!Wi~O!T#ei~P%^O#f!gO!R!ZX!w!ZX!y!ZX#g!ZX#i!ZX#j!ZX#k!ZX#l!ZX#m!ZX#n!ZX#o!ZX#p!ZX#q!ZX#r!ZX#s!ZX#t!ZX#u!ZX#v!ZX#w!ZX~O!a!`X!i!ZX~P6TO!i%OO#w!_O~O!w!{O!y!|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO#t!sO#u!}O#v#OO#w`X~O!R!zO!i`X~P7wO#f!gO~O![%PO~P%^Or%RO!c%TO#w!_O![!bX~O![%PO~O#i!nO#j!nO#k!nO_!ni!R!ni!Y!ni!w!ni!y!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!nir!ni!Q!ni![!ni^!ni#[!niS!nim!nix!ni!c!niU!ni!T!ni~O#g!ni#l!ni~P9|O#g!oO#l!oO~P9|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO_!ni!R!ni!Y!ni!w!ni!y!ni#s!ni#t!ni#u!ni#v!ni#w!nir!ni!Q!ni![!ni^!ni#[!niS!nim!nix!ni!c!niU!ni!T!ni~O#q!ni#r!ni~P<bO#q!qO#r!qO~P<bO#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO!R!ni!w!ni!y!ni#t!ni#u!ni#v!ni#w!nir!ni^!ni!T!ni~O_!ni!Y!ni!Q!ni![!ni#[!niS!nim!nix!ni!c!niU!ni~P>vO!Q%UO#w!_O~O!T%WO~OS%XO~OS%XOm%YO#w!_O~Oq$oOu%ZOx$pO~OU%[Om%]O~Om%_Oq#iOx#kO|$mO~Oq$oOx$pOz%aO|%bO~OU%cOm%dO~OU%eOm%fO~Or%hOs%gO~O^%iO~OU%kOm%lO#w!_O~O#w!_Or#Va!Q#Va![#Va!c#Va~O!Q#di~P%^O!Q%nO~O^%qO~O![%yO~O![!ba~P%^Or%zO!c%{O![!ba~OS%|O~OU%}Om&OO~OU%}O~Oq$oOx$pO|%bO~OU&QOm&RO~OU&SOm&TO~OU&QO~OU&SO~O^&VO~Or&XOs&WO~OU&YOm&ZO#w!_O~OU&YO~O#w!_O!Q!ga!T!ga~O!Y&[O#w!_O~Os&]O~Ox$xO!T&^O!c&`O#w!_O~O#i!nO#j!nO#k!nO!R!ni!w!ni!y!ni#g!ni#l!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PGeO#g!oO#i!nO#j!nO#k!nO#l!oO!R!ni!w!ni!y!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PHyO#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO!R!ni!w!ni!y!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PJ_O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO!R!ni!i!ni!w!ni!y!ni#t!ni#u!ni#v!ni#w!ni~O#s!ni~PKsO#s!rO~PKsO![!bi~P%^OU&aO~OU&bO~OU&cO~OU&dOm&eO#w!_O~Os&fO~O^&hO~OU&iO~O!T&kO~Ox$xO!T&kO~OU&mO~OU&mOm&oO#w!_O~Os&pO~O!Y&qO#w!_O~O!T&rO~OU&sOm&tO#w!_O~OU&sO~OU&vO~OU&vOm&wO#w!_O~OU&xO~OP!ZXb!ZXd!ZXe!ZXg!ZX!U!ZX!a!ZX#b!ZX#c!ZX#h!ZX~P&oOP`Xb`Xd`Xe`Xg`X!R`X!U`X!a`X#b`X#c`X#h`X~P)ZOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PGeOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PHyOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PJ_O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qOP!ni^!nib!nid!nie!nig!nir!ni!R!ni!T!ni!U!ni!a!ni!w!ni!y!ni#b!ni#c!ni#h!ni#t!ni#u!ni#v!ni#w!ni~O#s!ni~P!&jO#s!rO~P!&jOb!w!y^X#j^~",
goto: "CW#fPPPPPPPP#gPPP#j#sPP#y%xP'YPP'YP(j*O%x*c+sP+s,R,aPPP,oP,R,uP-TP-ZP*c-i%xPP.yPP0Z1n1{P%xP%x2U3g3nP5RP%x5X6i6o5XP%x6x8Y8Y6x9n:W:m;P;`;l;u;zP;uP;u<O<O<T=O=o#s=x>O>S>[>v?Q?WP@kPPPP@qByPPCPCSRXPQVOQ#X{R$_#VXTOU{#V^bQde!c!d#v$}Q!YaQ#RtQ#SvQ#_!PQ#q!]Q#s!`Q#t!aQ#y!eS#}!l#|Q$W!zQ$c#^Q$q#kY$s#n$t%R%m%zQ%j$pQ%o$xQ%p$yU%r$|'`'aQ%s%OQ&U%gQ&g&WQ&j&]Q&n&fQ&u&pQ'b'dR'c'e#msQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#m]Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#l]Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eTxSzQ!O[f!T!Q!R!U!X#a#d#e#l$h$j%`R#Tw#m`Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'ei!T[!Q!R!U!X#a#d#e#l$h$j%`i!S[!Q!R!U!X#a#d#e#l$h$j%`i!Q[!Q!R!U!X#a#d#e#l$h$j%`Q#`!QR$d#ai!R[!Q!R!U!X#a#d#e#l$h$j%`Q#d!RR$j#eQ#c!RS$g#d#eS%^$h$jR&P%`#m_Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#mhQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eQVO#mhQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eSdQeS!bd!dV#u!c#v$}_cQde!c!d#v$}#ojQadekptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eZ!hi!i#x#{&y#llQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eR!kkQ$P!lR%Q#|#mmQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eQ!]aR!eeQ$w#qQ&_%sR&l&`#mrQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e!vpQadeptv!P!]!`!c!d!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'd'eb&z!a&z&}'P'R'T'V'X'`c&{!e&{'O'Q'S'U'W'Y'a[!tq$R$S$T$U$V[&}&|'Z'[']'^'_]'O#z%t%u%v%w%xY!uq$S$T$U$VY'P&|'[']'^'_Z'Q#z%u%v%w%xW!vq$T$U$VW'R&|']'^'_X'S#z%v%w%xU!wq$U$VU'T&|'^'_V'U#z%w%xS!xq$VS'V&|'_T'W#z%xQ!yqQ'X&|R'Y#zV#Qq#z&|T!{q#zV#Pq#z&|!quQadetv!P!]!`!a!c!d!e!l!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'`'a'd'e!O!`b!Y#R#S#_#q#s#}$W$c$q$s%j%o%p%s&U&g&j&n&u'b'cQ'd#tR'e#yQ$|#sQ'`'bR'a'cXSOU{#VTySzUUO{#VR|UQ!U[Q#a!QQ#e!R[#g!U#a#e#l$h%`Q#l!XQ$h#dR%`$jQ#o!YS$u#o%SR%S#}Q!ddR#w!d#lkQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eR!jkQzSR#Uz!hqQadetv!P!]!`!c!d!l!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'd'eU!mp&z&{S#z!e'aU$Q!t&}'OQ$R!uQ$S!vQ$T!wQ$U!xQ$V!yQ%t'QQ%u'SQ%v'UQ%w'WQ%x'YS&|!a'`Q'Z'PQ'['RQ']'TQ'^'VR'_'XQ#Z}R$`#YR![aTgQe",
nodeNames:
"⚠ QuotedTemplateStart QuotedTemplateEnd TemplateInterpolationStart TemplateInterpolationEnd TemplateDirectiveStart TemplateDirectiveEnd HeredocIdentifier Comment LineComment BlockComment ConfigFile Body Attribute Identifier AssignOp Expression LiteralValue NumericLit BoolLit true false NullLit null StringLit TemplateLiteral TemplateExpr QuotedTemplate TemplateInterpolation StripMarker TemplateDirective TemplateFor TemplateForStart for Comma in TemplateForEnd endfor TemplateIf TemplateIfIntro if TemplateElseIntro else TemplateIfEnd endif HeredocTemplate HeredocStart CollectionValue ] [ Tuple } { Object ObjectElem ObjectKey Colon VariableExpr ) FunctionCall FunctionNamespace NamespaceSeparator FunctionName ( FunctionArguments Ellipsis ForExpr ForTupleExpr ForIntro ForCond ForObjectExpr Arrow Operation UnaryOperation ArithOp LogicOp BinaryOperation ArithOp ArithOp CompareOp CompareOp LogicOp LogicOp Index NewIndex LegacyIndex GetAttr DerefOp Splat AttrSplat FullSplat Conditional ControlOp ControlOp Block BlockType BlockLabel",
maxTerm: 131,
context: trackTemplate,
nodeProps: [
["openedBy", 48, "[", 51, "{"],
["closedBy", 49, "]", 52, "}"],
],
skippedNodes: [0, 8, 9, 10],
repeatNodeCount: 7,
tokenData:
"-p~RvX^#ipq#iqr$^st$kuv%Svw%Xxy%dyz%iz{%n{|%s|}%x}!O%}!O!P&S!P!Q&z!Q!R(^!R![)l![!]*c!^!_*p!_!`+_!`!a+t!a!b,R!c!},W!}#O,l#P#Q-P#R#S,W#T#o,W#o#p-U#p#q-Z#q#r-f#r#s-k#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~#nY#_~X^#ipq#i#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~$cP#h~!_!`$f~$kO#r~~$pSX~OY$kZ;'S$k;'S;=`$|<%lO$k~%PP;=`<%l$k~%XO#k~~%[Pvw%_~%dO#s~~%iO!a~~%nO![~~%sO#i~~%xO#l~~%}Or~~&SO#g~~&XR!y~z{&b!O!P&g!Q![&r~&gO#u~~&jP!O!P&m~&rO!c~~&wP!w~!Q![&r~'PQ#j~z{'V!P!Q$k~'YTOz'Vz{'i{;'S'V;'S;=`(W<%lO'V~'lVOz'Vz{'i{!P'V!P!Q(R!Q;'S'V;'S;=`(W<%lO'V~(WOY~~(ZP;=`<%l'V~(cRb~!O!P(l!Q![)l#l#m)w~(oP!Q![(r~(wRb~!Q![(r!g!h)Q#X#Y)Q~)TR{|)^}!O)^!Q![)d~)aP!Q![)d~)iPb~!Q![)d~)qQb~!O!P(l!Q![)l~)zR!Q![*T!c!i*T#T#Z*T~*YRb~!Q![*T!c!i*T#T#Z*T~*hP!Y~![!]*k~*pO#f~~*uQ#o~!^!_*{!_!`+Y~+QP#b~}!O+T~+YO#c~~+_O#p~~+dQ_P!_!`+j!`!a+o~+oO#q~Q+tO!iQ~+yP#m~!_!`+|~,RO#n~~,WO#w~~,]T^~}!O,W!Q![,W!c!},W#R#S,W#T#o,W~,qP!R~z{,t~,wP#P#Q,z~-PO#v~~-UO!Q~~-ZO!U~~-^P#p#q-a~-fO#t~~-kO!T~~-pOm~",
tokenizers: [
scanQuotedTemplateStart,
scanQuotedTemplateEnd,
scanTemplateInterpolationStart,
scanTemplateInterpolationEnd,
scanTemplateDirectiveStart,
scanTemplateDirectiveEnd,
scanHeredocIdentifier,
scanTemplateLiteralChunk,
0,
1,
],
topRules: { ConfigFile: [0, 11] },
dynamicPrecedences: { "19": 1, "22": 1 },
specialized: [{ term: 14, get: (value: string) => spec_Identifier.get(value) ?? -1 }],
propSources: [hclHighlighting],
tokenPrec: 2481,
});
+10
View File
@@ -0,0 +1,10 @@
// Generated by lezer-generator, taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE).
// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here.
export const QuotedTemplateStart = 1;
export const QuotedTemplateEnd = 2;
export const TemplateInterpolationStart = 3;
export const TemplateInterpolationEnd = 4;
export const TemplateDirectiveStart = 5;
export const TemplateDirectiveEnd = 6;
export const HeredocIdentifier = 7;
export const templateLiteralChunk = 105;
+317
View File
@@ -0,0 +1,317 @@
// Taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE).
// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here.
import { ContextTracker, ExternalTokenizer, type InputStream } from "@lezer/lr";
import {
QuotedTemplateStart,
QuotedTemplateEnd,
TemplateInterpolationStart,
TemplateInterpolationEnd,
TemplateDirectiveStart,
TemplateDirectiveEnd,
HeredocIdentifier,
templateLiteralChunk,
} from "./terms.js";
const ContextType = {
QUOTED_TEMPLATE: 1,
TEMPLATE_INTERPOLATION: 2,
TEMPLATE_DIRECTIVE: 4,
HEREDOC_TEMPLATE: 8,
};
const hashStr = function (str: string) {
var hash = 0,
i,
chr;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
class Context {
readonly parent: Context | null;
readonly type: number;
readonly heredoc_identifier: string;
readonly hash: number;
constructor(parent: Context | null, type: number, heredoc_identifier: string) {
this.parent = parent;
this.type = type;
this.heredoc_identifier = heredoc_identifier;
this.hash =
(parent ? (parent.hash + parent.hash) << 8 : 0) +
(type << 4) +
(this.heredoc_identifier === "" ? 0 : hashStr(this.heredoc_identifier));
}
}
function readTokenText(input: InputStream, end: number): string {
let text = "";
for (let offset = 0; offset < end - input.pos; offset++) {
text += String.fromCharCode(input.peek(offset));
}
return text;
}
const topCtx = new Context(null, 0, "");
export const trackTemplate = new ContextTracker({
start: topCtx,
shift(context, term, stack, input) {
if (term === QuotedTemplateStart) {
return new Context(context, ContextType.QUOTED_TEMPLATE, "");
}
if (term === TemplateInterpolationStart) {
return new Context(context, ContextType.TEMPLATE_INTERPOLATION, "");
}
if (term === TemplateDirectiveStart) {
return new Context(context, ContextType.TEMPLATE_DIRECTIVE, "");
}
if (term === HeredocIdentifier && context.type !== ContextType.HEREDOC_TEMPLATE) {
const ctx = new Context(
context,
ContextType.HEREDOC_TEMPLATE,
readTokenText(input, stack.pos),
);
return ctx;
}
if (term === HeredocIdentifier && context.type === ContextType.HEREDOC_TEMPLATE) {
return context.parent ?? context;
}
switch (term) {
case QuotedTemplateEnd:
case TemplateInterpolationEnd:
case TemplateDirectiveEnd:
return context.parent ?? context;
}
return context;
},
hash(context) {
return context.hash;
},
});
const HEX_DIGIT = /[0-9a-fA-F]/;
function scanEscapeSequence(input: InputStream): void {
input.advance();
const kind = String.fromCharCode(input.next);
if (kind === "u" || kind === "U") {
const digits = kind === "u" ? 4 : 8;
for (let i = 0; i < digits; i++) {
input.advance();
if (!HEX_DIGIT.test(String.fromCharCode(input.next))) return;
}
} else if (!'"nrt\\'.includes(kind)) {
return;
}
input.advance();
input.acceptToken(templateLiteralChunk);
}
function scanHeredocBody(input: InputStream, expected: string): void {
let has_leading_whitespace_with_newline = false;
let has_leading_whitespace = false;
let next = String.fromCharCode(input.next);
while (/\s/.test(next)) {
has_leading_whitespace = true;
if (next === "\n") {
has_leading_whitespace_with_newline = true;
}
input.advance();
next = String.fromCharCode(input.next);
}
if (has_leading_whitespace) {
input.acceptToken(templateLiteralChunk);
}
if (!has_leading_whitespace_with_newline) {
return;
}
let advance = 0;
for (let i = 0; i < expected.length; i++) {
if (String.fromCharCode(input.next) === expected[i]) {
input.advance();
advance++;
} else {
if (advance) {
input.acceptToken(templateLiteralChunk);
}
return;
}
}
// check if the identifier is on a line of its own
let i = 0;
while (true) {
const ch = input.peek(i);
if (ch == -1 || String.fromCharCode(ch) === "\n") {
return input.acceptToken(HeredocIdentifier);
}
if (!/\s/.test(String.fromCharCode(ch))) {
return input.acceptToken(templateLiteralChunk);
}
i++;
}
}
// `$${` and `%%{` are escapes. The doubled marker counts only when `{` follows it, so in `$$${`
// the first `$` is literal text and the rest is the escape.
function scanTemplateMarker(input: InputStream, marker: string, startToken: number): void {
input.advance();
if (String.fromCharCode(input.next) === "{") {
input.advance();
return input.acceptToken(startToken);
}
if (String.fromCharCode(input.next) === marker && String.fromCharCode(input.peek(1)) === "{") {
input.advance();
input.advance();
}
input.acceptToken(templateLiteralChunk);
}
export const scanQuotedTemplateStart = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(QuotedTemplateStart) &&
!(stack.context && stack.context.type === ContextType.QUOTED_TEMPLATE) &&
String.fromCharCode(input.next) === '"'
) {
input.advance();
return input.acceptToken(QuotedTemplateStart);
}
},
{ contextual: true, fallback: true },
);
export const scanQuotedTemplateEnd = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(QuotedTemplateEnd) &&
stack.context &&
stack.context.type === ContextType.QUOTED_TEMPLATE &&
String.fromCharCode(input.next) === '"'
) {
input.advance();
return input.acceptToken(QuotedTemplateEnd);
}
},
{ contextual: true, fallback: true },
);
export const scanTemplateLiteralChunk = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(templateLiteralChunk) &&
stack.context &&
stack.context.type === ContextType.QUOTED_TEMPLATE
) {
if (String.fromCharCode(input.next) === "\\") {
return scanEscapeSequence(input);
}
}
if (
stack.canShift(templateLiteralChunk) &&
stack.context &&
(stack.context.type === ContextType.QUOTED_TEMPLATE ||
stack.context.type === ContextType.HEREDOC_TEMPLATE) &&
input.next !== -1
) {
input.advance();
return input.acceptToken(templateLiteralChunk);
}
},
{ contextual: true, fallback: true },
);
export const scanTemplateInterpolationStart = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(TemplateInterpolationStart) &&
stack.canShift(templateLiteralChunk) &&
!(stack.context && stack.context.type === ContextType.TEMPLATE_INTERPOLATION) &&
String.fromCharCode(input.next) === "$"
) {
return scanTemplateMarker(input, "$", TemplateInterpolationStart);
}
},
{ contextual: true, fallback: true },
);
export const scanTemplateInterpolationEnd = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(TemplateInterpolationEnd) &&
stack.context &&
stack.context.type === ContextType.TEMPLATE_INTERPOLATION &&
String.fromCharCode(input.next) === "}"
) {
input.advance();
return input.acceptToken(TemplateInterpolationEnd);
}
},
{ contextual: true, fallback: true },
);
export const scanTemplateDirectiveStart = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(TemplateDirectiveStart) &&
stack.canShift(templateLiteralChunk) &&
!(stack.context && stack.context.type === ContextType.TEMPLATE_DIRECTIVE) &&
String.fromCharCode(input.next) === "%"
) {
return scanTemplateMarker(input, "%", TemplateDirectiveStart);
}
},
{ contextual: true, fallback: true },
);
export const scanTemplateDirectiveEnd = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(TemplateDirectiveEnd) &&
stack.context &&
stack.context.type === ContextType.TEMPLATE_DIRECTIVE &&
String.fromCharCode(input.next) === "}"
) {
input.advance();
return input.acceptToken(TemplateDirectiveEnd);
}
},
{ contextual: true, fallback: true },
);
export const scanHeredocIdentifier = new ExternalTokenizer(
(input, stack) => {
if (
stack.canShift(HeredocIdentifier) &&
!(stack.context && stack.context.type === ContextType.HEREDOC_TEMPLATE) &&
input.next !== -1
) {
let next = String.fromCharCode(input.next);
while (/[\p{L}\p{N}_-]/u.test(next)) {
input.advance();
next = String.fromCharCode(input.next);
}
return input.acceptToken(HeredocIdentifier);
}
if (
stack.canShift(HeredocIdentifier) &&
stack.canShift(templateLiteralChunk) &&
stack.context &&
stack.context.type === ContextType.HEREDOC_TEMPLATE &&
input.next != -1
) {
return scanHeredocBody(input, stack.context.heredoc_identifier);
}
},
{ contextual: true, fallback: true },
);
+5
View File
@@ -20,6 +20,7 @@ import { csharpLanguage } from "./csharp/language.js";
import { astroParser } from "./astro/parser.js";
import { vueParser } from "./vue/parser.js";
import { nixLanguage } from "./nix/language.js";
import { hclLanguage } from "./hcl/language.js";
import { parser as svelteBaseParser } from "./svelte/parser.js";
import { configureNesting, defaultNesting } from "./svelte/nesting.js";
@@ -82,6 +83,10 @@ const languagesByExtension: Record<string, Language> = {
cs: csharpLanguage,
// Nix
nix: nixLanguage,
// Terraform / HCL
tf: hclLanguage,
tfvars: hclLanguage,
hcl: hclLanguage,
// Elixir
ex: language(elixirParser),
exs: language(elixirParser),