@rohal12/spindle-lsp 0.8.3 → 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 CHANGED
@@ -6602,7 +6602,52 @@ var init_macro_registry = __esm({
6602
6602
  });
6603
6603
 
6604
6604
  // src/core/workspace/variable-tracker.ts
6605
- var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, VariableTracker;
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: () => 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 escapeRegex(str) {
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 = escapeRegex(delimiter ?? ":");
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 = escapeRegex(def.includes);
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(`^${escapeRegex(def.prefix)}.*`);
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(`.*${escapeRegex(def.suffix)}$`);
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" ? escapeRegex(o) : o.toString()).join("|")})$`);
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" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
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(escapeRegex(`${part}`));
25161
+ regexParts.push(escapeRegex2(`${part}`));
24789
25162
  } else {
24790
25163
  throw new Error(`Invalid template literal part: ${part}`);
24791
25164
  }
@@ -59707,7 +60080,7 @@ function startServer(_args) {
59707
60080
  connection.onInitialize((params) => {
59708
60081
  console.error("[spindle-lsp] onInitialize called");
59709
60082
  const initOptions = params.initializationOptions ?? {};
59710
- workspaceRoot = initOptions.workspaceRoot ?? (params.workspaceFolders?.[0]?.uri ? uriToFsPath(params.workspaceFolders[0].uri) : void 0);
60083
+ workspaceRoot = initOptions.workspaceRoot ?? (params.workspaceFolders?.[0]?.uri ? uriToFsPath(params.workspaceFolders[0].uri) : void 0) ?? (params.rootUri ? uriToFsPath(params.rootUri) : void 0);
59711
60084
  const projectConfig = workspaceRoot ? loadConfigFromDisk(workspaceRoot) : { macros: {} };
59712
60085
  const config2 = {
59713
60086
  disabledPlugins: initOptions.disabledPlugins,
@@ -59783,7 +60156,13 @@ function startServer(_args) {
59783
60156
  connection.onDidCloseTextDocument(({ textDocument }) => {
59784
60157
  documents.delete(textDocument.uri);
59785
60158
  if (workspace) {
59786
- workspace.documents.close(textDocument.uri);
60159
+ try {
60160
+ const fsPath = uriToFsPath(textDocument.uri);
60161
+ const text = readFileSync7(fsPath, "utf-8");
60162
+ workspace.documents.update(textDocument.uri, text);
60163
+ } catch {
60164
+ workspace.documents.close(textDocument.uri);
60165
+ }
59787
60166
  }
59788
60167
  });
59789
60168
  connection.onDidChangeWatchedFiles(({ changes }) => {
@@ -59852,7 +60231,7 @@ var init_server4 = __esm({
59852
60231
 
59853
60232
  // src/bin.ts
59854
60233
  var args = process.argv.slice(2);
59855
- var VERSION = "0.8.3";
60234
+ var VERSION = "0.9.0";
59856
60235
  var HELP = `spindle-lsp v${VERSION}
59857
60236
  Language Server Protocol server for Spindle story format
59858
60237