@rohal12/spindle-lsp 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +385 -12
- package/dist/bin.js.map +3 -3
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -6602,7 +6602,52 @@ var init_macro_registry = __esm({
|
|
|
6602
6602
|
});
|
|
6603
6603
|
|
|
6604
6604
|
// src/core/workspace/variable-tracker.ts
|
|
6605
|
-
|
|
6605
|
+
function findLiteralEnd(text, start) {
|
|
6606
|
+
const open = text[start];
|
|
6607
|
+
if (open === '"' || open === "'" || open === "`") {
|
|
6608
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
6609
|
+
const ch = text[i];
|
|
6610
|
+
if (ch === "\\") {
|
|
6611
|
+
i++;
|
|
6612
|
+
continue;
|
|
6613
|
+
}
|
|
6614
|
+
if (open === "`" && ch === "$" && text[i + 1] === "{") return -1;
|
|
6615
|
+
if (ch === open) return i;
|
|
6616
|
+
}
|
|
6617
|
+
return -1;
|
|
6618
|
+
}
|
|
6619
|
+
let depth = 0;
|
|
6620
|
+
for (let i = start; i < text.length; i++) {
|
|
6621
|
+
const ch = text[i];
|
|
6622
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
6623
|
+
const end = findLiteralEnd(text, i);
|
|
6624
|
+
if (end === -1) return -1;
|
|
6625
|
+
i = end;
|
|
6626
|
+
continue;
|
|
6627
|
+
}
|
|
6628
|
+
if (ch === "[" || ch === "{" || ch === "(") depth++;
|
|
6629
|
+
else if (ch === "]" || ch === "}" || ch === ")") {
|
|
6630
|
+
depth--;
|
|
6631
|
+
if (depth === 0) return i;
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
return -1;
|
|
6635
|
+
}
|
|
6636
|
+
function inferLiteralType(expr) {
|
|
6637
|
+
const e = expr.trim();
|
|
6638
|
+
if (e === "") return void 0;
|
|
6639
|
+
if (e === "true" || e === "false") return "boolean";
|
|
6640
|
+
if (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/.test(e)) return "number";
|
|
6641
|
+
const first = e[0];
|
|
6642
|
+
if (first !== "[" && first !== "{" && first !== '"' && first !== "'" && first !== "`") {
|
|
6643
|
+
return void 0;
|
|
6644
|
+
}
|
|
6645
|
+
if (findLiteralEnd(e, 0) !== e.length - 1) return void 0;
|
|
6646
|
+
if (first === "[") return "array";
|
|
6647
|
+
if (first === "{") return "object";
|
|
6648
|
+
return "string";
|
|
6649
|
+
}
|
|
6650
|
+
var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, ARRAY_MEMBERS, VariableTracker;
|
|
6606
6651
|
var init_variable_tracker = __esm({
|
|
6607
6652
|
"src/core/workspace/variable-tracker.ts"() {
|
|
6608
6653
|
"use strict";
|
|
@@ -6631,6 +6676,20 @@ var init_variable_tracker = __esm({
|
|
|
6631
6676
|
/'(?:\\.|[^'\\])*'/g
|
|
6632
6677
|
// single-quoted strings
|
|
6633
6678
|
];
|
|
6679
|
+
ARRAY_MEMBERS = /* @__PURE__ */ new Set([
|
|
6680
|
+
...Object.getOwnPropertyNames(Array.prototype),
|
|
6681
|
+
...Object.getOwnPropertyNames(Object.prototype),
|
|
6682
|
+
"at",
|
|
6683
|
+
"flat",
|
|
6684
|
+
"flatMap",
|
|
6685
|
+
"includes",
|
|
6686
|
+
"findLast",
|
|
6687
|
+
"findLastIndex",
|
|
6688
|
+
"toReversed",
|
|
6689
|
+
"toSorted",
|
|
6690
|
+
"toSpliced",
|
|
6691
|
+
"with"
|
|
6692
|
+
]);
|
|
6634
6693
|
VariableTracker = class {
|
|
6635
6694
|
declared = /* @__PURE__ */ new Map();
|
|
6636
6695
|
_hasStoryVariables = false;
|
|
@@ -6673,6 +6732,8 @@ var init_variable_tracker = __esm({
|
|
|
6673
6732
|
continue;
|
|
6674
6733
|
}
|
|
6675
6734
|
const decl = { name, sigil: "$" };
|
|
6735
|
+
const type = inferLiteralType(expr);
|
|
6736
|
+
if (type) decl.type = type;
|
|
6676
6737
|
if (expr.startsWith("{")) {
|
|
6677
6738
|
const fieldRegex = /(\w+)\s*:/g;
|
|
6678
6739
|
let fieldMatch;
|
|
@@ -6718,6 +6779,8 @@ var init_variable_tracker = __esm({
|
|
|
6718
6779
|
continue;
|
|
6719
6780
|
}
|
|
6720
6781
|
const decl = { name, sigil: "%" };
|
|
6782
|
+
const type = inferLiteralType(expr);
|
|
6783
|
+
if (type) decl.type = type;
|
|
6721
6784
|
if (expr.startsWith("{")) {
|
|
6722
6785
|
const fieldRegex = /(\w+)\s*:/g;
|
|
6723
6786
|
let fieldMatch;
|
|
@@ -6878,6 +6941,29 @@ var init_variable_tracker = __esm({
|
|
|
6878
6941
|
hasStoryTransients() {
|
|
6879
6942
|
return this._hasStoryTransients;
|
|
6880
6943
|
}
|
|
6944
|
+
/**
|
|
6945
|
+
* Get `$var.member` / `%var.member` accesses in a document where `var` is
|
|
6946
|
+
* declared with an array literal default and `member` is not a property
|
|
6947
|
+
* that JavaScript arrays have (so the access always yields undefined).
|
|
6948
|
+
*/
|
|
6949
|
+
getArrayMemberAccesses(uri) {
|
|
6950
|
+
const results = [];
|
|
6951
|
+
const sources = [
|
|
6952
|
+
["$", this.usagesByUri.get(uri), this.declared],
|
|
6953
|
+
["%", this.transientUsagesByUri.get(uri), this.declaredTransient]
|
|
6954
|
+
];
|
|
6955
|
+
for (const [sigil, usages, declared] of sources) {
|
|
6956
|
+
if (!usages) continue;
|
|
6957
|
+
for (const u of usages) {
|
|
6958
|
+
const member = u.fullName.split(".")[1];
|
|
6959
|
+
if (member === void 0) continue;
|
|
6960
|
+
if (declared.get(u.baseName)?.type !== "array") continue;
|
|
6961
|
+
if (ARRAY_MEMBERS.has(member)) continue;
|
|
6962
|
+
results.push({ sigil, name: u.baseName, member, range: u.range });
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
return results;
|
|
6966
|
+
}
|
|
6881
6967
|
/** Get variables declared with null values in StoryVariables. */
|
|
6882
6968
|
getNullDeclarations() {
|
|
6883
6969
|
return this._nullDeclarations;
|
|
@@ -6899,6 +6985,8 @@ var init_widget_registry = __esm({
|
|
|
6899
6985
|
paramRegex = /@([A-Za-z_$][\w$]*)/g;
|
|
6900
6986
|
WidgetRegistry = class {
|
|
6901
6987
|
widgets = /* @__PURE__ */ new Map();
|
|
6988
|
+
/** Per-URI set of lower-cased macro names opened in that document. */
|
|
6989
|
+
invokedByUri = /* @__PURE__ */ new Map();
|
|
6902
6990
|
/**
|
|
6903
6991
|
* Scan all passages for widget definitions.
|
|
6904
6992
|
* Only passages tagged `widget` are examined.
|
|
@@ -6960,6 +7048,30 @@ var init_widget_registry = __esm({
|
|
|
6960
7048
|
getAllWidgets() {
|
|
6961
7049
|
return Array.from(this.widgets.values());
|
|
6962
7050
|
}
|
|
7051
|
+
/**
|
|
7052
|
+
* Record the macro names opened in a document, replacing any previous
|
|
7053
|
+
* record for that URI. Spindle resolves widget names case-insensitively,
|
|
7054
|
+
* so names are stored lower-cased.
|
|
7055
|
+
*/
|
|
7056
|
+
recordInvocations(uri, macros) {
|
|
7057
|
+
const names = /* @__PURE__ */ new Set();
|
|
7058
|
+
for (const macro of macros) {
|
|
7059
|
+
if (macro.open) names.add(macro.name.toLowerCase());
|
|
7060
|
+
}
|
|
7061
|
+
this.invokedByUri.set(uri, names);
|
|
7062
|
+
}
|
|
7063
|
+
/** Forget all recorded invocations. */
|
|
7064
|
+
clearInvocations() {
|
|
7065
|
+
this.invokedByUri.clear();
|
|
7066
|
+
}
|
|
7067
|
+
/** Whether any recorded document invokes the widget `{name}`. */
|
|
7068
|
+
isInvoked(name) {
|
|
7069
|
+
const key = name.toLowerCase();
|
|
7070
|
+
for (const names of this.invokedByUri.values()) {
|
|
7071
|
+
if (names.has(key)) return true;
|
|
7072
|
+
}
|
|
7073
|
+
return false;
|
|
7074
|
+
}
|
|
6963
7075
|
};
|
|
6964
7076
|
}
|
|
6965
7077
|
});
|
|
@@ -7484,11 +7596,13 @@ var init_workspace_model = __esm({
|
|
|
7484
7596
|
this.variables.parseStoryTransients(content, contentStart);
|
|
7485
7597
|
}
|
|
7486
7598
|
}
|
|
7599
|
+
this.widgets.clearInvocations();
|
|
7487
7600
|
for (const uri of this.documents.getUris()) {
|
|
7488
7601
|
const text = this.documents.getText(uri);
|
|
7489
7602
|
if (text) {
|
|
7490
7603
|
const macros = parseMacros(text);
|
|
7491
7604
|
this.variables.scanDocument(uri, text, macros);
|
|
7605
|
+
this.widgets.recordInvocations(uri, macros);
|
|
7492
7606
|
}
|
|
7493
7607
|
}
|
|
7494
7608
|
const allPassages = this.passages.getAllPassages();
|
|
@@ -7533,8 +7647,12 @@ var init_diagnostic_codes = __esm({
|
|
|
7533
7647
|
NoStoryVariables: "SP202",
|
|
7534
7648
|
UndeclaredTransient: "SP203",
|
|
7535
7649
|
NullVariableValue: "SP204",
|
|
7650
|
+
TemporaryAssignedInLoop: "SP205",
|
|
7651
|
+
ArrayMemberAccess: "SP206",
|
|
7536
7652
|
BrokenPassageLink: "SP300",
|
|
7537
7653
|
WidgetArgCountMismatch: "SP301",
|
|
7654
|
+
IncludeWidgetPassage: "SP302",
|
|
7655
|
+
UnusedWidget: "SP303",
|
|
7538
7656
|
DeadEndPassage: "SP400",
|
|
7539
7657
|
UnreachablePassage: "SP401",
|
|
7540
7658
|
LineTooLong: "SP500"
|
|
@@ -7556,8 +7674,12 @@ var init_diagnostic_codes = __esm({
|
|
|
7556
7674
|
SP202: "info",
|
|
7557
7675
|
SP203: "warning",
|
|
7558
7676
|
SP204: "error",
|
|
7677
|
+
SP205: "warning",
|
|
7678
|
+
SP206: "warning",
|
|
7559
7679
|
SP300: "warning",
|
|
7560
7680
|
SP301: "warning",
|
|
7681
|
+
SP302: "warning",
|
|
7682
|
+
SP303: "hint",
|
|
7561
7683
|
SP400: "hint",
|
|
7562
7684
|
SP401: "hint",
|
|
7563
7685
|
SP500: "warning"
|
|
@@ -8471,6 +8593,22 @@ function computeDiagnostics(uri, workspace, options) {
|
|
|
8471
8593
|
validateWidgetInvocations(macros, workspace, diagnostics);
|
|
8472
8594
|
} catch {
|
|
8473
8595
|
}
|
|
8596
|
+
try {
|
|
8597
|
+
validateArrayMemberAccess(uri, workspace, diagnostics);
|
|
8598
|
+
} catch {
|
|
8599
|
+
}
|
|
8600
|
+
try {
|
|
8601
|
+
validateLoopTemporaries(uri, text, macros, workspace, diagnostics);
|
|
8602
|
+
} catch {
|
|
8603
|
+
}
|
|
8604
|
+
try {
|
|
8605
|
+
validateWidgetIncludes(text, macros, workspace, diagnostics);
|
|
8606
|
+
} catch {
|
|
8607
|
+
}
|
|
8608
|
+
try {
|
|
8609
|
+
validateUnusedWidgets(uri, workspace, diagnostics);
|
|
8610
|
+
} catch {
|
|
8611
|
+
}
|
|
8474
8612
|
if (options?.maxLineLength) {
|
|
8475
8613
|
try {
|
|
8476
8614
|
validateLineLength(text, passages, options.maxLineLength, diagnostics);
|
|
@@ -8725,6 +8863,226 @@ function validateWidgetInvocations(macros, workspace, diagnostics) {
|
|
|
8725
8863
|
}
|
|
8726
8864
|
}
|
|
8727
8865
|
}
|
|
8866
|
+
function validateArrayMemberAccess(uri, workspace, diagnostics) {
|
|
8867
|
+
for (const a of workspace.variables.getArrayMemberAccesses(uri)) {
|
|
8868
|
+
const passage = a.sigil === "%" ? "StoryTransients" : "StoryVariables";
|
|
8869
|
+
const ref = `${a.sigil}${a.name}`;
|
|
8870
|
+
diagnostics.push(makeDiag(
|
|
8871
|
+
a.range,
|
|
8872
|
+
DiagnosticCode.ArrayMemberAccess,
|
|
8873
|
+
`'${ref}' is declared as an array in ${passage}, and arrays have no '${a.member}' property: '${ref}.${a.member}' is always undefined. If you meant to test membership, use ${ref}.includes("${a.member}").`
|
|
8874
|
+
));
|
|
8875
|
+
}
|
|
8876
|
+
}
|
|
8877
|
+
function temporaryRefRegex(name) {
|
|
8878
|
+
const ident = name ? escapeRegex(name) : "[A-Za-z_$][\\w$]*";
|
|
8879
|
+
return new RegExp(`(?<![.\\w$@%])_(${ident})(?![\\w$])`, "g");
|
|
8880
|
+
}
|
|
8881
|
+
function escapeRegex(s) {
|
|
8882
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8883
|
+
}
|
|
8884
|
+
function maskStrings(code) {
|
|
8885
|
+
let out = "";
|
|
8886
|
+
let i = 0;
|
|
8887
|
+
while (i < code.length) {
|
|
8888
|
+
const ch = code[i];
|
|
8889
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
8890
|
+
out += ch;
|
|
8891
|
+
i++;
|
|
8892
|
+
while (i < code.length && code[i] !== ch) {
|
|
8893
|
+
if (code[i] === "\\" && i + 1 < code.length) {
|
|
8894
|
+
out += " ";
|
|
8895
|
+
i += 2;
|
|
8896
|
+
} else if (ch === "`" && code[i] === "$" && code[i + 1] === "{") {
|
|
8897
|
+
const end = interpolationEnd(code, i + 2);
|
|
8898
|
+
out += maskStrings(code.slice(i, end));
|
|
8899
|
+
i = end;
|
|
8900
|
+
} else {
|
|
8901
|
+
out += code[i] === "\n" ? "\n" : " ";
|
|
8902
|
+
i++;
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
if (i < code.length) {
|
|
8906
|
+
out += ch;
|
|
8907
|
+
i++;
|
|
8908
|
+
}
|
|
8909
|
+
continue;
|
|
8910
|
+
}
|
|
8911
|
+
out += ch;
|
|
8912
|
+
i++;
|
|
8913
|
+
}
|
|
8914
|
+
return out;
|
|
8915
|
+
}
|
|
8916
|
+
function interpolationEnd(code, from) {
|
|
8917
|
+
let depth = 1;
|
|
8918
|
+
for (let i = from; i < code.length; i++) {
|
|
8919
|
+
if (code[i] === "{") depth++;
|
|
8920
|
+
else if (code[i] === "}" && --depth === 0) return i + 1;
|
|
8921
|
+
}
|
|
8922
|
+
return code.length;
|
|
8923
|
+
}
|
|
8924
|
+
function computedTemporaryTarget(rawArgs) {
|
|
8925
|
+
const lead = rawArgs.length - rawArgs.trimStart().length;
|
|
8926
|
+
const trimmed = rawArgs.trim();
|
|
8927
|
+
let depth = 0;
|
|
8928
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
8929
|
+
const ch = trimmed[i];
|
|
8930
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
8931
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
8932
|
+
else if (ch === "=" && depth === 0) {
|
|
8933
|
+
if (trimmed[i + 1] === "=") {
|
|
8934
|
+
i++;
|
|
8935
|
+
continue;
|
|
8936
|
+
}
|
|
8937
|
+
if (i > 0 && trimmed[i - 1] === "!") continue;
|
|
8938
|
+
const target = trimmed.slice(0, i).trim();
|
|
8939
|
+
const m = /^_(\w+)$/.exec(target);
|
|
8940
|
+
return m ? [{ name: m[1], offset: lead }] : [];
|
|
8941
|
+
}
|
|
8942
|
+
}
|
|
8943
|
+
return [];
|
|
8944
|
+
}
|
|
8945
|
+
function setTemporaryTargets(rawArgs) {
|
|
8946
|
+
const code = maskStrings(rawArgs);
|
|
8947
|
+
const targets = [];
|
|
8948
|
+
const assignRe = /(?<![.\w$@%])_([A-Za-z_$][\w$]*)\s*=(?![=>])/g;
|
|
8949
|
+
let m;
|
|
8950
|
+
while ((m = assignRe.exec(code)) !== null) {
|
|
8951
|
+
const name = m[1];
|
|
8952
|
+
const rhs = code.slice(m.index + m[0].length, statementEnd(code, m.index + m[0].length));
|
|
8953
|
+
if (temporaryRefRegex(name).test(rhs)) continue;
|
|
8954
|
+
targets.push({ name, offset: m.index });
|
|
8955
|
+
}
|
|
8956
|
+
return targets;
|
|
8957
|
+
}
|
|
8958
|
+
function statementEnd(code, from) {
|
|
8959
|
+
let depth = 0;
|
|
8960
|
+
for (let i = from; i < code.length; i++) {
|
|
8961
|
+
const ch = code[i];
|
|
8962
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
8963
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
8964
|
+
else if ((ch === ";" || ch === ",") && depth <= 0) return i;
|
|
8965
|
+
}
|
|
8966
|
+
return code.length;
|
|
8967
|
+
}
|
|
8968
|
+
function enclosingLoop(macros, index, uri, workspace) {
|
|
8969
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
8970
|
+
const candidate = macros[j];
|
|
8971
|
+
if (!candidate.open || candidate.pair === -1 || candidate.pair < index) continue;
|
|
8972
|
+
const name = candidate.name.toLowerCase();
|
|
8973
|
+
if (DEFERRED_CONTAINERS.has(name)) return -1;
|
|
8974
|
+
if (name !== "for") continue;
|
|
8975
|
+
const loopPassage = workspace.passages.getPassageAt(uri, candidate.range.start.line);
|
|
8976
|
+
const macroPassage = workspace.passages.getPassageAt(uri, macros[index].range.start.line);
|
|
8977
|
+
return loopPassage === macroPassage ? j : -1;
|
|
8978
|
+
}
|
|
8979
|
+
return -1;
|
|
8980
|
+
}
|
|
8981
|
+
function positionToOffset(pos, lineStarts) {
|
|
8982
|
+
return (lineStarts[pos.line] ?? 0) + pos.character;
|
|
8983
|
+
}
|
|
8984
|
+
function sourceArgs(macro, text, lineStarts) {
|
|
8985
|
+
const end = positionToOffset(macro.range.end, lineStarts) - 1;
|
|
8986
|
+
const start = end - (macro.rawArgs?.length ?? 0);
|
|
8987
|
+
return { args: text.slice(start, end), start };
|
|
8988
|
+
}
|
|
8989
|
+
function validateLoopTemporaries(uri, text, macros, workspace, diagnostics) {
|
|
8990
|
+
const lineStarts = buildLineStarts(text);
|
|
8991
|
+
for (let k = 0; k < macros.length; k++) {
|
|
8992
|
+
const macro = macros[k];
|
|
8993
|
+
if (!macro.open || !macro.rawArgs) continue;
|
|
8994
|
+
const name = macro.name.toLowerCase();
|
|
8995
|
+
if (name !== "computed" && name !== "set") continue;
|
|
8996
|
+
const { args: args2, start: argsStart } = sourceArgs(macro, text, lineStarts);
|
|
8997
|
+
const targets = name === "computed" ? computedTemporaryTarget(args2) : setTemporaryTargets(args2);
|
|
8998
|
+
if (targets.length === 0) continue;
|
|
8999
|
+
const loopIndex = enclosingLoop(macros, k, uri, workspace);
|
|
9000
|
+
if (loopIndex === -1) continue;
|
|
9001
|
+
const loop = macros[loopIndex];
|
|
9002
|
+
const bodyStart = positionToOffset(loop.range.end, lineStarts);
|
|
9003
|
+
const bodyEnd = positionToOffset(macros[loop.pair].range.start, lineStarts);
|
|
9004
|
+
const macroStart = positionToOffset(macro.range.start, lineStarts);
|
|
9005
|
+
const macroEnd = positionToOffset(macro.range.end, lineStarts);
|
|
9006
|
+
const body = text.slice(bodyStart, bodyEnd);
|
|
9007
|
+
for (const target of targets) {
|
|
9008
|
+
if (!isReadInBody(body, bodyStart, target.name, macroStart, macroEnd)) continue;
|
|
9009
|
+
const start = argsStart + target.offset;
|
|
9010
|
+
diagnostics.push(makeDiag(
|
|
9011
|
+
{
|
|
9012
|
+
start: offsetToPosition(start, lineStarts),
|
|
9013
|
+
end: offsetToPosition(start + 1 + target.name.length, lineStarts)
|
|
9014
|
+
},
|
|
9015
|
+
DiagnosticCode.TemporaryAssignedInLoop,
|
|
9016
|
+
`Temporary '_${target.name}' is assigned inside {${loop.name}} and read in the loop body. Temporaries are shared by every iteration, so all iterations end up seeing the value assigned by the last one. Use the iteration-local '@${target.name}' instead, or assign it once before the loop if the value does not depend on the iteration.`
|
|
9017
|
+
));
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
}
|
|
9021
|
+
function isReadInBody(body, bodyOffset, name, macroStart, macroEnd) {
|
|
9022
|
+
const re = temporaryRefRegex(name);
|
|
9023
|
+
let m;
|
|
9024
|
+
while ((m = re.exec(body)) !== null) {
|
|
9025
|
+
const offset = bodyOffset + m.index;
|
|
9026
|
+
if (offset >= macroStart && offset < macroEnd) continue;
|
|
9027
|
+
const after = body.slice(m.index + m[0].length);
|
|
9028
|
+
if (/^\s*=(?![=>])/.test(after)) continue;
|
|
9029
|
+
return true;
|
|
9030
|
+
}
|
|
9031
|
+
return false;
|
|
9032
|
+
}
|
|
9033
|
+
function resolveIncludeTarget(rawArgs) {
|
|
9034
|
+
const expr = rawArgs.replace(/\binline\b/, "").trim();
|
|
9035
|
+
if (expr === "") return null;
|
|
9036
|
+
const literal2 = /^(["'`])((?:\\.|(?!\1)[^\\])*)\1$/s.exec(expr);
|
|
9037
|
+
if (literal2) {
|
|
9038
|
+
if (literal2[1] === "`" && literal2[2].includes("${")) return null;
|
|
9039
|
+
return literal2[2].replace(/\\(.)/g, "$1");
|
|
9040
|
+
}
|
|
9041
|
+
if (/[$@%"'`(]/.test(expr) || temporaryRefRegex().test(expr)) return null;
|
|
9042
|
+
if (EXPRESSION_BUILTINS.has(expr)) return null;
|
|
9043
|
+
return expr;
|
|
9044
|
+
}
|
|
9045
|
+
function widgetDefinitionNames(content) {
|
|
9046
|
+
const names = [];
|
|
9047
|
+
const re = /\{widget\s+(["']?)([^\s"'}]+)\1/gi;
|
|
9048
|
+
let m;
|
|
9049
|
+
while ((m = re.exec(content)) !== null) names.push(m[2]);
|
|
9050
|
+
return names;
|
|
9051
|
+
}
|
|
9052
|
+
function passageContent(passage, workspace) {
|
|
9053
|
+
const text = workspace.documents.getText(passage.uri) ?? "";
|
|
9054
|
+
return text.split("\n").slice(passage.range.start.line + 1, passage.range.end.line + 1).join("\n");
|
|
9055
|
+
}
|
|
9056
|
+
function validateWidgetIncludes(text, macros, workspace, diagnostics) {
|
|
9057
|
+
let lineStarts = null;
|
|
9058
|
+
for (const macro of macros) {
|
|
9059
|
+
if (!macro.open || !macro.rawArgs || macro.name.toLowerCase() !== "include") continue;
|
|
9060
|
+
lineStarts ??= buildLineStarts(text);
|
|
9061
|
+
const target = resolveIncludeTarget(sourceArgs(macro, text, lineStarts).args);
|
|
9062
|
+
if (target === null) continue;
|
|
9063
|
+
const passage = workspace.passages.getPassage(target);
|
|
9064
|
+
if (!passage?.tags?.includes("widget")) continue;
|
|
9065
|
+
const widgets = widgetDefinitionNames(passageContent(passage, workspace));
|
|
9066
|
+
if (widgets.length === 0) continue;
|
|
9067
|
+
const list = widgets.map((w) => `{${w}}`).join(", ");
|
|
9068
|
+
diagnostics.push(makeDiag(
|
|
9069
|
+
macro.range,
|
|
9070
|
+
DiagnosticCode.IncludeWidgetPassage,
|
|
9071
|
+
`{include}: passage "${target}" is tagged [widget] and defines ${list}. Including it does not invoke ${widgets.length === 1 ? "it" : "them"}: {widget} definitions render nothing. Invoke the widget instead, e.g. {${widgets[0]}}.`
|
|
9072
|
+
));
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
function validateUnusedWidgets(uri, workspace, diagnostics) {
|
|
9076
|
+
for (const widget of workspace.widgets.getAllWidgets()) {
|
|
9077
|
+
if (widget.uri !== uri) continue;
|
|
9078
|
+
if (workspace.widgets.isInvoked(widget.name)) continue;
|
|
9079
|
+
diagnostics.push(makeDiag(
|
|
9080
|
+
widget.range,
|
|
9081
|
+
DiagnosticCode.UnusedWidget,
|
|
9082
|
+
`Widget "${widget.name}" is defined but never invoked in the workspace`
|
|
9083
|
+
));
|
|
9084
|
+
}
|
|
9085
|
+
}
|
|
8728
9086
|
function validateLineLength(text, passages, maxLength, diagnostics) {
|
|
8729
9087
|
const excludedTags = /* @__PURE__ */ new Set(["script", "stylesheet"]);
|
|
8730
9088
|
const excludedPassages = new Set(passages.filter((p) => p.tags?.some((t) => excludedTags.has(t))).map((p) => p.name));
|
|
@@ -8772,7 +9130,7 @@ function toLspDiagnostic(d) {
|
|
|
8772
9130
|
message: d.message
|
|
8773
9131
|
};
|
|
8774
9132
|
}
|
|
8775
|
-
var diagnosticsPlugin;
|
|
9133
|
+
var DEFERRED_CONTAINERS, EXPRESSION_BUILTINS, diagnosticsPlugin;
|
|
8776
9134
|
var init_diagnostics = __esm({
|
|
8777
9135
|
"src/plugins/diagnostics.ts"() {
|
|
8778
9136
|
"use strict";
|
|
@@ -8781,6 +9139,21 @@ var init_diagnostics = __esm({
|
|
|
8781
9139
|
init_argument_lexer();
|
|
8782
9140
|
init_parameter_validator();
|
|
8783
9141
|
init_link_parser();
|
|
9142
|
+
DEFERRED_CONTAINERS = /* @__PURE__ */ new Set(["link", "button"]);
|
|
9143
|
+
EXPRESSION_BUILTINS = /* @__PURE__ */ new Set([
|
|
9144
|
+
"currentPassage",
|
|
9145
|
+
"previousPassage",
|
|
9146
|
+
"visited",
|
|
9147
|
+
"hasVisited",
|
|
9148
|
+
"hasVisitedAny",
|
|
9149
|
+
"hasVisitedAll",
|
|
9150
|
+
"rendered",
|
|
9151
|
+
"hasRendered",
|
|
9152
|
+
"hasRenderedAny",
|
|
9153
|
+
"hasRenderedAll",
|
|
9154
|
+
"random",
|
|
9155
|
+
"randomInt"
|
|
9156
|
+
]);
|
|
8784
9157
|
diagnosticsPlugin = {
|
|
8785
9158
|
id: "diagnostics",
|
|
8786
9159
|
capabilities: {},
|
|
@@ -21275,7 +21648,7 @@ __export(util_exports, {
|
|
|
21275
21648
|
createTransparentProxy: () => createTransparentProxy,
|
|
21276
21649
|
defineLazy: () => defineLazy,
|
|
21277
21650
|
esc: () => esc,
|
|
21278
|
-
escapeRegex: () =>
|
|
21651
|
+
escapeRegex: () => escapeRegex2,
|
|
21279
21652
|
extend: () => extend,
|
|
21280
21653
|
finalizeIssue: () => finalizeIssue,
|
|
21281
21654
|
floatSafeRemainder: () => floatSafeRemainder2,
|
|
@@ -21487,7 +21860,7 @@ function numKeys(data) {
|
|
|
21487
21860
|
}
|
|
21488
21861
|
return keyCount;
|
|
21489
21862
|
}
|
|
21490
|
-
function
|
|
21863
|
+
function escapeRegex2(str) {
|
|
21491
21864
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21492
21865
|
}
|
|
21493
21866
|
function clone(inst, def, params) {
|
|
@@ -22293,7 +22666,7 @@ var init_regexes = __esm({
|
|
|
22293
22666
|
ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
|
|
22294
22667
|
ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
|
22295
22668
|
mac = (delimiter) => {
|
|
22296
|
-
const escapedDelim =
|
|
22669
|
+
const escapedDelim = escapeRegex2(delimiter ?? ":");
|
|
22297
22670
|
return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
|
|
22298
22671
|
};
|
|
22299
22672
|
cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
|
@@ -22783,7 +23156,7 @@ var init_checks = __esm({
|
|
|
22783
23156
|
});
|
|
22784
23157
|
$ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
|
|
22785
23158
|
$ZodCheck.init(inst, def);
|
|
22786
|
-
const escapedRegex =
|
|
23159
|
+
const escapedRegex = escapeRegex2(def.includes);
|
|
22787
23160
|
const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
|
|
22788
23161
|
def.pattern = pattern;
|
|
22789
23162
|
inst._zod.onattach.push((inst2) => {
|
|
@@ -22807,7 +23180,7 @@ var init_checks = __esm({
|
|
|
22807
23180
|
});
|
|
22808
23181
|
$ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
|
|
22809
23182
|
$ZodCheck.init(inst, def);
|
|
22810
|
-
const pattern = new RegExp(`^${
|
|
23183
|
+
const pattern = new RegExp(`^${escapeRegex2(def.prefix)}.*`);
|
|
22811
23184
|
def.pattern ?? (def.pattern = pattern);
|
|
22812
23185
|
inst._zod.onattach.push((inst2) => {
|
|
22813
23186
|
const bag = inst2._zod.bag;
|
|
@@ -22830,7 +23203,7 @@ var init_checks = __esm({
|
|
|
22830
23203
|
});
|
|
22831
23204
|
$ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
|
|
22832
23205
|
$ZodCheck.init(inst, def);
|
|
22833
|
-
const pattern = new RegExp(`.*${
|
|
23206
|
+
const pattern = new RegExp(`.*${escapeRegex2(def.suffix)}$`);
|
|
22834
23207
|
def.pattern ?? (def.pattern = pattern);
|
|
22835
23208
|
inst._zod.onattach.push((inst2) => {
|
|
22836
23209
|
const bag = inst2._zod.bag;
|
|
@@ -24468,7 +24841,7 @@ var init_schemas = __esm({
|
|
|
24468
24841
|
const values = getEnumValues(def.entries);
|
|
24469
24842
|
const valuesSet = new Set(values);
|
|
24470
24843
|
inst._zod.values = valuesSet;
|
|
24471
|
-
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ?
|
|
24844
|
+
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex2(o) : o.toString()).join("|")})$`);
|
|
24472
24845
|
inst._zod.parse = (payload, _ctx) => {
|
|
24473
24846
|
const input = payload.value;
|
|
24474
24847
|
if (valuesSet.has(input)) {
|
|
@@ -24490,7 +24863,7 @@ var init_schemas = __esm({
|
|
|
24490
24863
|
}
|
|
24491
24864
|
const values = new Set(def.values);
|
|
24492
24865
|
inst._zod.values = values;
|
|
24493
|
-
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ?
|
|
24866
|
+
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex2(o) : o ? escapeRegex2(o.toString()) : String(o)).join("|")})$`);
|
|
24494
24867
|
inst._zod.parse = (payload, _ctx) => {
|
|
24495
24868
|
const input = payload.value;
|
|
24496
24869
|
if (values.has(input)) {
|
|
@@ -24785,7 +25158,7 @@ var init_schemas = __esm({
|
|
|
24785
25158
|
const end = source.endsWith("$") ? source.length - 1 : source.length;
|
|
24786
25159
|
regexParts.push(source.slice(start, end));
|
|
24787
25160
|
} else if (part === null || primitiveTypes.has(typeof part)) {
|
|
24788
|
-
regexParts.push(
|
|
25161
|
+
regexParts.push(escapeRegex2(`${part}`));
|
|
24789
25162
|
} else {
|
|
24790
25163
|
throw new Error(`Invalid template literal part: ${part}`);
|
|
24791
25164
|
}
|
|
@@ -59858,7 +60231,7 @@ var init_server4 = __esm({
|
|
|
59858
60231
|
|
|
59859
60232
|
// src/bin.ts
|
|
59860
60233
|
var args = process.argv.slice(2);
|
|
59861
|
-
var VERSION = "0.
|
|
60234
|
+
var VERSION = "0.9.0";
|
|
59862
60235
|
var HELP = `spindle-lsp v${VERSION}
|
|
59863
60236
|
Language Server Protocol server for Spindle story format
|
|
59864
60237
|
|