@rohal12/spindle-lsp 0.8.4 → 0.9.1

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.
Files changed (3) hide show
  1. package/dist/bin.js +1445 -420
  2. package/dist/bin.js.map +4 -4
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -6303,11 +6303,15 @@ function parsePassageHeader(line, lineNumber) {
6303
6303
  nameRange
6304
6304
  };
6305
6305
  }
6306
- var passageHeaderRegex;
6306
+ function isScriptOrStylesheetPassage(passage) {
6307
+ return passage.tags?.some((t) => CODE_PASSAGE_TAGS.has(t)) ?? false;
6308
+ }
6309
+ var passageHeaderRegex, CODE_PASSAGE_TAGS;
6307
6310
  var init_passage_parser = __esm({
6308
6311
  "src/core/parsing/passage-parser.ts"() {
6309
6312
  "use strict";
6310
6313
  passageHeaderRegex = /^(::\s+)(.*?)(\[.*?\]\s*)?(\{.*?\}\s*)?\r?$/;
6314
+ CODE_PASSAGE_TAGS = /* @__PURE__ */ new Set(["script", "stylesheet"]);
6311
6315
  }
6312
6316
  });
6313
6317
 
@@ -6339,6 +6343,7 @@ var init_passage_index = __esm({
6339
6343
  // will be extended below
6340
6344
  },
6341
6345
  headerEnd: header.headerRange,
6346
+ nameRange: header.nameRange,
6342
6347
  uri,
6343
6348
  tags: header.tags.length > 0 ? header.tags : void 0,
6344
6349
  meta: header.meta
@@ -6443,6 +6448,12 @@ var init_macro_registry = __esm({
6443
6448
  "use strict";
6444
6449
  MacroRegistry = class {
6445
6450
  macros = /* @__PURE__ */ new Map();
6451
+ /** Entries as they were before the current user config was applied (undefined = absent). */
6452
+ configSnapshot = /* @__PURE__ */ new Map();
6453
+ /** Tier 3: macros discovered from Story.defineMacro() calls, keyed by lowercase name. */
6454
+ discovered = /* @__PURE__ */ new Map();
6455
+ /** Tier 4: user config entries, kept so they can be re-applied over discovered macros. */
6456
+ configEntries = /* @__PURE__ */ new Map();
6446
6457
  /**
6447
6458
  * Load built-in macro metadata from @rohal12/spindle's macro-registry.json.
6448
6459
  * This is the base layer that provides name, block, subMacros, flags, source.
@@ -6499,10 +6510,67 @@ var init_macro_registry = __esm({
6499
6510
  /**
6500
6511
  * Overlay user config onto the registry.
6501
6512
  * Same format as supplements; applied last so it wins.
6513
+ *
6514
+ * Replaces any previously loaded config: entries from the last call are
6515
+ * reverted first, so macros removed from the config disappear (or fall
6516
+ * back to their builtin/supplement definition).
6502
6517
  */
6503
6518
  loadConfig(config2) {
6519
+ for (const [key, previous] of this.configSnapshot) {
6520
+ if (previous) {
6521
+ this.macros.set(key, previous);
6522
+ } else {
6523
+ this.macros.delete(key);
6524
+ }
6525
+ }
6526
+ this.configSnapshot.clear();
6527
+ this.configEntries.clear();
6528
+ for (const rawKey of Object.keys(config2)) {
6529
+ const key = rawKey.toLowerCase();
6530
+ if (!this.configSnapshot.has(key)) {
6531
+ const existing = this.macros.get(key);
6532
+ this.configSnapshot.set(key, existing ? { ...existing } : void 0);
6533
+ }
6534
+ this.configEntries.set(key, { ...this.configEntries.get(key), ...config2[rawKey] });
6535
+ }
6504
6536
  this.mergeEntries(config2);
6505
6537
  }
6538
+ /**
6539
+ * Replace the set of macros discovered from Story.defineMacro() calls.
6540
+ * Previously discovered definitions that are no longer present are dropped,
6541
+ * restoring whatever the other tiers define for that name.
6542
+ *
6543
+ * Mirrors Story.defineMacro(): a macro is a block when `block: true`, or
6544
+ * when it declares sub-macros and does not set `block: false`. Each
6545
+ * sub-macro becomes a known macro that may only appear inside its parent.
6546
+ */
6547
+ setDiscoveredMacros(macros) {
6548
+ this.discovered.clear();
6549
+ for (const m of macros) {
6550
+ const subMacros = m.subMacros ?? [];
6551
+ this.discovered.set(m.name.toLowerCase(), {
6552
+ name: m.name,
6553
+ block: m.block === true || m.block !== false && subMacros.length > 0,
6554
+ subMacros,
6555
+ storeVar: m.storeVar,
6556
+ interpolate: m.interpolate,
6557
+ merged: m.merged,
6558
+ description: m.description,
6559
+ source: "user"
6560
+ });
6561
+ }
6562
+ for (const m of macros) {
6563
+ for (const sub of m.subMacros ?? []) {
6564
+ const key = sub.toLowerCase();
6565
+ const existing = this.discovered.get(key);
6566
+ if (existing) {
6567
+ existing.parents = [...existing.parents ?? [], m.name];
6568
+ } else {
6569
+ this.discovered.set(key, { name: sub, block: false, subMacros: [], source: "user", parents: [m.name] });
6570
+ }
6571
+ }
6572
+ }
6573
+ }
6506
6574
  /** Add or replace a single macro entry. */
6507
6575
  addMacro(info) {
6508
6576
  const key = info.name.toLowerCase();
@@ -6524,20 +6592,54 @@ var init_macro_registry = __esm({
6524
6592
  }
6525
6593
  /** Get a macro by name (case-insensitive). */
6526
6594
  getMacro(name) {
6527
- return this.macros.get(name.toLowerCase());
6595
+ return this.resolve(name.toLowerCase());
6528
6596
  }
6529
6597
  /** Check if a macro is a block (container) macro. */
6530
6598
  isBlock(name) {
6531
- return this.macros.get(name.toLowerCase())?.block ?? false;
6599
+ return this.getMacro(name)?.block ?? false;
6532
6600
  }
6533
6601
  /** Check if a macro is a sub-macro (has parents). */
6534
6602
  isSubMacro(name) {
6535
- const info = this.macros.get(name.toLowerCase());
6603
+ const info = this.getMacro(name);
6536
6604
  return info?.parents != null && info.parents.length > 0;
6537
6605
  }
6538
6606
  /** Get all registered macros. */
6539
6607
  getAllMacros() {
6540
- return Array.from(this.macros.values());
6608
+ const keys = /* @__PURE__ */ new Set([...this.macros.keys(), ...this.discovered.keys()]);
6609
+ return Array.from(keys, (key) => this.resolve(key));
6610
+ }
6611
+ /**
6612
+ * Compose the effective entry for a lowercase key: builtins/supplements/
6613
+ * config (stored in `macros`), overlaid by discovered metadata, with user
6614
+ * config re-applied on top so it keeps precedence.
6615
+ */
6616
+ resolve(key) {
6617
+ const base = this.macros.get(key);
6618
+ const found = this.discovered.get(key);
6619
+ if (!found) return base;
6620
+ const info = {
6621
+ ...base,
6622
+ name: base?.name ?? found.name,
6623
+ block: found.block ?? base?.block ?? false,
6624
+ subMacros: found.subMacros?.length ? found.subMacros : base?.subMacros ?? [],
6625
+ source: "user",
6626
+ storeVar: found.storeVar ?? base?.storeVar,
6627
+ interpolate: found.interpolate ?? base?.interpolate,
6628
+ merged: found.merged ?? base?.merged,
6629
+ description: found.description ?? base?.description,
6630
+ // A discovered sub-macro may also belong to other (e.g. built-in) parents
6631
+ parents: found.parents ? [.../* @__PURE__ */ new Set([...base?.parents ?? [], ...found.parents])] : base?.parents
6632
+ };
6633
+ const config2 = this.configEntries.get(key);
6634
+ if (config2) {
6635
+ if (config2.description !== void 0) info.description = config2.description;
6636
+ if (config2.parameters !== void 0) info.parameters = config2.parameters;
6637
+ if (config2.children !== void 0) info.children = config2.children;
6638
+ if (config2.parents !== void 0) info.parents = config2.parents;
6639
+ if (config2.skipArgs !== void 0) info.skipArgs = config2.skipArgs;
6640
+ if (config2.container !== void 0) info.block = config2.container;
6641
+ }
6642
+ return info;
6541
6643
  }
6542
6644
  /**
6543
6645
  * Walk up directory tree from `startDir` to find the
@@ -6602,7 +6704,63 @@ var init_macro_registry = __esm({
6602
6704
  });
6603
6705
 
6604
6706
  // src/core/workspace/variable-tracker.ts
6605
- var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, VariableTracker;
6707
+ function findLiteralEnd(text, start) {
6708
+ const open = text[start];
6709
+ if (open === '"' || open === "'" || open === "`") {
6710
+ for (let i = start + 1; i < text.length; i++) {
6711
+ const ch = text[i];
6712
+ if (ch === "\\") {
6713
+ i++;
6714
+ continue;
6715
+ }
6716
+ if (open === "`" && ch === "$" && text[i + 1] === "{") return -1;
6717
+ if (ch === open) return i;
6718
+ }
6719
+ return -1;
6720
+ }
6721
+ let depth = 0;
6722
+ for (let i = start; i < text.length; i++) {
6723
+ const ch = text[i];
6724
+ if (ch === '"' || ch === "'" || ch === "`") {
6725
+ const end = findLiteralEnd(text, i);
6726
+ if (end === -1) return -1;
6727
+ i = end;
6728
+ continue;
6729
+ }
6730
+ if (ch === "[" || ch === "{" || ch === "(") depth++;
6731
+ else if (ch === "]" || ch === "}" || ch === ")") {
6732
+ depth--;
6733
+ if (depth === 0) return i;
6734
+ }
6735
+ }
6736
+ return -1;
6737
+ }
6738
+ function inferLiteralType(expr) {
6739
+ const e = expr.trim();
6740
+ if (e === "") return void 0;
6741
+ if (e === "true" || e === "false") return "boolean";
6742
+ if (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/.test(e)) return "number";
6743
+ const first = e[0];
6744
+ if (first !== "[" && first !== "{" && first !== '"' && first !== "'" && first !== "`") {
6745
+ return void 0;
6746
+ }
6747
+ if (findLiteralEnd(e, 0) !== e.length - 1) return void 0;
6748
+ if (first === "[") return "array";
6749
+ if (first === "{") return "object";
6750
+ return "string";
6751
+ }
6752
+ function declarationLocation(line, name, absLine, uri) {
6753
+ const start = line.length - line.trimStart().length;
6754
+ const location = {
6755
+ declarationRange: {
6756
+ start: { line: absLine, character: start },
6757
+ end: { line: absLine, character: start + 1 + name.length }
6758
+ }
6759
+ };
6760
+ if (uri !== void 0) location.declarationUri = uri;
6761
+ return location;
6762
+ }
6763
+ var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, ARRAY_MEMBERS, VariableTracker;
6606
6764
  var init_variable_tracker = __esm({
6607
6765
  "src/core/workspace/variable-tracker.ts"() {
6608
6766
  "use strict";
@@ -6631,6 +6789,20 @@ var init_variable_tracker = __esm({
6631
6789
  /'(?:\\.|[^'\\])*'/g
6632
6790
  // single-quoted strings
6633
6791
  ];
6792
+ ARRAY_MEMBERS = /* @__PURE__ */ new Set([
6793
+ ...Object.getOwnPropertyNames(Array.prototype),
6794
+ ...Object.getOwnPropertyNames(Object.prototype),
6795
+ "at",
6796
+ "flat",
6797
+ "flatMap",
6798
+ "includes",
6799
+ "findLast",
6800
+ "findLastIndex",
6801
+ "toReversed",
6802
+ "toSorted",
6803
+ "toSpliced",
6804
+ "with"
6805
+ ]);
6634
6806
  VariableTracker = class {
6635
6807
  declared = /* @__PURE__ */ new Map();
6636
6808
  _hasStoryVariables = false;
@@ -6646,7 +6818,7 @@ var init_variable_tracker = __esm({
6646
6818
  * Parse the StoryVariables passage content for declarations.
6647
6819
  * Each line like `$name = value` becomes a declaration.
6648
6820
  */
6649
- parseStoryVariables(content, contentStartLine = 0) {
6821
+ parseStoryVariables(content, contentStartLine = 0, uri) {
6650
6822
  this.declared.clear();
6651
6823
  this._hasStoryVariables = true;
6652
6824
  this._nullDeclarations = [];
@@ -6658,9 +6830,10 @@ var init_variable_tracker = __esm({
6658
6830
  if (!match2) continue;
6659
6831
  const name = match2[1];
6660
6832
  const expr = match2[2].trim();
6833
+ const absLine = contentStartLine + i;
6834
+ const location = declarationLocation(lines[i], name, absLine, uri);
6661
6835
  if (expr === "null") {
6662
6836
  const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
6663
- const absLine = contentStartLine + i;
6664
6837
  this._nullDeclarations.push({
6665
6838
  name,
6666
6839
  sigil: "$",
@@ -6669,10 +6842,12 @@ var init_variable_tracker = __esm({
6669
6842
  end: { line: absLine, character: charIdx + 4 }
6670
6843
  }
6671
6844
  });
6672
- this.declared.set(name, { name, sigil: "$" });
6845
+ this.declared.set(name, { name, sigil: "$", ...location });
6673
6846
  continue;
6674
6847
  }
6675
- const decl = { name, sigil: "$" };
6848
+ const decl = { name, sigil: "$", ...location };
6849
+ const type = inferLiteralType(expr);
6850
+ if (type) decl.type = type;
6676
6851
  if (expr.startsWith("{")) {
6677
6852
  const fieldRegex = /(\w+)\s*:/g;
6678
6853
  let fieldMatch;
@@ -6687,11 +6862,23 @@ var init_variable_tracker = __esm({
6687
6862
  this.declared.set(name, decl);
6688
6863
  }
6689
6864
  }
6865
+ /** Forget StoryVariables declarations, e.g. after the passage was removed. */
6866
+ clearStoryVariables() {
6867
+ this.declared.clear();
6868
+ this._hasStoryVariables = false;
6869
+ this._nullDeclarations = [];
6870
+ }
6871
+ /** Forget StoryTransients declarations, e.g. after the passage was removed. */
6872
+ clearStoryTransients() {
6873
+ this.declaredTransient.clear();
6874
+ this._hasStoryTransients = false;
6875
+ this._nullTransientDeclarations = [];
6876
+ }
6690
6877
  /**
6691
6878
  * Parse the StoryTransients passage content for declarations.
6692
6879
  * Each line like `%name = value` becomes a declaration.
6693
6880
  */
6694
- parseStoryTransients(content, contentStartLine = 0) {
6881
+ parseStoryTransients(content, contentStartLine = 0, uri) {
6695
6882
  this.declaredTransient.clear();
6696
6883
  this._hasStoryTransients = true;
6697
6884
  this._nullTransientDeclarations = [];
@@ -6703,9 +6890,10 @@ var init_variable_tracker = __esm({
6703
6890
  if (!match2) continue;
6704
6891
  const name = match2[1];
6705
6892
  const expr = match2[2].trim();
6893
+ const absLine = contentStartLine + i;
6894
+ const location = declarationLocation(lines[i], name, absLine, uri);
6706
6895
  if (expr === "null") {
6707
6896
  const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
6708
- const absLine = contentStartLine + i;
6709
6897
  this._nullTransientDeclarations.push({
6710
6898
  name,
6711
6899
  sigil: "%",
@@ -6714,10 +6902,12 @@ var init_variable_tracker = __esm({
6714
6902
  end: { line: absLine, character: charIdx + 4 }
6715
6903
  }
6716
6904
  });
6717
- this.declaredTransient.set(name, { name, sigil: "%" });
6905
+ this.declaredTransient.set(name, { name, sigil: "%", ...location });
6718
6906
  continue;
6719
6907
  }
6720
- const decl = { name, sigil: "%" };
6908
+ const decl = { name, sigil: "%", ...location };
6909
+ const type = inferLiteralType(expr);
6910
+ if (type) decl.type = type;
6721
6911
  if (expr.startsWith("{")) {
6722
6912
  const fieldRegex = /(\w+)\s*:/g;
6723
6913
  let fieldMatch;
@@ -6746,19 +6936,20 @@ var init_variable_tracker = __esm({
6746
6936
  for (let i = 0; i < lines.length; i++) {
6747
6937
  const header = parsePassageHeader(lines[i], i);
6748
6938
  if (header) {
6749
- passageBoundaries.push({ name: header.name, startLine: i });
6939
+ passageBoundaries.push({ name: header.name, tags: header.tags, startLine: i });
6750
6940
  }
6751
6941
  }
6752
6942
  for (let pi = 0; pi < passageBoundaries.length; pi++) {
6753
6943
  const passage = passageBoundaries[pi];
6754
6944
  if (EXCLUDED_PASSAGES.has(passage.name)) continue;
6945
+ if (isScriptOrStylesheetPassage(passage)) continue;
6755
6946
  const contentStartLine = passage.startLine + 1;
6756
6947
  const contentEndLine = pi + 1 < passageBoundaries.length ? passageBoundaries[pi + 1].startLine : lines.length;
6757
6948
  const contentLines = lines.slice(contentStartLine, contentEndLine);
6758
6949
  const content = contentLines.join("\n");
6759
6950
  let cleaned = content;
6760
6951
  for (const pattern of CLEAN_PATTERNS) {
6761
- cleaned = cleaned.replace(pattern, (m) => " ".repeat(m.length));
6952
+ cleaned = cleaned.replace(pattern, (m) => m.replace(/[^\r\n]/g, " "));
6762
6953
  }
6763
6954
  const lineOffsets = [0];
6764
6955
  for (let i = 0; i < cleaned.length; i++) {
@@ -6878,6 +7069,29 @@ var init_variable_tracker = __esm({
6878
7069
  hasStoryTransients() {
6879
7070
  return this._hasStoryTransients;
6880
7071
  }
7072
+ /**
7073
+ * Get `$var.member` / `%var.member` accesses in a document where `var` is
7074
+ * declared with an array literal default and `member` is not a property
7075
+ * that JavaScript arrays have (so the access always yields undefined).
7076
+ */
7077
+ getArrayMemberAccesses(uri) {
7078
+ const results = [];
7079
+ const sources = [
7080
+ ["$", this.usagesByUri.get(uri), this.declared],
7081
+ ["%", this.transientUsagesByUri.get(uri), this.declaredTransient]
7082
+ ];
7083
+ for (const [sigil, usages, declared] of sources) {
7084
+ if (!usages) continue;
7085
+ for (const u of usages) {
7086
+ const member = u.fullName.split(".")[1];
7087
+ if (member === void 0) continue;
7088
+ if (declared.get(u.baseName)?.type !== "array") continue;
7089
+ if (ARRAY_MEMBERS.has(member)) continue;
7090
+ results.push({ sigil, name: u.baseName, member, range: u.range });
7091
+ }
7092
+ }
7093
+ return results;
7094
+ }
6881
7095
  /** Get variables declared with null values in StoryVariables. */
6882
7096
  getNullDeclarations() {
6883
7097
  return this._nullDeclarations;
@@ -6890,80 +7104,6 @@ var init_variable_tracker = __esm({
6890
7104
  }
6891
7105
  });
6892
7106
 
6893
- // src/core/workspace/widget-registry.ts
6894
- var widgetDefRegex, paramRegex, WidgetRegistry;
6895
- var init_widget_registry = __esm({
6896
- "src/core/workspace/widget-registry.ts"() {
6897
- "use strict";
6898
- widgetDefRegex = /\{widget\s+"([^"]+)"((?:\s+@[A-Za-z_$][\w$]*)*)\s*\}/gi;
6899
- paramRegex = /@([A-Za-z_$][\w$]*)/g;
6900
- WidgetRegistry = class {
6901
- widgets = /* @__PURE__ */ new Map();
6902
- /**
6903
- * Scan all passages for widget definitions.
6904
- * Only passages tagged `widget` are examined.
6905
- *
6906
- * @param passages - All known passages.
6907
- * @param getContent - Function to retrieve document text by URI.
6908
- */
6909
- scan(passages, getContent) {
6910
- this.widgets.clear();
6911
- for (const passage of passages) {
6912
- if (!passage.tags?.includes("widget")) continue;
6913
- const text = getContent(passage.uri);
6914
- if (!text) continue;
6915
- const lines = text.split("\n");
6916
- const contentStartLine = passage.range.start.line + 1;
6917
- let contentEndLine = lines.length;
6918
- for (let i = contentStartLine; i < lines.length; i++) {
6919
- if (/^::\s+/.test(lines[i])) {
6920
- contentEndLine = i;
6921
- break;
6922
- }
6923
- }
6924
- const contentLines = lines.slice(contentStartLine, contentEndLine);
6925
- for (let i = 0; i < contentLines.length; i++) {
6926
- const line = contentLines[i];
6927
- widgetDefRegex.lastIndex = 0;
6928
- let match2;
6929
- while ((match2 = widgetDefRegex.exec(line)) !== null) {
6930
- const widgetName = match2[1];
6931
- const paramString = match2[2] || "";
6932
- const params = [];
6933
- let paramMatch;
6934
- paramRegex.lastIndex = 0;
6935
- while ((paramMatch = paramRegex.exec(paramString)) !== null) {
6936
- params.push(paramMatch[1]);
6937
- }
6938
- const lineNum = contentStartLine + i;
6939
- const charStart = match2.index;
6940
- const charEnd = charStart + match2[0].length;
6941
- const range2 = {
6942
- start: { line: lineNum, character: charStart },
6943
- end: { line: lineNum, character: charEnd }
6944
- };
6945
- this.widgets.set(widgetName, {
6946
- name: widgetName,
6947
- params,
6948
- uri: passage.uri,
6949
- range: range2
6950
- });
6951
- }
6952
- }
6953
- }
6954
- }
6955
- /** Get a widget definition by name. */
6956
- getWidget(name) {
6957
- return this.widgets.get(name);
6958
- }
6959
- /** Get all registered widget definitions. */
6960
- getAllWidgets() {
6961
- return Array.from(this.widgets.values());
6962
- }
6963
- };
6964
- }
6965
- });
6966
-
6967
7107
  // src/core/parsing/macro-parser.ts
6968
7108
  function buildLineStarts(text) {
6969
7109
  const starts = [0];
@@ -7023,20 +7163,30 @@ function parseMacros(text) {
7023
7163
  }
7024
7164
  return macros;
7025
7165
  }
7026
- function pairMacros(macros, isBlock) {
7027
- const stacks = {};
7166
+ function pairMacros(macros, isBlock, passageStartLines = []) {
7167
+ const boundaries = [...passageStartLines].sort((a, b) => a - b);
7168
+ let nextBoundary = 0;
7169
+ let stack = [];
7028
7170
  for (const macro of macros) {
7029
- const name = macro.name.toLowerCase();
7171
+ let crossed = false;
7172
+ while (nextBoundary < boundaries.length && macro.range.start.line >= boundaries[nextBoundary]) {
7173
+ nextBoundary++;
7174
+ crossed = true;
7175
+ }
7176
+ if (crossed) stack = [];
7030
7177
  if (!isBlock(macro.name)) continue;
7031
7178
  if (macro.open) {
7032
- if (!stacks[name]) stacks[name] = [];
7033
- stacks[name].push(macro.id);
7034
- } else {
7035
- const stack = stacks[name];
7036
- if (stack && stack.length > 0) {
7037
- const openId = stack.pop();
7038
- macros[openId].pair = macro.id;
7039
- macro.pair = openId;
7179
+ stack.push(macro);
7180
+ continue;
7181
+ }
7182
+ const name = macro.name.toLowerCase();
7183
+ for (let i = stack.length - 1; i >= 0; i--) {
7184
+ if (stack[i].name.toLowerCase() === name) {
7185
+ const opener = stack[i];
7186
+ opener.pair = macro.id;
7187
+ macro.pair = opener.id;
7188
+ stack.length = i;
7189
+ break;
7040
7190
  }
7041
7191
  }
7042
7192
  }
@@ -7050,6 +7200,221 @@ var init_macro_parser = __esm({
7050
7200
  }
7051
7201
  });
7052
7202
 
7203
+ // src/core/workspace/widget-registry.ts
7204
+ function parseParams(argString) {
7205
+ return argString.trim().split(/\s+/).filter((t) => /^[$_@]/.test(t) && t !== "@children");
7206
+ }
7207
+ var widgetDefRegex, WidgetRegistry;
7208
+ var init_widget_registry = __esm({
7209
+ "src/core/workspace/widget-registry.ts"() {
7210
+ "use strict";
7211
+ init_macro_parser();
7212
+ widgetDefRegex = /(\{widget\s+["']?)([^\s"'}]+)["']?([^}]*)\}/gi;
7213
+ WidgetRegistry = class {
7214
+ /** Widgets keyed by lower-cased name; Spindle resolves widgets case-insensitively. */
7215
+ widgets = /* @__PURE__ */ new Map();
7216
+ /** Per-URI set of lower-cased macro names opened in that document. */
7217
+ invokedByUri = /* @__PURE__ */ new Map();
7218
+ /**
7219
+ * Scan all passages for widget definitions.
7220
+ * Only passages tagged `widget` are examined.
7221
+ *
7222
+ * @param passages - All known passages.
7223
+ * @param getContent - Function to retrieve document text by URI.
7224
+ */
7225
+ scan(passages, getContent) {
7226
+ this.widgets.clear();
7227
+ for (const passage of passages) {
7228
+ if (!passage.tags?.includes("widget")) continue;
7229
+ const text = getContent(passage.uri);
7230
+ if (!text) continue;
7231
+ const lines = text.split("\n");
7232
+ const contentStartLine = passage.range.start.line + 1;
7233
+ let contentEndLine = lines.length;
7234
+ for (let i = contentStartLine; i < lines.length; i++) {
7235
+ if (/^::\s+/.test(lines[i])) {
7236
+ contentEndLine = i;
7237
+ break;
7238
+ }
7239
+ }
7240
+ const content = lines.slice(contentStartLine, contentEndLine).join("\n");
7241
+ const lineStarts = buildLineStarts(content);
7242
+ const toPosition = (offset) => {
7243
+ const pos = offsetToPosition(offset, lineStarts);
7244
+ return { line: contentStartLine + pos.line, character: pos.character };
7245
+ };
7246
+ widgetDefRegex.lastIndex = 0;
7247
+ let match2;
7248
+ while ((match2 = widgetDefRegex.exec(content)) !== null) {
7249
+ const widgetName = match2[2];
7250
+ const nameStart = match2.index + match2[1].length;
7251
+ const range2 = {
7252
+ start: toPosition(match2.index),
7253
+ end: toPosition(match2.index + match2[0].length)
7254
+ };
7255
+ const nameRange = {
7256
+ start: toPosition(nameStart),
7257
+ end: toPosition(nameStart + widgetName.length)
7258
+ };
7259
+ const bodyStart = match2.index + match2[0].length;
7260
+ const rest = content.slice(bodyStart);
7261
+ const closeIdx = rest.search(/\{\/widget\}/i);
7262
+ const body = closeIdx >= 0 ? rest.slice(0, closeIdx) : rest;
7263
+ this.widgets.set(widgetName.toLowerCase(), {
7264
+ name: widgetName,
7265
+ params: parseParams(match2[3]),
7266
+ uri: passage.uri,
7267
+ range: range2,
7268
+ nameRange,
7269
+ block: /\{@children\}/.test(body)
7270
+ });
7271
+ }
7272
+ }
7273
+ }
7274
+ /** Get a widget definition by name (case-insensitive). */
7275
+ getWidget(name) {
7276
+ return this.widgets.get(name.toLowerCase());
7277
+ }
7278
+ /** Get all registered widget definitions. */
7279
+ getAllWidgets() {
7280
+ return Array.from(this.widgets.values());
7281
+ }
7282
+ /**
7283
+ * Record the macro names opened in a document, replacing any previous
7284
+ * record for that URI. Spindle resolves widget names case-insensitively,
7285
+ * so names are stored lower-cased.
7286
+ */
7287
+ recordInvocations(uri, macros) {
7288
+ const names = /* @__PURE__ */ new Set();
7289
+ for (const macro of macros) {
7290
+ if (macro.open) names.add(macro.name.toLowerCase());
7291
+ }
7292
+ this.invokedByUri.set(uri, names);
7293
+ }
7294
+ /** Forget all recorded invocations. */
7295
+ clearInvocations() {
7296
+ this.invokedByUri.clear();
7297
+ }
7298
+ /** Whether any recorded document invokes the widget `{name}`. */
7299
+ isInvoked(name) {
7300
+ const key = name.toLowerCase();
7301
+ for (const names of this.invokedByUri.values()) {
7302
+ if (names.has(key)) return true;
7303
+ }
7304
+ return false;
7305
+ }
7306
+ };
7307
+ }
7308
+ });
7309
+
7310
+ // src/core/parsing/macro-discovery.ts
7311
+ function extractBalancedBraces(source, start) {
7312
+ let depth = 0;
7313
+ let inSingleQuote = false;
7314
+ let inDoubleQuote = false;
7315
+ let inBacktick = false;
7316
+ for (let i = start; i < source.length; i++) {
7317
+ const ch = source[i];
7318
+ const prev = i > start ? source[i - 1] : "";
7319
+ if (prev === "\\") continue;
7320
+ if (inSingleQuote) {
7321
+ if (ch === "'") inSingleQuote = false;
7322
+ continue;
7323
+ }
7324
+ if (inDoubleQuote) {
7325
+ if (ch === '"') inDoubleQuote = false;
7326
+ continue;
7327
+ }
7328
+ if (inBacktick) {
7329
+ if (ch === "`") inBacktick = false;
7330
+ continue;
7331
+ }
7332
+ if (ch === "'") {
7333
+ inSingleQuote = true;
7334
+ } else if (ch === '"') {
7335
+ inDoubleQuote = true;
7336
+ } else if (ch === "`") {
7337
+ inBacktick = true;
7338
+ } else if (ch === "{") {
7339
+ depth++;
7340
+ } else if (ch === "}") {
7341
+ depth--;
7342
+ if (depth === 0) {
7343
+ return source.slice(start, i + 1);
7344
+ }
7345
+ }
7346
+ }
7347
+ return null;
7348
+ }
7349
+ function parseConfigFields(config2) {
7350
+ const nameMatch = config2.match(/name:\s*['"]([^'"]+)['"]/);
7351
+ if (!nameMatch) return null;
7352
+ const macro = {
7353
+ name: nameMatch[1]
7354
+ };
7355
+ const boolFields = [
7356
+ "block",
7357
+ "storeVar",
7358
+ "merged",
7359
+ "interpolate"
7360
+ ];
7361
+ for (const field of boolFields) {
7362
+ const re = new RegExp(`${field}:\\s*(true|false)`);
7363
+ const m = config2.match(re);
7364
+ if (m) {
7365
+ macro[field] = m[1] === "true";
7366
+ }
7367
+ }
7368
+ const descMatch = config2.match(/description:\s*['"]([^'"]+)['"]/);
7369
+ if (descMatch) {
7370
+ macro.description = descMatch[1];
7371
+ }
7372
+ const subMatch = config2.match(/subMacros:\s*\[([^\]]*)\]/);
7373
+ if (subMatch) {
7374
+ const inner = subMatch[1].trim();
7375
+ if (inner) {
7376
+ macro.subMacros = inner.split(",").map(
7377
+ (s) => s.trim().replace(/^['"]|['"]$/g, "")
7378
+ ).filter((s) => s.length > 0);
7379
+ }
7380
+ }
7381
+ return macro;
7382
+ }
7383
+ function discoverMacrosFromSource(source) {
7384
+ const macros = [];
7385
+ defineMacroRegex.lastIndex = 0;
7386
+ let match2;
7387
+ while ((match2 = defineMacroRegex.exec(source)) !== null) {
7388
+ const braceStart = match2.index + match2[0].length - 1;
7389
+ const config2 = extractBalancedBraces(source, braceStart);
7390
+ if (!config2) continue;
7391
+ const macro = parseConfigFields(config2);
7392
+ if (macro) {
7393
+ macros.push(macro);
7394
+ }
7395
+ }
7396
+ return macros;
7397
+ }
7398
+ function discoverMacrosFromStoryInit(passageContent2) {
7399
+ const macros = [];
7400
+ doBlockRegex.lastIndex = 0;
7401
+ let doMatch;
7402
+ while ((doMatch = doBlockRegex.exec(passageContent2)) !== null) {
7403
+ const jsContent = doMatch[1];
7404
+ const found = discoverMacrosFromSource(jsContent);
7405
+ macros.push(...found);
7406
+ }
7407
+ return macros;
7408
+ }
7409
+ var defineMacroRegex, doBlockRegex;
7410
+ var init_macro_discovery = __esm({
7411
+ "src/core/parsing/macro-discovery.ts"() {
7412
+ "use strict";
7413
+ defineMacroRegex = /Story\.defineMacro\s*\(\s*\{/g;
7414
+ doBlockRegex = /\{do\}([\s\S]*?)\{\/do\}/g;
7415
+ }
7416
+ });
7417
+
7053
7418
  // src/macro-supplements.json
7054
7419
  var macro_supplements_default;
7055
7420
  var init_macro_supplements = __esm({
@@ -7240,7 +7605,7 @@ var init_macro_supplements = __esm({
7240
7605
  },
7241
7606
  include: {
7242
7607
  name: "include",
7243
- description: 'Outputs the contents of the specified passage.\n\nUsage: `{include "passage"}`',
7608
+ description: 'Outputs the contents of the specified passage. The argument is a passage name or an expression that evaluates to one; add `inline` to render the passage as inline content without markdown processing.\n\nUsage: `{include "passage"}`, `{include $passageName}` or `{include "passage" inline}`',
7244
7609
  parameters: ["text"]
7245
7610
  },
7246
7611
  passage: {
@@ -7250,28 +7615,28 @@ var init_macro_supplements = __esm({
7250
7615
  },
7251
7616
  textbox: {
7252
7617
  name: "textbox",
7253
- description: 'Creates a text input box.\n\nUsage: `{textbox "$receiver" defaultValue}`',
7254
- parameters: ["receiver &+ text"]
7618
+ description: 'Creates a text input box bound to a story variable.\n\nUsage: `{textbox $receiver}` or `{textbox $receiver "placeholder"}`',
7619
+ parameters: ["receiver |+ text"]
7255
7620
  },
7256
7621
  numberbox: {
7257
7622
  name: "numberbox",
7258
- description: 'Creates a number input box.\n\nUsage: `{numberbox "$receiver" defaultValue}`',
7259
- parameters: ["receiver &+ text"]
7623
+ description: 'Creates a number input box bound to a story variable.\n\nUsage: `{numberbox $receiver}` or `{numberbox $receiver "placeholder"}`',
7624
+ parameters: ["receiver |+ text"]
7260
7625
  },
7261
7626
  textarea: {
7262
7627
  name: "textarea",
7263
- description: 'Creates a multi-line text input area.\n\nUsage: `{textarea "$receiver" defaultValue}`',
7264
- parameters: ["receiver &+ text"]
7628
+ description: 'Creates a multi-line text input area bound to a story variable.\n\nUsage: `{textarea $receiver}` or `{textarea $receiver "placeholder"}`',
7629
+ parameters: ["receiver |+ text"]
7265
7630
  },
7266
7631
  checkbox: {
7267
7632
  name: "checkbox",
7268
- description: 'Creates a checkbox input.\n\nUsage: `{checkbox "$receiver" uncheckedValue checkedValue}`',
7269
- parameters: ["receiver &+ text"]
7633
+ description: 'Creates a checkbox that toggles a boolean story variable.\n\nUsage: `{checkbox $receiver "label"}`',
7634
+ parameters: ["receiver |+ text"]
7270
7635
  },
7271
7636
  radiobutton: {
7272
7637
  name: "radiobutton",
7273
- description: 'Creates a radio button input.\n\nUsage: `{radiobutton "$receiver" value}`',
7274
- parameters: ["receiver &+ text &+ text"]
7638
+ description: 'Creates a radio button that sets a story variable to its value. Use the same variable for every option in a group.\n\nUsage: `{radiobutton $receiver "value" "label"}`',
7639
+ parameters: ["receiver &+ text |+ text"]
7275
7640
  },
7276
7641
  quicksave: {
7277
7642
  name: "quicksave",
@@ -7301,8 +7666,8 @@ var init_macro_supplements = __esm({
7301
7666
  dialog: {
7302
7667
  name: "dialog",
7303
7668
  container: true,
7304
- description: 'Opens a dialog with its contents.\n\nUsage:\n\n```\n{dialog "title"}\n ...\n{/dialog}\n```',
7305
- parameters: ["text"]
7669
+ description: 'Creates a button that opens the passage named in its body in a dialog. Add `noclose` to hide the dialog\'s close button.\n\nUsage:\n\n```\n{dialog "label"}Passage Name{/dialog}\n{dialog "label" noclose}Passage Name{/dialog}\n```',
7670
+ parameters: ["text |+ 'noclose'"]
7306
7671
  },
7307
7672
  nobr: {
7308
7673
  name: "nobr",
@@ -7358,6 +7723,7 @@ var init_workspace_model = __esm({
7358
7723
  init_variable_tracker();
7359
7724
  init_widget_registry();
7360
7725
  init_macro_parser();
7726
+ init_macro_discovery();
7361
7727
  init_macro_supplements();
7362
7728
  WorkspaceModel = class extends EventEmitter3 {
7363
7729
  documents;
@@ -7405,6 +7771,14 @@ var init_workspace_model = __esm({
7405
7771
  this.initialized = true;
7406
7772
  this.scheduleModelReady();
7407
7773
  }
7774
+ /**
7775
+ * Re-run cross-document analysis and schedule 'modelReady', e.g. after
7776
+ * the macro configuration changed without any document changing.
7777
+ */
7778
+ refresh() {
7779
+ this.cascade();
7780
+ this.scheduleModelReady();
7781
+ }
7408
7782
  /** Clean up listeners and timers. */
7409
7783
  dispose() {
7410
7784
  this.documents.removeListener("documentChanged", this.onDocumentChanged);
@@ -7422,6 +7796,7 @@ var init_workspace_model = __esm({
7422
7796
  if (text !== void 0) {
7423
7797
  this.passages.rebuild(uri, text);
7424
7798
  }
7799
+ this.refreshDiscoveredMacros();
7425
7800
  this.cascade();
7426
7801
  this.emit("documentChanged", uri);
7427
7802
  this.emit("passagesUpdated", uri);
@@ -7430,6 +7805,7 @@ var init_workspace_model = __esm({
7430
7805
  /** Handle a document close: remove its passages and cascade. */
7431
7806
  handleDocumentClose(uri) {
7432
7807
  this.passages.remove(uri);
7808
+ this.refreshDiscoveredMacros();
7433
7809
  this.cascade();
7434
7810
  this.emit("documentClosed", uri);
7435
7811
  this.emit("passagesUpdated", uri);
@@ -7443,8 +7819,39 @@ var init_workspace_model = __esm({
7443
7819
  this.passages.rebuild(uri, text);
7444
7820
  }
7445
7821
  }
7822
+ this.refreshDiscoveredMacros();
7446
7823
  this.cascade();
7447
7824
  }
7825
+ /**
7826
+ * Re-run static macro discovery over the workspace and replace the
7827
+ * registry's discovered tier (dropping definitions whose source is gone).
7828
+ *
7829
+ * Sources scanned for Story.defineMacro({...}) calls:
7830
+ * - `{do}` blocks in StoryInit passages
7831
+ * - passages tagged `script` (Story JavaScript)
7832
+ * - JavaScript/TypeScript documents in the workspace
7833
+ */
7834
+ refreshDiscoveredMacros() {
7835
+ const found = [];
7836
+ for (const uri of this.documents.getUris()) {
7837
+ const text = this.documents.getText(uri);
7838
+ if (!text) continue;
7839
+ if (/\.[cm]?[jt]s$/i.test(uri)) {
7840
+ found.push(...discoverMacrosFromSource(text));
7841
+ continue;
7842
+ }
7843
+ let lines;
7844
+ for (const passage of this.passages.getPassagesInDocument(uri)) {
7845
+ const isStoryInit = passage.name === "StoryInit";
7846
+ const isScript = passage.tags?.includes("script") ?? false;
7847
+ if (!isStoryInit && !isScript) continue;
7848
+ lines ??= text.split("\n");
7849
+ const content = lines.slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
7850
+ found.push(...isScript ? discoverMacrosFromSource(content) : discoverMacrosFromStoryInit(content));
7851
+ }
7852
+ }
7853
+ this.macros.setDiscoveredMacros(found);
7854
+ }
7448
7855
  /**
7449
7856
  * Cascade: rescan variables and widgets based on current passages.
7450
7857
  * Called after any passage index update.
@@ -7464,8 +7871,10 @@ var init_workspace_model = __esm({
7464
7871
  }
7465
7872
  }
7466
7873
  const content = lines.slice(contentStart, contentEnd).join("\n");
7467
- this.variables.parseStoryVariables(content, contentStart);
7874
+ this.variables.parseStoryVariables(content, contentStart, storyVars.uri);
7468
7875
  }
7876
+ } else {
7877
+ this.variables.clearStoryVariables();
7469
7878
  }
7470
7879
  const storyTransients = this.passages.getStoryTransients();
7471
7880
  if (storyTransients) {
@@ -7481,14 +7890,18 @@ var init_workspace_model = __esm({
7481
7890
  }
7482
7891
  }
7483
7892
  const content = lines.slice(contentStart, contentEnd).join("\n");
7484
- this.variables.parseStoryTransients(content, contentStart);
7893
+ this.variables.parseStoryTransients(content, contentStart, storyTransients.uri);
7485
7894
  }
7895
+ } else {
7896
+ this.variables.clearStoryTransients();
7486
7897
  }
7898
+ this.widgets.clearInvocations();
7487
7899
  for (const uri of this.documents.getUris()) {
7488
7900
  const text = this.documents.getText(uri);
7489
7901
  if (text) {
7490
7902
  const macros = parseMacros(text);
7491
7903
  this.variables.scanDocument(uri, text, macros);
7904
+ this.widgets.recordInvocations(uri, macros);
7492
7905
  }
7493
7906
  }
7494
7907
  const allPassages = this.passages.getAllPassages();
@@ -7533,8 +7946,12 @@ var init_diagnostic_codes = __esm({
7533
7946
  NoStoryVariables: "SP202",
7534
7947
  UndeclaredTransient: "SP203",
7535
7948
  NullVariableValue: "SP204",
7949
+ TemporaryAssignedInLoop: "SP205",
7950
+ ArrayMemberAccess: "SP206",
7536
7951
  BrokenPassageLink: "SP300",
7537
7952
  WidgetArgCountMismatch: "SP301",
7953
+ IncludeWidgetPassage: "SP302",
7954
+ UnusedWidget: "SP303",
7538
7955
  DeadEndPassage: "SP400",
7539
7956
  UnreachablePassage: "SP401",
7540
7957
  LineTooLong: "SP500"
@@ -7556,8 +7973,12 @@ var init_diagnostic_codes = __esm({
7556
7973
  SP202: "info",
7557
7974
  SP203: "warning",
7558
7975
  SP204: "error",
7976
+ SP205: "warning",
7977
+ SP206: "warning",
7559
7978
  SP300: "warning",
7560
7979
  SP301: "warning",
7980
+ SP302: "warning",
7981
+ SP303: "hint",
7561
7982
  SP400: "hint",
7562
7983
  SP401: "hint",
7563
7984
  SP500: "warning"
@@ -8231,6 +8652,30 @@ var init_parameter_validator = __esm({
8231
8652
  return arg.type === 4 /* Variable */ ? null : new TypeError2("Argument is not a variable");
8232
8653
  }
8233
8654
  },
8655
+ {
8656
+ // The story variable a form control binds to: `$name` or `"$name"`.
8657
+ // Spindle strips the quotes and the `$` sigil from the first argument
8658
+ // and reads the rest as a dot path into the story variables.
8659
+ name: ["receiver"],
8660
+ validate(arg) {
8661
+ let name;
8662
+ if (arg.type === 0 /* String */) {
8663
+ name = arg.text.slice(1, -1);
8664
+ } else if (arg.type === 8 /* Bareword */) {
8665
+ name = arg.text;
8666
+ }
8667
+ if (name === void 0) {
8668
+ return new TypeError2("Argument is not a variable receiver");
8669
+ }
8670
+ if (/^[_@%]/.test(name)) {
8671
+ return new TypeError2(`Variable receiver must be a story variable ($name), got '${name}'`);
8672
+ }
8673
+ if (!/^\$?[A-Za-z_][\w$]*(?:\.[\w$]+)*$/.test(name)) {
8674
+ return new TypeError2(`Argument is not a valid variable receiver: '${name}'`);
8675
+ }
8676
+ return null;
8677
+ }
8678
+ },
8234
8679
  {
8235
8680
  name: ["passage"],
8236
8681
  validate(arg, _args, _index, state) {
@@ -8383,58 +8828,175 @@ var init_parameter_validator = __esm({
8383
8828
  });
8384
8829
 
8385
8830
  // src/core/parsing/link-parser.ts
8386
- function buildLineStarts2(text) {
8387
- const starts = [0];
8388
- for (let i = 0; i < text.length; i++) {
8389
- if (text[i] === "\n") {
8390
- starts.push(i + 1);
8831
+ function skipSelectors(text, i) {
8832
+ while (text[i] === "." || text[i] === "#") {
8833
+ i++;
8834
+ for (; ; ) {
8835
+ if (/[a-zA-Z0-9_-]/.test(text[i] ?? "")) {
8836
+ i++;
8837
+ continue;
8838
+ }
8839
+ const interpolation = /^\{[$_@][\w.]*\}/.exec(text.slice(i));
8840
+ if (!interpolation) break;
8841
+ i += interpolation[0].length;
8391
8842
  }
8392
8843
  }
8393
- return starts;
8844
+ return i;
8394
8845
  }
8395
- function offsetToPosition2(offset, lineStarts) {
8396
- let low = 0;
8397
- let high = lineStarts.length - 1;
8398
- while (low < high) {
8399
- const mid = low + high + 1 >> 1;
8400
- if (lineStarts[mid] <= offset) {
8401
- low = mid;
8402
- } else {
8403
- high = mid - 1;
8404
- }
8846
+ function locateTarget(inner) {
8847
+ let start = 0;
8848
+ let end = inner.length;
8849
+ const pipeIdx = inner.indexOf("|");
8850
+ const arrowIdx = inner.indexOf("->");
8851
+ const revIdx = inner.indexOf("<-");
8852
+ if (pipeIdx !== -1) {
8853
+ start = pipeIdx + 1;
8854
+ } else if (arrowIdx !== -1) {
8855
+ start = arrowIdx + 2;
8856
+ } else if (revIdx !== -1) {
8857
+ end = revIdx;
8405
8858
  }
8406
- return { line: low, character: offset - lineStarts[low] };
8859
+ while (start < end && /\s/.test(inner[start])) start++;
8860
+ while (end > start && /\s/.test(inner[end - 1])) end--;
8861
+ return { start, end };
8407
8862
  }
8408
8863
  function parseLinks(text, lineOffset = 0) {
8409
- const lineStarts = buildLineStarts2(text);
8864
+ const lineStarts = buildLineStarts(text);
8410
8865
  const refs = [];
8411
- linkRegex.lastIndex = 0;
8412
- let match2;
8413
- while ((match2 = linkRegex.exec(text)) !== null) {
8414
- const passageName = match2[2].trim();
8415
- if (!passageName) continue;
8416
- const nameStartInMatch = match2[0].indexOf(passageName);
8417
- const nameStart = match2.index + nameStartInMatch;
8418
- const nameEnd = nameStart + passageName.length;
8419
- const startPos = offsetToPosition2(nameStart, lineStarts);
8420
- const endPos = offsetToPosition2(nameEnd, lineStarts);
8421
- const range2 = {
8422
- start: { line: startPos.line + lineOffset, character: startPos.character },
8423
- end: { line: endPos.line + lineOffset, character: endPos.character }
8424
- };
8866
+ let i = text.indexOf("[[");
8867
+ while (i !== -1) {
8868
+ const linkStart = i;
8869
+ i += 2;
8870
+ if (text[i] === "." || text[i] === "#") {
8871
+ i = skipSelectors(text, i);
8872
+ if (text[i] === " ") i++;
8873
+ }
8874
+ const innerStart = i;
8875
+ let depth = 1;
8876
+ while (i < text.length) {
8877
+ if (text.startsWith("[[", i)) {
8878
+ depth++;
8879
+ i += 2;
8880
+ } else if (text.startsWith("]]", i)) {
8881
+ if (--depth === 0) break;
8882
+ i += 2;
8883
+ } else {
8884
+ i++;
8885
+ }
8886
+ }
8887
+ if (depth !== 0) {
8888
+ i = text.indexOf("[[", linkStart + 2);
8889
+ continue;
8890
+ }
8891
+ const target = locateTarget(text.slice(innerStart, i));
8892
+ if (target.end > target.start) {
8893
+ const nameStart = innerStart + target.start;
8894
+ const nameEnd = innerStart + target.end;
8895
+ const startPos = offsetToPosition(nameStart, lineStarts);
8896
+ const endPos = offsetToPosition(nameEnd, lineStarts);
8897
+ refs.push({
8898
+ name: text.slice(nameStart, nameEnd),
8899
+ range: {
8900
+ start: { line: startPos.line + lineOffset, character: startPos.character },
8901
+ end: { line: endPos.line + lineOffset, character: endPos.character }
8902
+ },
8903
+ source: "link"
8904
+ });
8905
+ }
8906
+ i = text.indexOf("[[", i + 2);
8907
+ }
8908
+ return refs;
8909
+ }
8910
+ function findPassageRefAt(text, position) {
8911
+ const { line, character } = position;
8912
+ return [...parseLinks(text), ...parseMacroPassageRefs(text)].find(({ range: { start, end } }) => (line > start.line || line === start.line && character >= start.character) && (line < end.line || line === end.line && character <= end.character));
8913
+ }
8914
+ function resolveExpressionTarget(args2) {
8915
+ const lead = args2.length - args2.trimStart().length;
8916
+ const expr = args2.trim();
8917
+ if (expr === "") return null;
8918
+ const lexed = lexArguments(expr);
8919
+ if (lexed.length === 1 && lexed[0].start === 0 && lexed[0].end === expr.length && (lexed[0].type === 0 /* String */ || lexed[0].type === 7 /* Expression */)) {
8920
+ const raw = expr.slice(1, -1);
8921
+ if (interpolationRegex.test(raw) || raw.includes("${")) return null;
8922
+ return { name: raw.replace(/\\(.)/g, "$1"), start: lead + 1, end: lead + expr.length - 1 };
8923
+ }
8924
+ if (/[$@%"'`(]/.test(expr) || temporaryRefRegex.test(expr)) return null;
8925
+ if (EXPRESSION_BUILTINS.has(expr)) return null;
8926
+ return { name: expr, start: lead, end: lead + expr.length };
8927
+ }
8928
+ function resolveIncludeTarget(args2) {
8929
+ const inline = /\binline\b/.exec(args2);
8930
+ if (!inline) return resolveExpressionTarget(args2);
8931
+ const cut = inline.index;
8932
+ const width = inline[0].length;
8933
+ const target = resolveExpressionTarget(args2.slice(0, cut) + args2.slice(cut + width));
8934
+ if (!target || target.start < cut && cut < target.end) return null;
8935
+ return {
8936
+ name: target.name,
8937
+ start: target.start < cut ? target.start : target.start + width,
8938
+ end: target.end <= cut ? target.end : target.end + width
8939
+ };
8940
+ }
8941
+ function resolveLinkMacroTarget(args2) {
8942
+ const strings = lexArguments(args2).filter((a) => a.type === 0 /* String */);
8943
+ if (strings.length < 2) return null;
8944
+ const target = strings[1];
8945
+ const name = target.text.slice(1, -1);
8946
+ if (name.includes("\\") || interpolationRegex.test(name)) return null;
8947
+ return { name, start: target.start + 1, end: target.end - 1 };
8948
+ }
8949
+ function parseMacroPassageRefs(text, lineOffset = 0) {
8950
+ const lineStarts = buildLineStarts(text);
8951
+ const refs = [];
8952
+ for (const macro of parseMacros(text)) {
8953
+ if (!macro.open || !macro.rawArgs) continue;
8954
+ const resolve3 = macroTargetResolvers[macro.name.toLowerCase()];
8955
+ if (!resolve3) continue;
8956
+ const argsEnd = lineStarts[macro.range.end.line] + macro.range.end.character - 1;
8957
+ const argsStart = argsEnd - macro.rawArgs.length;
8958
+ const target = resolve3(text.slice(argsStart, argsEnd));
8959
+ if (!target || target.name === "") continue;
8960
+ const startPos = offsetToPosition(argsStart + target.start, lineStarts);
8961
+ const endPos = offsetToPosition(argsStart + target.end, lineStarts);
8425
8962
  refs.push({
8426
- name: passageName,
8427
- range: range2,
8428
- source: "link"
8963
+ name: target.name,
8964
+ range: {
8965
+ start: { line: startPos.line + lineOffset, character: startPos.character },
8966
+ end: { line: endPos.line + lineOffset, character: endPos.character }
8967
+ },
8968
+ source: "macro"
8429
8969
  });
8430
8970
  }
8431
8971
  return refs;
8432
8972
  }
8433
- var linkRegex;
8973
+ var EXPRESSION_BUILTINS, interpolationRegex, temporaryRefRegex, macroTargetResolvers;
8434
8974
  var init_link_parser = __esm({
8435
8975
  "src/core/parsing/link-parser.ts"() {
8436
8976
  "use strict";
8437
- linkRegex = /\[\[([^\]]*?\|)?([^\]]+?)\]\]/g;
8977
+ init_macro_parser();
8978
+ init_argument_lexer();
8979
+ EXPRESSION_BUILTINS = /* @__PURE__ */ new Set([
8980
+ "currentPassage",
8981
+ "previousPassage",
8982
+ "visited",
8983
+ "hasVisited",
8984
+ "hasVisitedAny",
8985
+ "hasVisitedAll",
8986
+ "rendered",
8987
+ "hasRendered",
8988
+ "hasRenderedAny",
8989
+ "hasRenderedAll",
8990
+ "random",
8991
+ "randomInt"
8992
+ ]);
8993
+ interpolationRegex = /\{[$_@%][A-Za-z_$]/;
8994
+ temporaryRefRegex = /(?<![.\w$@%])_[A-Za-z_$][\w$]*(?![\w$])/;
8995
+ macroTargetResolvers = {
8996
+ goto: resolveExpressionTarget,
8997
+ include: resolveIncludeTarget,
8998
+ link: resolveLinkMacroTarget
8999
+ };
8438
9000
  }
8439
9001
  });
8440
9002
 
@@ -8447,8 +9009,13 @@ function computeDiagnostics(uri, workspace, options) {
8447
9009
  const passages = workspace.passages.getPassagesInDocument(uri);
8448
9010
  if (passages.length === 0) return [];
8449
9011
  const diagnostics = [];
8450
- const macros = parseMacros(text);
8451
- pairMacros(macros, (name) => workspace.macros.isBlock(name));
9012
+ const markupText = maskScriptAndStylesheetPassages(text, passages);
9013
+ const macros = parseMacros(markupText);
9014
+ pairMacros(
9015
+ macros,
9016
+ (name) => workspace.macros.isBlock(name),
9017
+ passages.map((p) => p.range.start.line)
9018
+ );
8452
9019
  const allPassages = workspace.passages.getAllPassages();
8453
9020
  const passageNames = new Set(allPassages.map((p) => p.name));
8454
9021
  try {
@@ -8464,13 +9031,29 @@ function computeDiagnostics(uri, workspace, options) {
8464
9031
  } catch {
8465
9032
  }
8466
9033
  try {
8467
- validateLinks(text, passages, passageNames, diagnostics);
9034
+ validateLinks(markupText, passages, passageNames, diagnostics);
8468
9035
  } catch {
8469
9036
  }
8470
9037
  try {
8471
9038
  validateWidgetInvocations(macros, workspace, diagnostics);
8472
9039
  } catch {
8473
9040
  }
9041
+ try {
9042
+ validateArrayMemberAccess(uri, workspace, diagnostics);
9043
+ } catch {
9044
+ }
9045
+ try {
9046
+ validateLoopTemporaries(uri, text, macros, workspace, diagnostics);
9047
+ } catch {
9048
+ }
9049
+ try {
9050
+ validateWidgetIncludes(text, macros, workspace, diagnostics);
9051
+ } catch {
9052
+ }
9053
+ try {
9054
+ validateUnusedWidgets(uri, workspace, diagnostics);
9055
+ } catch {
9056
+ }
8474
9057
  if (options?.maxLineLength) {
8475
9058
  try {
8476
9059
  validateLineLength(text, passages, options.maxLineLength, diagnostics);
@@ -8482,6 +9065,18 @@ function computeDiagnostics(uri, workspace, options) {
8482
9065
  return [];
8483
9066
  }
8484
9067
  }
9068
+ function maskScriptAndStylesheetPassages(text, passages) {
9069
+ const excluded = passages.filter(isScriptOrStylesheetPassage);
9070
+ if (excluded.length === 0) return text;
9071
+ const lines = text.split("\n");
9072
+ for (const passage of excluded) {
9073
+ const last = Math.min(passage.range.end.line, lines.length - 1);
9074
+ for (let i = passage.range.start.line + 1; i <= last; i++) {
9075
+ lines[i] = lines[i].replace(/[^\r]/g, " ");
9076
+ }
9077
+ }
9078
+ return lines.join("\n");
9079
+ }
8485
9080
  function validateMacros(macros, workspace, diagnostics) {
8486
9081
  for (let curIndex = 0; curIndex < macros.length; curIndex++) {
8487
9082
  const macro = macros[curIndex];
@@ -8594,8 +9189,13 @@ function validateArguments(macros, workspace, passageNames, diagnostics) {
8594
9189
  if (info.skipArgs) continue;
8595
9190
  if (!info.parameters) continue;
8596
9191
  const rawArgs = macro.rawArgs ?? "";
8597
- const args2 = lexArguments(rawArgs);
8598
- const params = new Parameters(info.parameters);
9192
+ const args2 = macro.name.toLowerCase() === "include" ? includeArguments(rawArgs) : lexArguments(rawArgs);
9193
+ let params;
9194
+ try {
9195
+ params = new Parameters(info.parameters);
9196
+ } catch {
9197
+ continue;
9198
+ }
8599
9199
  if (params.isEmpty()) {
8600
9200
  if (args2.length > 0) {
8601
9201
  diagnostics.push(makeDiag(
@@ -8635,6 +9235,16 @@ function validateArguments(macros, workspace, passageNames, diagnostics) {
8635
9235
  }
8636
9236
  }
8637
9237
  }
9238
+ function includeArguments(rawArgs) {
9239
+ const expr = includeExpression(rawArgs);
9240
+ if (expr === "") return [];
9241
+ const args2 = lexArguments(expr);
9242
+ if (args2.length === 1) return args2;
9243
+ return [{ type: 7 /* Expression */, text: expr, start: 0, end: expr.length }];
9244
+ }
9245
+ function includeExpression(rawArgs) {
9246
+ return rawArgs.replace(/\binline\b/, "").trim();
9247
+ }
8638
9248
  function validateVariables(uri, workspace, diagnostics) {
8639
9249
  if (!workspace.variables.hasStoryVariables()) {
8640
9250
  const undeclared = workspace.variables.getUndeclared(uri);
@@ -8725,11 +9335,236 @@ function validateWidgetInvocations(macros, workspace, diagnostics) {
8725
9335
  }
8726
9336
  }
8727
9337
  }
9338
+ function validateArrayMemberAccess(uri, workspace, diagnostics) {
9339
+ for (const a of workspace.variables.getArrayMemberAccesses(uri)) {
9340
+ const passage = a.sigil === "%" ? "StoryTransients" : "StoryVariables";
9341
+ const ref = `${a.sigil}${a.name}`;
9342
+ diagnostics.push(makeDiag(
9343
+ a.range,
9344
+ DiagnosticCode.ArrayMemberAccess,
9345
+ `'${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}").`
9346
+ ));
9347
+ }
9348
+ }
9349
+ function temporaryRefRegex2(name) {
9350
+ const ident = name ? escapeRegex(name) : "[A-Za-z_$][\\w$]*";
9351
+ return new RegExp(`(?<![.\\w$@%])_(${ident})(?![\\w$])`, "g");
9352
+ }
9353
+ function escapeRegex(s) {
9354
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9355
+ }
9356
+ function maskStrings(code) {
9357
+ let out = "";
9358
+ let i = 0;
9359
+ while (i < code.length) {
9360
+ const ch = code[i];
9361
+ if (ch === '"' || ch === "'" || ch === "`") {
9362
+ out += ch;
9363
+ i++;
9364
+ while (i < code.length && code[i] !== ch) {
9365
+ if (code[i] === "\\" && i + 1 < code.length) {
9366
+ out += " ";
9367
+ i += 2;
9368
+ } else if (ch === "`" && code[i] === "$" && code[i + 1] === "{") {
9369
+ const end = interpolationEnd(code, i + 2);
9370
+ out += maskStrings(code.slice(i, end));
9371
+ i = end;
9372
+ } else {
9373
+ out += code[i] === "\n" ? "\n" : " ";
9374
+ i++;
9375
+ }
9376
+ }
9377
+ if (i < code.length) {
9378
+ out += ch;
9379
+ i++;
9380
+ }
9381
+ continue;
9382
+ }
9383
+ out += ch;
9384
+ i++;
9385
+ }
9386
+ return out;
9387
+ }
9388
+ function interpolationEnd(code, from) {
9389
+ let depth = 1;
9390
+ for (let i = from; i < code.length; i++) {
9391
+ if (code[i] === "{") depth++;
9392
+ else if (code[i] === "}" && --depth === 0) return i + 1;
9393
+ }
9394
+ return code.length;
9395
+ }
9396
+ function computedTemporaryTarget(rawArgs) {
9397
+ const lead = rawArgs.length - rawArgs.trimStart().length;
9398
+ const trimmed = rawArgs.trim();
9399
+ let depth = 0;
9400
+ for (let i = 0; i < trimmed.length; i++) {
9401
+ const ch = trimmed[i];
9402
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
9403
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
9404
+ else if (ch === "=" && depth === 0) {
9405
+ if (trimmed[i + 1] === "=") {
9406
+ i++;
9407
+ continue;
9408
+ }
9409
+ if (i > 0 && trimmed[i - 1] === "!") continue;
9410
+ const target = trimmed.slice(0, i).trim();
9411
+ const m = /^_(\w+)$/.exec(target);
9412
+ return m ? [{ name: m[1], offset: lead }] : [];
9413
+ }
9414
+ }
9415
+ return [];
9416
+ }
9417
+ function setTemporaryTargets(rawArgs) {
9418
+ const code = maskStrings(rawArgs);
9419
+ const targets = [];
9420
+ const assignRe = /(?<![.\w$@%])_([A-Za-z_$][\w$]*)\s*=(?![=>])/g;
9421
+ let m;
9422
+ while ((m = assignRe.exec(code)) !== null) {
9423
+ const name = m[1];
9424
+ const rhs = code.slice(m.index + m[0].length, statementEnd(code, m.index + m[0].length));
9425
+ if (temporaryRefRegex2(name).test(rhs)) continue;
9426
+ targets.push({ name, offset: m.index });
9427
+ }
9428
+ return targets;
9429
+ }
9430
+ function statementEnd(code, from) {
9431
+ let depth = 0;
9432
+ for (let i = from; i < code.length; i++) {
9433
+ const ch = code[i];
9434
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
9435
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
9436
+ else if ((ch === ";" || ch === ",") && depth <= 0) return i;
9437
+ }
9438
+ return code.length;
9439
+ }
9440
+ function enclosingLoop(macros, index, uri, workspace) {
9441
+ for (let j = index - 1; j >= 0; j--) {
9442
+ const candidate = macros[j];
9443
+ if (!candidate.open || candidate.pair === -1 || candidate.pair < index) continue;
9444
+ const name = candidate.name.toLowerCase();
9445
+ if (DEFERRED_CONTAINERS.has(name)) return -1;
9446
+ if (name !== "for") continue;
9447
+ const loopPassage = workspace.passages.getPassageAt(uri, candidate.range.start.line);
9448
+ const macroPassage = workspace.passages.getPassageAt(uri, macros[index].range.start.line);
9449
+ return loopPassage === macroPassage ? j : -1;
9450
+ }
9451
+ return -1;
9452
+ }
9453
+ function positionToOffset(pos, lineStarts) {
9454
+ return (lineStarts[pos.line] ?? 0) + pos.character;
9455
+ }
9456
+ function sourceArgs(macro, text, lineStarts) {
9457
+ const end = positionToOffset(macro.range.end, lineStarts) - 1;
9458
+ const start = end - (macro.rawArgs?.length ?? 0);
9459
+ return { args: text.slice(start, end), start };
9460
+ }
9461
+ function validateLoopTemporaries(uri, text, macros, workspace, diagnostics) {
9462
+ const lineStarts = buildLineStarts(text);
9463
+ for (let k = 0; k < macros.length; k++) {
9464
+ const macro = macros[k];
9465
+ if (!macro.open || !macro.rawArgs) continue;
9466
+ const name = macro.name.toLowerCase();
9467
+ if (name !== "computed" && name !== "set") continue;
9468
+ const { args: args2, start: argsStart } = sourceArgs(macro, text, lineStarts);
9469
+ const targets = name === "computed" ? computedTemporaryTarget(args2) : setTemporaryTargets(args2);
9470
+ if (targets.length === 0) continue;
9471
+ const loopIndex = enclosingLoop(macros, k, uri, workspace);
9472
+ if (loopIndex === -1) continue;
9473
+ const loop = macros[loopIndex];
9474
+ const bodyStart = positionToOffset(loop.range.end, lineStarts);
9475
+ const bodyEnd = positionToOffset(macros[loop.pair].range.start, lineStarts);
9476
+ const macroStart = positionToOffset(macro.range.start, lineStarts);
9477
+ const macroEnd = positionToOffset(macro.range.end, lineStarts);
9478
+ const body = text.slice(bodyStart, bodyEnd);
9479
+ for (const target of targets) {
9480
+ if (!isReadInBody(body, bodyStart, target.name, macroStart, macroEnd)) continue;
9481
+ const start = argsStart + target.offset;
9482
+ diagnostics.push(makeDiag(
9483
+ {
9484
+ start: offsetToPosition(start, lineStarts),
9485
+ end: offsetToPosition(start + 1 + target.name.length, lineStarts)
9486
+ },
9487
+ DiagnosticCode.TemporaryAssignedInLoop,
9488
+ `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.`
9489
+ ));
9490
+ }
9491
+ }
9492
+ }
9493
+ function isReadInBody(body, bodyOffset, name, macroStart, macroEnd) {
9494
+ const re = temporaryRefRegex2(name);
9495
+ let m;
9496
+ while ((m = re.exec(body)) !== null) {
9497
+ const offset = bodyOffset + m.index;
9498
+ if (offset >= macroStart && offset < macroEnd) continue;
9499
+ const after = body.slice(m.index + m[0].length);
9500
+ if (/^\s*=(?![=>])/.test(after)) continue;
9501
+ return true;
9502
+ }
9503
+ return false;
9504
+ }
9505
+ function resolveIncludeTarget2(rawArgs) {
9506
+ const expr = includeExpression(rawArgs);
9507
+ if (expr === "") return null;
9508
+ const literal2 = /^(["'`])((?:\\.|(?!\1)[^\\])*)\1$/s.exec(expr);
9509
+ if (literal2) {
9510
+ if (literal2[1] === "`" && literal2[2].includes("${")) return null;
9511
+ return literal2[2].replace(/\\(.)/g, "$1");
9512
+ }
9513
+ if (/[$@%"'`(]/.test(expr) || temporaryRefRegex2().test(expr)) return null;
9514
+ if (EXPRESSION_BUILTINS2.has(expr)) return null;
9515
+ return expr;
9516
+ }
9517
+ function widgetDefinitionNames(content) {
9518
+ const names = [];
9519
+ const re = /\{widget\s+(["']?)([^\s"'}]+)\1/gi;
9520
+ let m;
9521
+ while ((m = re.exec(content)) !== null) names.push(m[2]);
9522
+ return names;
9523
+ }
9524
+ function passageContent(passage, workspace) {
9525
+ const text = workspace.documents.getText(passage.uri) ?? "";
9526
+ return text.split("\n").slice(passage.range.start.line + 1, passage.range.end.line + 1).join("\n");
9527
+ }
9528
+ function validateWidgetIncludes(text, macros, workspace, diagnostics) {
9529
+ let lineStarts = null;
9530
+ for (const macro of macros) {
9531
+ if (!macro.open || !macro.rawArgs || macro.name.toLowerCase() !== "include") continue;
9532
+ lineStarts ??= buildLineStarts(text);
9533
+ const target = resolveIncludeTarget2(sourceArgs(macro, text, lineStarts).args);
9534
+ if (target === null) continue;
9535
+ const passage = workspace.passages.getPassage(target);
9536
+ if (!passage?.tags?.includes("widget")) continue;
9537
+ const widgets = widgetDefinitionNames(passageContent(passage, workspace));
9538
+ if (widgets.length === 0) continue;
9539
+ const list = widgets.map((w) => `{${w}}`).join(", ");
9540
+ diagnostics.push(makeDiag(
9541
+ macro.range,
9542
+ DiagnosticCode.IncludeWidgetPassage,
9543
+ `{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]}}.`
9544
+ ));
9545
+ }
9546
+ }
9547
+ function validateUnusedWidgets(uri, workspace, diagnostics) {
9548
+ for (const widget of workspace.widgets.getAllWidgets()) {
9549
+ if (widget.uri !== uri) continue;
9550
+ if (workspace.widgets.isInvoked(widget.name)) continue;
9551
+ diagnostics.push(makeDiag(
9552
+ widget.range,
9553
+ DiagnosticCode.UnusedWidget,
9554
+ `Widget "${widget.name}" is defined but never invoked in the workspace`
9555
+ ));
9556
+ }
9557
+ }
8728
9558
  function validateLineLength(text, passages, maxLength, diagnostics) {
8729
- const excludedTags = /* @__PURE__ */ new Set(["script", "stylesheet"]);
8730
- const excludedPassages = new Set(passages.filter((p) => p.tags?.some((t) => excludedTags.has(t))).map((p) => p.name));
9559
+ const excludedLines = /* @__PURE__ */ new Set();
9560
+ for (const passage of passages.filter(isScriptOrStylesheetPassage)) {
9561
+ for (let i = passage.range.start.line; i <= passage.range.end.line; i++) {
9562
+ excludedLines.add(i);
9563
+ }
9564
+ }
8731
9565
  const lines = text.split("\n");
8732
9566
  for (let i = 0; i < lines.length; i++) {
9567
+ if (excludedLines.has(i)) continue;
8733
9568
  const line = lines[i];
8734
9569
  if (/^::\s+/.test(line)) continue;
8735
9570
  if (/^\s*<[a-zA-Z]/.test(line)) continue;
@@ -8772,7 +9607,7 @@ function toLspDiagnostic(d) {
8772
9607
  message: d.message
8773
9608
  };
8774
9609
  }
8775
- var diagnosticsPlugin;
9610
+ var DEFERRED_CONTAINERS, EXPRESSION_BUILTINS2, diagnosticsPlugin;
8776
9611
  var init_diagnostics = __esm({
8777
9612
  "src/plugins/diagnostics.ts"() {
8778
9613
  "use strict";
@@ -8781,26 +9616,42 @@ var init_diagnostics = __esm({
8781
9616
  init_argument_lexer();
8782
9617
  init_parameter_validator();
8783
9618
  init_link_parser();
9619
+ init_passage_parser();
9620
+ DEFERRED_CONTAINERS = /* @__PURE__ */ new Set(["link", "button"]);
9621
+ EXPRESSION_BUILTINS2 = /* @__PURE__ */ new Set([
9622
+ "currentPassage",
9623
+ "previousPassage",
9624
+ "visited",
9625
+ "hasVisited",
9626
+ "hasVisitedAny",
9627
+ "hasVisitedAll",
9628
+ "rendered",
9629
+ "hasRendered",
9630
+ "hasRenderedAny",
9631
+ "hasRenderedAll",
9632
+ "random",
9633
+ "randomInt"
9634
+ ]);
8784
9635
  diagnosticsPlugin = {
8785
9636
  id: "diagnostics",
8786
9637
  capabilities: {},
8787
9638
  initialize(ctx) {
8788
- const publishDiagnostics = () => {
8789
- for (const uri of ctx.workspace.documents.getUris()) {
8790
- const diags = computeDiagnostics(uri, ctx.workspace);
8791
- ctx.connection.sendDiagnostics({
8792
- uri,
8793
- diagnostics: diags.map((d) => toLspDiagnostic(d))
8794
- });
8795
- }
8796
- };
8797
- ctx.workspace.on("modelReady", publishDiagnostics);
8798
- ctx.workspace.on("documentChanged", (uri) => {
9639
+ const isEnabled = (d) => ctx.config.diagnostics?.[d.code] !== false;
9640
+ const publishFor = (uri) => {
8799
9641
  const diags = computeDiagnostics(uri, ctx.workspace);
8800
9642
  ctx.connection.sendDiagnostics({
8801
9643
  uri,
8802
- diagnostics: diags.map((d) => toLspDiagnostic(d))
9644
+ diagnostics: diags.filter(isEnabled).map((d) => toLspDiagnostic(d))
8803
9645
  });
9646
+ };
9647
+ ctx.workspace.on("modelReady", () => {
9648
+ for (const uri of ctx.workspace.documents.getUris()) {
9649
+ publishFor(uri);
9650
+ }
9651
+ });
9652
+ ctx.workspace.on("documentChanged", publishFor);
9653
+ ctx.workspace.on("documentClosed", (uri) => {
9654
+ ctx.connection.sendDiagnostics({ uri, diagnostics: [] });
8804
9655
  });
8805
9656
  }
8806
9657
  };
@@ -8820,9 +9671,9 @@ var require_identity = __commonJS({
8820
9671
  var NODE_TYPE = Symbol.for("yaml.node.type");
8821
9672
  var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
8822
9673
  var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
8823
- var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
9674
+ var isMap2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
8824
9675
  var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
8825
- var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
9676
+ var isScalar2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
8826
9677
  var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
8827
9678
  function isCollection(node) {
8828
9679
  if (node && typeof node === "object")
@@ -8844,7 +9695,7 @@ var require_identity = __commonJS({
8844
9695
  }
8845
9696
  return false;
8846
9697
  }
8847
- var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
9698
+ var hasAnchor = (node) => (isScalar2(node) || isCollection(node)) && !!node.anchor;
8848
9699
  exports.ALIAS = ALIAS;
8849
9700
  exports.DOC = DOC;
8850
9701
  exports.MAP = MAP;
@@ -8856,10 +9707,10 @@ var require_identity = __commonJS({
8856
9707
  exports.isAlias = isAlias;
8857
9708
  exports.isCollection = isCollection;
8858
9709
  exports.isDocument = isDocument;
8859
- exports.isMap = isMap;
9710
+ exports.isMap = isMap2;
8860
9711
  exports.isNode = isNode;
8861
9712
  exports.isPair = isPair;
8862
- exports.isScalar = isScalar;
9713
+ exports.isScalar = isScalar2;
8863
9714
  exports.isSeq = isSeq;
8864
9715
  }
8865
9716
  });
@@ -12961,9 +13812,9 @@ var require_resolve_flow_collection = __commonJS({
12961
13812
  var blockMsg = "Block collections are not allowed within flow collections";
12962
13813
  var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq");
12963
13814
  function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
12964
- const isMap = fc.start.source === "{";
12965
- const fcName = isMap ? "flow map" : "flow sequence";
12966
- const NodeClass = tag?.nodeClass ?? (isMap ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
13815
+ const isMap2 = fc.start.source === "{";
13816
+ const fcName = isMap2 ? "flow map" : "flow sequence";
13817
+ const NodeClass = tag?.nodeClass ?? (isMap2 ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
12967
13818
  const coll = new NodeClass(ctx.schema);
12968
13819
  coll.flow = true;
12969
13820
  const atRoot = ctx.atRoot;
@@ -12999,7 +13850,7 @@ var require_resolve_flow_collection = __commonJS({
12999
13850
  offset = props.end;
13000
13851
  continue;
13001
13852
  }
13002
- if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key))
13853
+ if (!isMap2 && ctx.options.strict && utilContainsNewline.containsNewline(key))
13003
13854
  onError(
13004
13855
  key,
13005
13856
  // checked by containsNewline()
@@ -13039,7 +13890,7 @@ var require_resolve_flow_collection = __commonJS({
13039
13890
  }
13040
13891
  }
13041
13892
  }
13042
- if (!isMap && !sep2 && !props.found) {
13893
+ if (!isMap2 && !sep2 && !props.found) {
13043
13894
  const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
13044
13895
  coll.items.push(valueNode);
13045
13896
  offset = valueNode.range[2];
@@ -13062,7 +13913,7 @@ var require_resolve_flow_collection = __commonJS({
13062
13913
  startOnNewline: false
13063
13914
  });
13064
13915
  if (valueProps.found) {
13065
- if (!isMap && !props.found && ctx.options.strict) {
13916
+ if (!isMap2 && !props.found && ctx.options.strict) {
13066
13917
  if (sep2)
13067
13918
  for (const st of sep2) {
13068
13919
  if (st === valueProps.found)
@@ -13094,7 +13945,7 @@ var require_resolve_flow_collection = __commonJS({
13094
13945
  const pair = new Pair.Pair(keyNode, valueNode);
13095
13946
  if (ctx.options.keepSourceTokens)
13096
13947
  pair.srcToken = collItem;
13097
- if (isMap) {
13948
+ if (isMap2) {
13098
13949
  const map2 = coll;
13099
13950
  if (utilMapIncludes.mapIncludes(ctx, map2.items, keyNode))
13100
13951
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
@@ -13110,7 +13961,7 @@ var require_resolve_flow_collection = __commonJS({
13110
13961
  offset = valueNode ? valueNode.range[2] : valueProps.end;
13111
13962
  }
13112
13963
  }
13113
- const expectedEnd = isMap ? "}" : "]";
13964
+ const expectedEnd = isMap2 ? "}" : "]";
13114
13965
  const [ce, ...ee] = fc.end;
13115
13966
  let cePos = offset;
13116
13967
  if (ce?.source === expectedEnd)
@@ -14393,7 +15244,7 @@ var require_cst = __commonJS({
14393
15244
  var FLOW_END = "";
14394
15245
  var SCALAR = "";
14395
15246
  var isCollection = (token) => !!token && "items" in token;
14396
- var isScalar = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar");
15247
+ var isScalar2 = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar");
14397
15248
  function prettyToken(token) {
14398
15249
  switch (token) {
14399
15250
  case BOM:
@@ -14477,7 +15328,7 @@ var require_cst = __commonJS({
14477
15328
  exports.FLOW_END = FLOW_END;
14478
15329
  exports.SCALAR = SCALAR;
14479
15330
  exports.isCollection = isCollection;
14480
- exports.isScalar = isScalar;
15331
+ exports.isScalar = isScalar2;
14481
15332
  exports.prettyToken = prettyToken;
14482
15333
  exports.tokenType = tokenType;
14483
15334
  }
@@ -15990,7 +16841,7 @@ var require_public_api = __commonJS({
15990
16841
  return docs;
15991
16842
  return Object.assign([], { empty: true }, composer$1.streamInfo());
15992
16843
  }
15993
- function parseDocument(source, options = {}) {
16844
+ function parseDocument2(source, options = {}) {
15994
16845
  const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
15995
16846
  const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
15996
16847
  const composer$1 = new composer.Composer(options);
@@ -16016,7 +16867,7 @@ var require_public_api = __commonJS({
16016
16867
  } else if (options === void 0 && reviver && typeof reviver === "object") {
16017
16868
  options = reviver;
16018
16869
  }
16019
- const doc = parseDocument(src, options);
16870
+ const doc = parseDocument2(src, options);
16020
16871
  if (!doc)
16021
16872
  return null;
16022
16873
  doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
@@ -16052,7 +16903,7 @@ var require_public_api = __commonJS({
16052
16903
  }
16053
16904
  exports.parse = parse3;
16054
16905
  exports.parseAllDocuments = parseAllDocuments;
16055
- exports.parseDocument = parseDocument;
16906
+ exports.parseDocument = parseDocument2;
16056
16907
  exports.stringify = stringify;
16057
16908
  }
16058
16909
  });
@@ -16142,6 +16993,22 @@ function findConfigFile(workspaceRoot) {
16142
16993
  }
16143
16994
  return null;
16144
16995
  }
16996
+ function loadConfigFile(configPath) {
16997
+ let content;
16998
+ try {
16999
+ content = readFileSync3(configPath, "utf-8");
17000
+ } catch (err) {
17001
+ const reason = err?.code === "ENOENT" ? "file not found" : err instanceof Error ? err.message : String(err);
17002
+ throw new Error(`Cannot read config file '${configPath}': ${reason}`);
17003
+ }
17004
+ const format = configPath.toLowerCase().endsWith(".json") ? "json" : "yaml";
17005
+ try {
17006
+ return parseConfig(content, format);
17007
+ } catch (err) {
17008
+ const reason = err instanceof Error ? err.message : String(err);
17009
+ throw new Error(`Invalid ${format.toUpperCase()} in config file '${configPath}': ${reason}`);
17010
+ }
17011
+ }
16145
17012
  function loadConfigFromDisk(workspaceRoot) {
16146
17013
  const configPath = findConfigFile(workspaceRoot);
16147
17014
  if (!configPath) {
@@ -16409,6 +17276,15 @@ function filterBySeverity(diagnostics, minSeverity) {
16409
17276
  async function runCheck(args2) {
16410
17277
  const options = parseArgs(args2);
16411
17278
  const cwd = process.cwd();
17279
+ let explicitConfig = null;
17280
+ if (options.configPath) {
17281
+ try {
17282
+ explicitConfig = loadConfigFile(resolve(cwd, options.configPath));
17283
+ } catch (err) {
17284
+ console.error(`spindle-lsp check: ${err instanceof Error ? err.message : String(err)}`);
17285
+ return 2;
17286
+ }
17287
+ }
16412
17288
  const files = [];
16413
17289
  for (const pattern of options.patterns) {
16414
17290
  const matches = await glob(pattern, {
@@ -16429,12 +17305,12 @@ async function runCheck(args2) {
16429
17305
  }
16430
17306
  return 0;
16431
17307
  }
16432
- let configRoot;
16433
- if (options.configPath) {
16434
- configRoot = resolve(cwd, options.configPath, "..");
17308
+ let projectConfig;
17309
+ if (explicitConfig) {
17310
+ projectConfig = explicitConfig;
16435
17311
  } else {
16436
17312
  const dirs = uniqueFiles.map((f) => resolve(f, ".."));
16437
- configRoot = dirs.reduce((a, b) => {
17313
+ let configRoot = dirs.reduce((a, b) => {
16438
17314
  while (!b.startsWith(a)) a = resolve(a, "..");
16439
17315
  return a;
16440
17316
  });
@@ -16448,8 +17324,8 @@ async function runCheck(args2) {
16448
17324
  if (parent === search) break;
16449
17325
  search = parent;
16450
17326
  }
17327
+ projectConfig = loadConfigFromDisk(configRoot);
16451
17328
  }
16452
- const projectConfig = loadConfigFromDisk(configRoot);
16453
17329
  const workspace = new WorkspaceModel();
16454
17330
  const fileContents = /* @__PURE__ */ new Map();
16455
17331
  for (const filePath of uniqueFiles) {
@@ -16462,7 +17338,7 @@ async function runCheck(args2) {
16462
17338
  }
16463
17339
  workspace.initialize(fileContents);
16464
17340
  if (Object.keys(projectConfig.macros).length > 0) {
16465
- workspace.macros.loadSupplements(projectConfig.macros);
17341
+ workspace.macros.loadConfig(projectConfig.macros);
16466
17342
  }
16467
17343
  const diagOptions = {};
16468
17344
  if (options.maxLineLength !== null) {
@@ -16549,7 +17425,7 @@ function splitPassages(text) {
16549
17425
  function classifyPassage(header) {
16550
17426
  const tagMatch = header.match(/\[([^\]]*)\]/g);
16551
17427
  if (!tagMatch) return "normal";
16552
- const tags = tagMatch.map((t) => t.slice(1, -1).trim().toLowerCase());
17428
+ const tags = tagMatch.flatMap((t) => t.slice(1, -1).toLowerCase().split(/\s+/));
16553
17429
  if (tags.includes("script")) return "script";
16554
17430
  if (tags.includes("stylesheet")) return "stylesheet";
16555
17431
  return "normal";
@@ -16568,6 +17444,10 @@ function segmentRegions(body) {
16568
17444
  if (/^<script(\s|>)/i.test(trimmed)) {
16569
17445
  const scriptLines = [line];
16570
17446
  i++;
17447
+ if (/<\/script\s*>/i.test(line)) {
17448
+ regions.push({ type: "script", lines: scriptLines });
17449
+ continue;
17450
+ }
16571
17451
  while (i < lines.length && !/<\/script>/i.test(lines[i])) {
16572
17452
  scriptLines.push(lines[i]);
16573
17453
  i++;
@@ -16792,7 +17672,7 @@ function replaceSvgBlocks(html) {
16792
17672
  function restoreSvgBlocks(text, tokens) {
16793
17673
  let result = text;
16794
17674
  for (let i = 0; i < tokens.length; i++) {
16795
- result = result.replace(`<!--SVG:${i}-->`, tokens[i]);
17675
+ result = result.replace(`<!--SVG:${i}-->`, () => tokens[i]);
16796
17676
  }
16797
17677
  return result;
16798
17678
  }
@@ -16840,8 +17720,8 @@ function replaceSpindleTokens(html) {
16840
17720
  function restoreSpindleTokens(text, tokens) {
16841
17721
  let result = text;
16842
17722
  for (let i = 0; i < tokens.length; i++) {
16843
- result = result.replace(`<!--SP:${i}-->`, tokens[i]);
16844
- result = result.replace(`__SP${i}__`, tokens[i]);
17723
+ result = result.replace(`<!--SP:${i}-->`, () => tokens[i]);
17724
+ result = result.replace(`__SP${i}__`, () => tokens[i]);
16845
17725
  }
16846
17726
  return result;
16847
17727
  }
@@ -16884,6 +17764,7 @@ async function formatDocument(text, options) {
16884
17764
  const isDedenting = options?.isDedentingSubMacro ?? ((name) => DEFAULT_DEDENTING.has(name.toLowerCase()));
16885
17765
  const passages = splitPassages(text);
16886
17766
  const resultLines = [];
17767
+ const hardBreakLines = /* @__PURE__ */ new Set();
16887
17768
  for (let pi = 0; pi < passages.length; pi++) {
16888
17769
  const passage = passages[pi];
16889
17770
  if (pi > 0 && passage.header) {
@@ -16912,6 +17793,10 @@ async function formatDocument(text, options) {
16912
17793
  const regions = segmentRegions(passage.body);
16913
17794
  for (const region of regions) {
16914
17795
  if (region.type === "script") {
17796
+ if (region.lines.length === 1) {
17797
+ resultLines.push(region.lines[0]);
17798
+ continue;
17799
+ }
16915
17800
  const firstLine = region.lines[0];
16916
17801
  const lastLine = region.lines[region.lines.length - 1];
16917
17802
  const innerLines = region.lines.slice(1, region.lines.length - 1);
@@ -16946,49 +17831,92 @@ async function formatDocument(text, options) {
16946
17831
  if (options?.maxLineLength) {
16947
17832
  indented = wrapLines(indented, options.maxLineLength);
16948
17833
  }
16949
- resultLines.push(...indented);
17834
+ for (const line of indented) {
17835
+ if (line.endsWith(" ")) hardBreakLines.add(resultLines.length);
17836
+ resultLines.push(line);
17837
+ }
16950
17838
  }
16951
17839
  }
16952
- let output = resultLines.map((l) => l.replace(/\s+$/, "")).join("\n");
17840
+ let output = resultLines.map((l, i) => hardBreakLines.has(i) ? l : l.replace(/\s+$/, "")).join("\n");
16953
17841
  output = output.replace(/\n*$/, "\n");
16954
17842
  return output;
16955
17843
  }
16956
17844
  function indentMacros(lines, isBlock, isDedenting) {
16957
- const result = [];
16958
- let indentLevel = 0;
16959
- for (const rawLine of lines) {
16960
- const line = rawLine.replace(/\s+$/, "");
16961
- const trimmed = line.trim();
17845
+ const bodies = [{ openLine: -1, minCol: Infinity, keepRelative: false }];
17846
+ const stack = [0];
17847
+ const info = [];
17848
+ const openBody = (line) => {
17849
+ bodies.push({ openLine: line, minCol: Infinity, keepRelative: false });
17850
+ stack.push(bodies.length - 1);
17851
+ };
17852
+ const closeBody = () => stack.length > 1 ? stack.pop() : void 0;
17853
+ const addLine = (col, text) => {
17854
+ const body = bodies[stack[stack.length - 1]];
17855
+ body.minCol = Math.min(body.minCol, col);
17856
+ if (LIST_ITEM_REGEX.test(text) || CODE_FENCE_REGEX.test(text)) body.keepRelative = true;
17857
+ info.push({ body: stack[stack.length - 1], col, text });
17858
+ };
17859
+ for (let i = 0; i < lines.length; i++) {
17860
+ const trimmed = lines[i].trim();
16962
17861
  if (trimmed === "") {
16963
- result.push("");
17862
+ info.push(null);
16964
17863
  continue;
16965
17864
  }
17865
+ const col = indentColumn(lines[i]);
16966
17866
  const dedentMatch = trimmed.match(MACRO_OPEN_REGEX);
16967
17867
  if (dedentMatch && isDedenting(dedentMatch[1])) {
16968
- indentLevel = Math.max(0, indentLevel - 1);
16969
- result.push(indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed);
16970
- indentLevel++;
17868
+ const closed2 = closeBody();
17869
+ if (closed2 === void 0) addLine(col, trimmed);
17870
+ else info.push({ body: stack[stack.length - 1], closes: closed2, col, text: trimmed });
17871
+ openBody(i);
16971
17872
  continue;
16972
17873
  }
16973
- const closeMatch = trimmed.match(MACRO_CLOSE_REGEX);
16974
- if (closeMatch) {
16975
- indentLevel = Math.max(0, indentLevel - 1);
16976
- }
16977
- result.push(indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed);
17874
+ const closed = MACRO_CLOSE_REGEX.test(trimmed) ? closeBody() : void 0;
17875
+ if (closed === void 0) addLine(col, trimmed);
17876
+ else info.push({ body: stack[stack.length - 1], closes: closed, col, text: trimmed });
16978
17877
  const openMatch = trimmed.match(MACRO_OPEN_REGEX);
16979
17878
  if (openMatch && isBlock(openMatch[1])) {
16980
- indentLevel++;
17879
+ openBody(i);
16981
17880
  }
16982
17881
  const closeGlobal = /\{\/[A-Za-z][\w-]*\s*\}/g;
16983
17882
  let cm;
16984
17883
  while ((cm = closeGlobal.exec(trimmed)) !== null) {
16985
- if (cm.index > 0) {
16986
- indentLevel = Math.max(0, indentLevel - 1);
16987
- }
17884
+ if (cm.index > 0) closeBody();
17885
+ }
17886
+ }
17887
+ const outCol = [];
17888
+ const result = [];
17889
+ for (let i = 0; i < info.length; i++) {
17890
+ const line = info[i];
17891
+ if (!line) {
17892
+ outCol.push(0);
17893
+ result.push("");
17894
+ continue;
17895
+ }
17896
+ let col;
17897
+ if (line.closes !== void 0) {
17898
+ col = outCol[bodies[line.closes].openLine];
17899
+ } else {
17900
+ const body = bodies[line.body];
17901
+ const isRoot = body.openLine === -1;
17902
+ const base = isRoot ? 0 : outCol[body.openLine] + 2;
17903
+ col = base + (body.keepRelative ? line.col - (isRoot ? 0 : body.minCol) : 0);
16988
17904
  }
17905
+ outCol.push(col);
17906
+ const hardBreak = / {2,}$/.test(lines[i]) && info[i + 1] ? " " : "";
17907
+ result.push(" ".repeat(col) + line.text + hardBreak);
16989
17908
  }
16990
17909
  return result;
16991
17910
  }
17911
+ function indentColumn(line) {
17912
+ let col = 0;
17913
+ for (const ch of line) {
17914
+ if (ch === " ") col++;
17915
+ else if (ch === " ") col += 4 - col % 4;
17916
+ else break;
17917
+ }
17918
+ return col;
17919
+ }
16992
17920
  async function formatRange(text, _range, options) {
16993
17921
  return formatDocument(text, options);
16994
17922
  }
@@ -17010,6 +17938,7 @@ function wrapLines(lines, maxLen) {
17010
17938
  const indentMatch = line.match(/^(\s*)/);
17011
17939
  const indent = indentMatch ? indentMatch[1] : "";
17012
17940
  const wrapped = wordWrap(trimmed, maxLen, indent);
17941
+ if (line.endsWith(" ")) wrapped[wrapped.length - 1] += " ";
17013
17942
  result.push(...wrapped);
17014
17943
  }
17015
17944
  return result;
@@ -17070,7 +17999,7 @@ function normalizePassageHeader(line) {
17070
17999
  }
17071
18000
  return `:: ${name}${suffix}`;
17072
18001
  }
17073
- var PASSAGE_HEADER_REGEX2, MACRO_OPEN_REGEX, MACRO_CLOSE_REGEX, DEFAULT_DEDENTING, formatPlugin;
18002
+ var PASSAGE_HEADER_REGEX2, MACRO_OPEN_REGEX, MACRO_CLOSE_REGEX, DEFAULT_DEDENTING, LIST_ITEM_REGEX, CODE_FENCE_REGEX, formatPlugin;
17074
18003
  var init_format = __esm({
17075
18004
  "src/plugins/format.ts"() {
17076
18005
  "use strict";
@@ -17082,6 +18011,8 @@ var init_format = __esm({
17082
18011
  MACRO_OPEN_REGEX = /^\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\b/;
17083
18012
  MACRO_CLOSE_REGEX = /^\{\/([A-Za-z][\w-]*)\b/;
17084
18013
  DEFAULT_DEDENTING = /* @__PURE__ */ new Set(["else", "elseif", "next", "case", "default"]);
18014
+ LIST_ITEM_REGEX = /^(?:[-+*]|\d{1,9}[.)])(?:[ \t]|$)/;
18015
+ CODE_FENCE_REGEX = /^(?:`{3,}|~{3,})/;
17085
18016
  formatPlugin = {
17086
18017
  id: "format",
17087
18018
  capabilities: {
@@ -21275,7 +22206,7 @@ __export(util_exports, {
21275
22206
  createTransparentProxy: () => createTransparentProxy,
21276
22207
  defineLazy: () => defineLazy,
21277
22208
  esc: () => esc,
21278
- escapeRegex: () => escapeRegex,
22209
+ escapeRegex: () => escapeRegex2,
21279
22210
  extend: () => extend,
21280
22211
  finalizeIssue: () => finalizeIssue,
21281
22212
  floatSafeRemainder: () => floatSafeRemainder2,
@@ -21487,7 +22418,7 @@ function numKeys(data) {
21487
22418
  }
21488
22419
  return keyCount;
21489
22420
  }
21490
- function escapeRegex(str) {
22421
+ function escapeRegex2(str) {
21491
22422
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21492
22423
  }
21493
22424
  function clone(inst, def, params) {
@@ -22293,7 +23224,7 @@ var init_regexes = __esm({
22293
23224
  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
23225
  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
23226
  mac = (delimiter) => {
22296
- const escapedDelim = escapeRegex(delimiter ?? ":");
23227
+ const escapedDelim = escapeRegex2(delimiter ?? ":");
22297
23228
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
22298
23229
  };
22299
23230
  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 +23714,7 @@ var init_checks = __esm({
22783
23714
  });
22784
23715
  $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
22785
23716
  $ZodCheck.init(inst, def);
22786
- const escapedRegex = escapeRegex(def.includes);
23717
+ const escapedRegex = escapeRegex2(def.includes);
22787
23718
  const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
22788
23719
  def.pattern = pattern;
22789
23720
  inst._zod.onattach.push((inst2) => {
@@ -22807,7 +23738,7 @@ var init_checks = __esm({
22807
23738
  });
22808
23739
  $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
22809
23740
  $ZodCheck.init(inst, def);
22810
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
23741
+ const pattern = new RegExp(`^${escapeRegex2(def.prefix)}.*`);
22811
23742
  def.pattern ?? (def.pattern = pattern);
22812
23743
  inst._zod.onattach.push((inst2) => {
22813
23744
  const bag = inst2._zod.bag;
@@ -22830,7 +23761,7 @@ var init_checks = __esm({
22830
23761
  });
22831
23762
  $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
22832
23763
  $ZodCheck.init(inst, def);
22833
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
23764
+ const pattern = new RegExp(`.*${escapeRegex2(def.suffix)}$`);
22834
23765
  def.pattern ?? (def.pattern = pattern);
22835
23766
  inst._zod.onattach.push((inst2) => {
22836
23767
  const bag = inst2._zod.bag;
@@ -24468,7 +25399,7 @@ var init_schemas = __esm({
24468
25399
  const values = getEnumValues(def.entries);
24469
25400
  const valuesSet = new Set(values);
24470
25401
  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("|")})$`);
25402
+ inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex2(o) : o.toString()).join("|")})$`);
24472
25403
  inst._zod.parse = (payload, _ctx) => {
24473
25404
  const input = payload.value;
24474
25405
  if (valuesSet.has(input)) {
@@ -24490,7 +25421,7 @@ var init_schemas = __esm({
24490
25421
  }
24491
25422
  const values = new Set(def.values);
24492
25423
  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("|")})$`);
25424
+ inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex2(o) : o ? escapeRegex2(o.toString()) : String(o)).join("|")})$`);
24494
25425
  inst._zod.parse = (payload, _ctx) => {
24495
25426
  const input = payload.value;
24496
25427
  if (values.has(input)) {
@@ -24785,7 +25716,7 @@ var init_schemas = __esm({
24785
25716
  const end = source.endsWith("$") ? source.length - 1 : source.length;
24786
25717
  regexParts.push(source.slice(start, end));
24787
25718
  } else if (part === null || primitiveTypes.has(typeof part)) {
24788
- regexParts.push(escapeRegex(`${part}`));
25719
+ regexParts.push(escapeRegex2(`${part}`));
24789
25720
  } else {
24790
25721
  throw new Error(`Invalid template literal part: ${part}`);
24791
25722
  }
@@ -48422,7 +49353,7 @@ function createWorkspace(files) {
48422
49353
  }
48423
49354
  workspace.initialize(fileContents);
48424
49355
  if (Object.keys(projectConfig.macros).length > 0) {
48425
- workspace.macros.loadSupplements(projectConfig.macros);
49356
+ workspace.macros.loadConfig(projectConfig.macros);
48426
49357
  }
48427
49358
  return workspace;
48428
49359
  }
@@ -57688,7 +58619,7 @@ function getCompletions(uri, position, triggerChar, workspace) {
57688
58619
  if (position.line >= lines.length) return [];
57689
58620
  const lineText = lines[position.line].substring(0, position.character);
57690
58621
  if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
57691
- return getClosingMacroCompletions(text, position, workspace);
58622
+ return getClosingMacroCompletions(uri, text, position, workspace);
57692
58623
  }
57693
58624
  const transientDotPathMatch = /%([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
57694
58625
  if (transientDotPathMatch) {
@@ -57718,11 +58649,18 @@ function getCompletions(uri, position, triggerChar, workspace) {
57718
58649
  }
57719
58650
  return [];
57720
58651
  }
57721
- function getClosingMacroCompletions(text, position, workspace) {
58652
+ function getClosingMacroCompletions(uri, text, position, workspace) {
58653
+ const passages = workspace.passages.getPassagesInDocument(uri);
57722
58654
  const macros = parseMacros(text);
57723
- pairMacros(macros, (name) => workspace.macros.isBlock(name));
58655
+ pairMacros(
58656
+ macros,
58657
+ (name) => workspace.macros.isBlock(name),
58658
+ passages.map((p) => p.range.start.line)
58659
+ );
58660
+ const passageStart = workspace.passages.getPassageAt(uri, position.line)?.range.start.line ?? 0;
57724
58661
  const openStack = [];
57725
58662
  for (const macro of macros) {
58663
+ if (macro.range.start.line < passageStart) continue;
57726
58664
  if (macro.range.start.line > position.line || macro.range.start.line === position.line && macro.range.start.character >= position.character) {
57727
58665
  break;
57728
58666
  }
@@ -57773,7 +58711,7 @@ function getMacroCompletions(workspace) {
57773
58711
  kind: 3,
57774
58712
  // CompletionItemKind.Function
57775
58713
  detail: `(widget) ${widget.name}`,
57776
- documentation: widget.params.length > 0 ? `Parameters: ${widget.params.map((p) => `@${p}`).join(", ")}` : void 0
58714
+ documentation: widget.params.length > 0 ? `Parameters: ${widget.params.join(", ")}` : void 0
57777
58715
  });
57778
58716
  }
57779
58717
  return items;
@@ -58048,7 +58986,7 @@ function getWidgetHover(line, position, workspace) {
58048
58986
  return null;
58049
58987
  }
58050
58988
  function buildWidgetHover(widget, line, nameStart, nameEnd) {
58051
- const sig = widget.params.length > 0 ? widget.params.map((p) => `@${p}`).join(", ") : "no parameters";
58989
+ const sig = widget.params.length > 0 ? widget.params.join(", ") : "no parameters";
58052
58990
  return {
58053
58991
  contents: `**Widget** \`${widget.name}\`
58054
58992
 
@@ -58122,7 +59060,7 @@ function getSignatureHelp(uri, position, workspace) {
58122
59060
  }
58123
59061
  const widget = workspace.widgets.getWidget(macroName);
58124
59062
  if (widget && widget.params.length > 0) {
58125
- const paramLabels = widget.params.map((p) => `@${p}`);
59063
+ const paramLabels = widget.params;
58126
59064
  return {
58127
59065
  signatures: [{
58128
59066
  label: `{${macroName} ${paramLabels.join(", ")}}`,
@@ -58178,53 +59116,21 @@ function getDefinition(uri, position, workspace) {
58178
59116
  const lines = text.split("\n");
58179
59117
  if (position.line >= lines.length) return null;
58180
59118
  const line = lines[position.line];
58181
- const linkResult = getPassageLinkDefinition(text, position, workspace);
58182
- if (linkResult) return linkResult;
58183
- const macroPassageResult = getMacroPassageDefinition(line, position, workspace);
58184
- if (macroPassageResult) return macroPassageResult;
59119
+ const passageResult = getPassageRefDefinition(text, position, workspace);
59120
+ if (passageResult) return passageResult;
58185
59121
  const widgetResult = getWidgetDefinition(line, position, workspace);
58186
59122
  if (widgetResult) return widgetResult;
58187
59123
  return null;
58188
59124
  }
58189
- function getPassageLinkDefinition(text, position, workspace) {
58190
- const links = parseLinks(text);
58191
- for (const link of links) {
58192
- if (link.range.start.line === position.line && position.character >= link.range.start.character && position.character <= link.range.end.character) {
58193
- const passage = workspace.passages.getPassage(link.name);
58194
- if (passage) {
58195
- return {
58196
- uri: passage.uri,
58197
- range: passage.headerEnd
58198
- };
58199
- }
58200
- }
58201
- }
58202
- return null;
58203
- }
58204
- function getMacroPassageDefinition(line, position, workspace) {
58205
- const patterns = [
58206
- /\{(?:goto|include)\s+"([^"]+)"\s*\}/gi,
58207
- /\{(?:link|button)\s+"[^"]*"\s+"([^"]+)"\s*\}/gi
58208
- ];
58209
- for (const pattern of patterns) {
58210
- pattern.lastIndex = 0;
58211
- let match2;
58212
- while ((match2 = pattern.exec(line)) !== null) {
58213
- const passageName = match2[1];
58214
- const nameStart = match2.index + match2[0].indexOf(passageName);
58215
- const nameEnd = nameStart + passageName.length;
58216
- if (position.character >= nameStart && position.character <= nameEnd) {
58217
- const passage = workspace.passages.getPassage(passageName);
58218
- if (passage) {
58219
- return {
58220
- uri: passage.uri,
58221
- range: passage.headerEnd
58222
- };
58223
- }
58224
- }
58225
- }
58226
- }
58227
- return null;
59125
+ function getPassageRefDefinition(text, position, workspace) {
59126
+ const ref = findPassageRefAt(text, position);
59127
+ if (!ref) return null;
59128
+ const passage = workspace.passages.getPassage(ref.name);
59129
+ if (!passage) return null;
59130
+ return {
59131
+ uri: passage.uri,
59132
+ range: passage.headerEnd
59133
+ };
58228
59134
  }
58229
59135
  function getWidgetDefinition(line, position, workspace) {
58230
59136
  const re = /\{([A-Za-z_$][\w$]*)/g;
@@ -58324,19 +59230,25 @@ function findReferences(uri, position, workspace, includeDeclaration) {
58324
59230
  }
58325
59231
  }
58326
59232
  {
58327
- const widgetRegex = /\{([A-Za-z_$][\w$]*)/g;
59233
+ const widgetRegex = /\{\/?([A-Za-z_$][\w$]*)/g;
58328
59234
  let match2;
58329
59235
  while ((match2 = widgetRegex.exec(line)) !== null) {
58330
59236
  const name = match2[1];
58331
- const nameStart = match2.index + 1;
59237
+ const nameStart = match2.index + match2[0].length - name.length;
58332
59238
  const nameEnd = nameStart + name.length;
58333
59239
  if (position.character >= nameStart && position.character <= nameEnd) {
58334
- if (!workspace.macros.getMacro(name) && workspace.widgets.getWidget(name)) {
59240
+ const widget = workspace.widgets.getWidget(name);
59241
+ const isClosing = match2[0][1] === "/";
59242
+ if (!workspace.macros.getMacro(name) && widget && (!isClosing || widget.block)) {
58335
59243
  return findWidgetReferences(name, workspace, includeDeclaration);
58336
59244
  }
58337
59245
  }
58338
59246
  }
58339
59247
  }
59248
+ const passageRef = findPassageRefAt(text, position);
59249
+ if (passageRef) {
59250
+ return findPassageReferences(passageRef.name, workspace, includeDeclaration);
59251
+ }
58340
59252
  {
58341
59253
  const allPassages = workspace.passages.getAllPassages();
58342
59254
  const passageNames = new Set(allPassages.map((p) => p.name));
@@ -58362,39 +59274,16 @@ function findPassageReferences(passageName, workspace, includeDeclaration) {
58362
59274
  if (passage) {
58363
59275
  locations.push({
58364
59276
  uri: passage.uri,
58365
- range: passage.headerEnd
59277
+ range: passage.nameRange
58366
59278
  });
58367
59279
  }
58368
59280
  }
58369
- const gotoRegex = /\{(?:goto|include)\s+"([^"]+)"\s*\}/gi;
58370
- const linkMacroRegex = /\{(?:link|button)\s+"[^"]*"\s+"([^"]+)"\s*\}/gi;
58371
59281
  for (const docUri of workspace.documents.getUris()) {
58372
59282
  const docText = workspace.documents.getText(docUri);
58373
59283
  if (!docText) continue;
58374
- const links = parseLinks(docText);
58375
- for (const link of links) {
58376
- if (link.name === passageName) {
58377
- locations.push({ uri: docUri, range: link.range });
58378
- }
58379
- }
58380
- const lines = docText.split("\n");
58381
- for (let lineNum = 0; lineNum < lines.length; lineNum++) {
58382
- const line = lines[lineNum];
58383
- for (const regex of [gotoRegex, linkMacroRegex]) {
58384
- regex.lastIndex = 0;
58385
- let match2;
58386
- while ((match2 = regex.exec(line)) !== null) {
58387
- if (match2[1] === passageName) {
58388
- const nameStart = match2.index + match2[0].indexOf(match2[1]);
58389
- locations.push({
58390
- uri: docUri,
58391
- range: {
58392
- start: { line: lineNum, character: nameStart },
58393
- end: { line: lineNum, character: nameStart + match2[1].length }
58394
- }
58395
- });
58396
- }
58397
- }
59284
+ for (const ref of [...parseLinks(docText), ...parseMacroPassageRefs(docText)]) {
59285
+ if (ref.name === passageName) {
59286
+ locations.push({ uri: docUri, range: ref.range });
58398
59287
  }
58399
59288
  }
58400
59289
  }
@@ -58433,30 +59322,30 @@ function findTransientReferences(varName, workspace, includeDeclaration) {
58433
59322
  }
58434
59323
  function findWidgetReferences(widgetName, workspace, includeDeclaration) {
58435
59324
  const locations = [];
58436
- if (includeDeclaration) {
58437
- const widget = workspace.widgets.getWidget(widgetName);
58438
- if (widget) {
58439
- locations.push({ uri: widget.uri, range: widget.range });
58440
- }
59325
+ const widget = workspace.widgets.getWidget(widgetName);
59326
+ if (includeDeclaration && widget) {
59327
+ locations.push({ uri: widget.uri, range: widget.range });
58441
59328
  }
58442
- const widgetInvocationRegex = /\{([A-Za-z_$][\w$]*)\b/g;
59329
+ const widgetInvocationRegex = /\{(\/)?([A-Za-z_$][\w$]*)\b/g;
59330
+ const lowerName = widgetName.toLowerCase();
59331
+ const isBlock = widget?.block ?? false;
58443
59332
  for (const docUri of workspace.documents.getUris()) {
58444
59333
  const docText = workspace.documents.getText(docUri);
58445
59334
  if (!docText) continue;
58446
59335
  const lines = docText.split("\n");
58447
59336
  for (let lineNum = 0; lineNum < lines.length; lineNum++) {
58448
59337
  const line = lines[lineNum];
58449
- if (/\{widget\s+"/i.test(line)) continue;
58450
59338
  widgetInvocationRegex.lastIndex = 0;
58451
59339
  let match2;
58452
59340
  while ((match2 = widgetInvocationRegex.exec(line)) !== null) {
58453
- if (match2[1] === widgetName) {
58454
- const nameStart = match2.index + 1;
59341
+ if (match2[1] && !isBlock) continue;
59342
+ if (match2[2].toLowerCase() === lowerName) {
59343
+ const nameStart = match2.index + 1 + (match2[1] ? 1 : 0);
58455
59344
  locations.push({
58456
59345
  uri: docUri,
58457
59346
  range: {
58458
59347
  start: { line: lineNum, character: nameStart },
58459
- end: { line: lineNum, character: nameStart + widgetName.length }
59348
+ end: { line: lineNum, character: nameStart + match2[2].length }
58460
59349
  }
58461
59350
  });
58462
59351
  }
@@ -58524,10 +59413,13 @@ function computeRename(uri, position, newName, workspace) {
58524
59413
  }
58525
59414
  case "variable": {
58526
59415
  const bareName = newName.startsWith("$") ? newName.slice(1) : newName.startsWith("%") ? newName.slice(1) : newName;
58527
- const isTransient = workspace.variables.getDeclaredTransient().has(symbol2.name);
58528
- const refs = isTransient ? findTransientReferences(symbol2.name, workspace, true) : findVariableReferences(symbol2.name, workspace, true);
59416
+ const refs = symbol2.sigil === "%" ? findTransientReferences(symbol2.name, workspace, true) : findVariableReferences(symbol2.name, workspace, true);
58529
59417
  for (const ref of refs) {
58530
- addEdit(ref.uri, ref.range, bareName);
59418
+ const start = ref.range.start.character + 1;
59419
+ addEdit(ref.uri, {
59420
+ start: { line: ref.range.start.line, character: start },
59421
+ end: { line: ref.range.start.line, character: start + symbol2.name.length }
59422
+ }, bareName);
58531
59423
  }
58532
59424
  break;
58533
59425
  }
@@ -58538,23 +59430,7 @@ function computeRename(uri, position, newName, workspace) {
58538
59430
  }
58539
59431
  const widget = workspace.widgets.getWidget(symbol2.name);
58540
59432
  if (widget) {
58541
- const docText = workspace.documents.getText(widget.uri);
58542
- if (docText) {
58543
- const lines = docText.split("\n");
58544
- const defLine = lines[widget.range.start.line];
58545
- if (defLine) {
58546
- const defSlice = defLine.slice(widget.range.start.character);
58547
- const nameMatch = /\{widget\s+"([^"]+)"/.exec(defSlice);
58548
- if (nameMatch) {
58549
- const nameOffsetInSlice = defSlice.indexOf(nameMatch[1]);
58550
- const nameStart = widget.range.start.character + nameOffsetInSlice;
58551
- addEdit(widget.uri, {
58552
- start: { line: widget.range.start.line, character: nameStart },
58553
- end: { line: widget.range.start.line, character: nameStart + symbol2.name.length }
58554
- }, newName);
58555
- }
58556
- }
58557
- }
59433
+ addEdit(widget.uri, widget.nameRange, newName);
58558
59434
  }
58559
59435
  break;
58560
59436
  }
@@ -58596,6 +59472,7 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58596
59472
  return {
58597
59473
  kind: "variable",
58598
59474
  name: match2[1],
59475
+ sigil: "$",
58599
59476
  range: {
58600
59477
  start: { line: position.line, character: start },
58601
59478
  end: { line: position.line, character: end }
@@ -58614,6 +59491,7 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58614
59491
  return {
58615
59492
  kind: "variable",
58616
59493
  name: match2[1],
59494
+ sigil: "%",
58617
59495
  range: {
58618
59496
  start: { line: position.line, character: start },
58619
59497
  end: { line: position.line, character: end }
@@ -58622,34 +59500,23 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58622
59500
  }
58623
59501
  }
58624
59502
  }
58625
- {
58626
- const widgetDefRegex2 = /\{widget\s+"([^"]+)"/gi;
58627
- let match2;
58628
- while ((match2 = widgetDefRegex2.exec(line)) !== null) {
58629
- const name = match2[1];
58630
- const nameStart = match2.index + match2[0].indexOf(name);
58631
- const nameEnd = nameStart + name.length;
58632
- if (position.character >= nameStart && position.character <= nameEnd) {
58633
- return {
58634
- kind: "widget",
58635
- name,
58636
- range: {
58637
- start: { line: position.line, character: nameStart },
58638
- end: { line: position.line, character: nameEnd }
58639
- }
58640
- };
58641
- }
59503
+ for (const widget of workspace.widgets.getAllWidgets()) {
59504
+ const { start, end } = widget.nameRange;
59505
+ if (widget.uri === uri && position.line === start.line && position.character >= start.character && position.character <= end.character) {
59506
+ return { kind: "widget", name: widget.name, range: widget.nameRange };
58642
59507
  }
58643
59508
  }
58644
59509
  {
58645
- const re = /\{([A-Za-z_$][\w$]*)/g;
59510
+ const re = /\{\/?([A-Za-z_$][\w$]*)/g;
58646
59511
  let match2;
58647
59512
  while ((match2 = re.exec(line)) !== null) {
58648
59513
  const name = match2[1];
58649
- const nameStart = match2.index + 1;
59514
+ const nameStart = match2.index + match2[0].length - name.length;
58650
59515
  const nameEnd = nameStart + name.length;
58651
59516
  if (position.character >= nameStart && position.character <= nameEnd) {
58652
- if (!workspace.macros.getMacro(name) && workspace.widgets.getWidget(name)) {
59517
+ const widget = workspace.widgets.getWidget(name);
59518
+ const isClosing = match2[0][1] === "/";
59519
+ if (!workspace.macros.getMacro(name) && widget && (!isClosing || widget.block)) {
58653
59520
  return {
58654
59521
  kind: "widget",
58655
59522
  name,
@@ -58743,7 +59610,7 @@ function computeCodeLenses(uri, workspace) {
58743
59610
  });
58744
59611
  continue;
58745
59612
  }
58746
- const widgetMatch = line.match(/\{widget\s+"([^"]+)"/);
59613
+ const widgetMatch = line.match(/\{widget\s+["']?([^\s"'}]+)/);
58747
59614
  if (widgetMatch) {
58748
59615
  const widgetName = widgetMatch[1];
58749
59616
  const refs = findWidgetReferences(widgetName, workspace, true);
@@ -58823,34 +59690,24 @@ function computeInlayHints(uri, range2, workspace) {
58823
59690
  return hints;
58824
59691
  }
58825
59692
  function addWidgetParamHints(text, range2, workspace, hints) {
58826
- const allWidgets = workspace.widgets.getAllWidgets();
58827
- if (allWidgets.length === 0) return;
58828
- const widgetMap = new Map(allWidgets.map((w) => [w.name, w]));
59693
+ if (workspace.widgets.getAllWidgets().length === 0) return;
58829
59694
  const macros = parseMacros(text);
59695
+ const lineStarts = buildLineStarts(text);
58830
59696
  for (const macro of macros) {
58831
59697
  if (!macro.open) continue;
58832
59698
  if (macro.range.start.line < range2.start.line || macro.range.start.line > range2.end.line) continue;
58833
- const widget = widgetMap.get(macro.name);
59699
+ const widget = workspace.widgets.getWidget(macro.name);
58834
59700
  if (!widget || widget.params.length === 0) continue;
58835
59701
  if (!macro.rawArgs || macro.rawArgs.trim() === "") continue;
58836
59702
  const args2 = lexArguments(macro.rawArgs);
58837
- const lines = text.split("\n");
58838
- const macroLine = lines[macro.range.start.line];
58839
- if (!macroLine) continue;
58840
- const macroText = macroLine.substring(macro.range.start.character, macro.range.end.character);
58841
- const nameEnd = macroText.indexOf(macro.name) + macro.name.length;
58842
- const argsText = macroText.substring(nameEnd).replace(/^\s+/, "").replace(/\}$/, "");
58843
- if (!argsText) continue;
59703
+ const macroEnd = lineStarts[macro.range.end.line] + macro.range.end.character;
59704
+ const argsStart = macroEnd - 1 - macro.rawArgs.length;
58844
59705
  for (let i = 0; i < Math.min(args2.length, widget.params.length); i++) {
58845
- const argText = args2[i].text;
58846
- const idx = macroLine.indexOf(argText, macro.range.start.character + nameEnd);
58847
- if (idx >= 0) {
58848
- hints.push({
58849
- position: { line: macro.range.start.line, character: idx },
58850
- label: `@${widget.params[i]}:`,
58851
- kind: "parameter"
58852
- });
58853
- }
59706
+ hints.push({
59707
+ position: offsetToPosition(argsStart + args2[i].start, lineStarts),
59708
+ label: `${widget.params[i]}:`,
59709
+ kind: "parameter"
59710
+ });
58854
59711
  }
58855
59712
  }
58856
59713
  }
@@ -59133,12 +59990,15 @@ var init_semantic_tokens = __esm({
59133
59990
  });
59134
59991
 
59135
59992
  // src/plugins/code-actions.ts
59136
- function computeCodeActions(uri, diagnostics, workspace) {
59993
+ import { readFileSync as readFileSync7 } from "node:fs";
59994
+ import { basename, dirname as dirname2, join as join3 } from "node:path";
59995
+ import { fileURLToPath as fileURLToPath6, pathToFileURL as pathToFileURL4 } from "node:url";
59996
+ function computeCodeActions(uri, diagnostics, workspace, options = {}) {
59137
59997
  const actions = [];
59138
59998
  for (const diag of diagnostics) {
59139
59999
  switch (diag.code) {
59140
60000
  case DiagnosticCode.UndefinedMacro: {
59141
- const action = fixUndefinedMacro(diag);
60001
+ const action = fixUndefinedMacro(uri, diag, workspace, options);
59142
60002
  if (action) actions.push(action);
59143
60003
  break;
59144
60004
  }
@@ -59166,27 +60026,141 @@ function computeCodeActions(uri, diagnostics, workspace) {
59166
60026
  }
59167
60027
  return actions;
59168
60028
  }
59169
- function fixUndefinedMacro(diag) {
60029
+ function fixUndefinedMacro(uri, diag, workspace, options) {
59170
60030
  const match2 = diag.message.match(/\{(\w[\w-]*)\}/);
59171
60031
  if (!match2) return null;
59172
60032
  const macroName = match2[1];
59173
- const configUri = "file:///spindle.config.yaml";
59174
- const yamlSnippet = `
59175
- ${macroName}:
59176
- description: ""
59177
- `;
59178
- return {
59179
- title: `Add '${macroName}' to spindle.config.yaml`,
60033
+ const root = resolveConfigRoot(uri, options.workspaceRoot);
60034
+ if (!root) return null;
60035
+ const existingPath = findConfigFile(root);
60036
+ const configPath = existingPath ?? join3(root, DEFAULT_CONFIG_FILENAME);
60037
+ const configUri = pathToFileURL4(configPath).toString();
60038
+ const action = {
60039
+ title: `Add '${macroName}' to ${basename(configPath)}`,
59180
60040
  kind: "quickfix",
59181
- diagnosticCodes: [DiagnosticCode.UndefinedMacro],
59182
- edits: [{
59183
- uri: configUri,
59184
- range: {
59185
- start: { line: Number.MAX_SAFE_INTEGER, character: 0 },
59186
- end: { line: Number.MAX_SAFE_INTEGER, character: 0 }
59187
- },
59188
- newText: yamlSnippet
59189
- }]
60041
+ diagnosticCodes: [DiagnosticCode.UndefinedMacro]
60042
+ };
60043
+ if (!existingPath) {
60044
+ const start = { line: 0, character: 0 };
60045
+ return {
60046
+ ...action,
60047
+ createFile: configUri,
60048
+ edits: [{
60049
+ uri: configUri,
60050
+ range: { start, end: start },
60051
+ newText: (0, import_yaml2.stringify)({ macros: { [macroName]: NEW_MACRO_ENTRY } })
60052
+ }]
60053
+ };
60054
+ }
60055
+ let text = workspace.documents.getText(configUri);
60056
+ if (text === void 0) {
60057
+ try {
60058
+ text = readFileSync7(existingPath, "utf-8");
60059
+ } catch {
60060
+ return null;
60061
+ }
60062
+ }
60063
+ const edit = configPath.toLowerCase().endsWith(".json") ? addMacroToJsonConfig(text, macroName) : addMacroToYamlConfig(text, macroName);
60064
+ if (!edit) return null;
60065
+ return { ...action, edits: [{ uri: configUri, ...edit }] };
60066
+ }
60067
+ function resolveConfigRoot(uri, workspaceRoot) {
60068
+ if (workspaceRoot) {
60069
+ return workspaceRoot.startsWith("file:") ? fileURLToPath6(workspaceRoot) : workspaceRoot;
60070
+ }
60071
+ if (!uri.startsWith("file:")) return null;
60072
+ const docDir = dirname2(fileURLToPath6(uri));
60073
+ let search = docDir;
60074
+ for (let i = 0; i < 10; i++) {
60075
+ if (findConfigFile(search)) return search;
60076
+ const parent = dirname2(search);
60077
+ if (parent === search) break;
60078
+ search = parent;
60079
+ }
60080
+ return docDir;
60081
+ }
60082
+ function macrosPath(root) {
60083
+ const legacy = root?.["spindle-0"];
60084
+ return legacy !== null && typeof legacy === "object" ? ["spindle-0", "macros"] : ["macros"];
60085
+ }
60086
+ function definesMacro(macros, macroName) {
60087
+ if (macros === null || typeof macros !== "object") return false;
60088
+ const lower = macroName.toLowerCase();
60089
+ return Object.keys(macros).some((k) => k.toLowerCase() === lower);
60090
+ }
60091
+ function addMacroToYamlConfig(text, macroName) {
60092
+ const doc = (0, import_yaml2.parseDocument)(text);
60093
+ if (doc.errors.length > 0) return null;
60094
+ if (doc.contents !== null && !(0, import_yaml2.isMap)(doc.contents)) return null;
60095
+ const path2 = macrosPath(doc.toJS());
60096
+ const parent = path2.length > 1 ? doc.getIn(path2.slice(0, -1), true) : doc.contents;
60097
+ const macros = doc.getIn(path2, true);
60098
+ if ((0, import_yaml2.isMap)(macros) && definesMacro(macros.toJS(doc), macroName)) return null;
60099
+ if ((0, import_yaml2.isMap)(parent) && (0, import_yaml2.isMap)(macros) && !macros.flow && macros.items.length > 0 && macros.range) {
60100
+ const macrosKey = parent.items.find((p) => (0, import_yaml2.isScalar)(p.key) && p.key.value === "macros")?.key;
60101
+ const firstKey = macros.items[0].key;
60102
+ if ((0, import_yaml2.isScalar)(macrosKey) && macrosKey.range && (0, import_yaml2.isScalar)(firstKey) && firstKey.range) {
60103
+ const indent = columnOf(text, firstKey.range[0]);
60104
+ const step = Math.max(indent - columnOf(text, macrosKey.range[0]), 1);
60105
+ const entry = (0, import_yaml2.stringify)({ [macroName]: NEW_MACRO_ENTRY }, { indent: step }).replace(/^(?=.)/gm, " ".repeat(indent));
60106
+ let offset = macros.range[1];
60107
+ if (offset > 0 && text[offset - 1] !== "\n") {
60108
+ const nl = text.indexOf("\n", offset);
60109
+ offset = nl === -1 ? text.length : nl + 1;
60110
+ }
60111
+ const lead = offset === text.length && text.length > 0 && !text.endsWith("\n") ? "\n" : "";
60112
+ const pos = offsetToPosition2(text, offset);
60113
+ return { range: { start: pos, end: pos }, newText: lead + entry };
60114
+ }
60115
+ }
60116
+ if ((0, import_yaml2.isMap)(macros)) {
60117
+ macros.set(macroName, doc.createNode(NEW_MACRO_ENTRY));
60118
+ } else {
60119
+ doc.setIn(path2, doc.createNode({ [macroName]: NEW_MACRO_ENTRY }));
60120
+ }
60121
+ return minimalEdit(text, doc.toString({ lineWidth: 0 }));
60122
+ }
60123
+ function addMacroToJsonConfig(text, macroName) {
60124
+ let raw;
60125
+ try {
60126
+ raw = text.trim() ? JSON.parse(text) : {};
60127
+ } catch {
60128
+ return null;
60129
+ }
60130
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
60131
+ const path2 = macrosPath(raw);
60132
+ const container = path2.length > 1 ? raw[path2[0]] : raw;
60133
+ const macros = container.macros;
60134
+ if (definesMacro(macros, macroName)) return null;
60135
+ if (macros !== null && typeof macros === "object" && !Array.isArray(macros)) {
60136
+ macros[macroName] = NEW_MACRO_ENTRY;
60137
+ } else {
60138
+ container.macros = { [macroName]: NEW_MACRO_ENTRY };
60139
+ }
60140
+ const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
60141
+ const newline = text.endsWith("\n") || !text.trim() ? "\n" : "";
60142
+ return minimalEdit(text, JSON.stringify(raw, null, indent) + newline);
60143
+ }
60144
+ function columnOf(text, offset) {
60145
+ return offset - (text.lastIndexOf("\n", offset - 1) + 1);
60146
+ }
60147
+ function offsetToPosition2(text, offset) {
60148
+ const before = text.slice(0, offset);
60149
+ const lineStart = before.lastIndexOf("\n") + 1;
60150
+ return { line: before.split("\n").length - 1, character: offset - lineStart };
60151
+ }
60152
+ function minimalEdit(oldText, newText) {
60153
+ const max = Math.min(oldText.length, newText.length);
60154
+ let prefix = 0;
60155
+ while (prefix < max && oldText[prefix] === newText[prefix]) prefix++;
60156
+ let suffix = 0;
60157
+ while (suffix < max - prefix && oldText[oldText.length - 1 - suffix] === newText[newText.length - 1 - suffix]) suffix++;
60158
+ return {
60159
+ range: {
60160
+ start: offsetToPosition2(oldText, prefix),
60161
+ end: offsetToPosition2(oldText, oldText.length - suffix)
60162
+ },
60163
+ newText: newText.slice(prefix, newText.length - suffix)
59190
60164
  };
59191
60165
  }
59192
60166
  function fixUndeclaredVariable(diag, workspace) {
@@ -59296,11 +60270,15 @@ function toLspRange6(r) {
59296
60270
  end: { line: r.end.line, character: r.end.character }
59297
60271
  };
59298
60272
  }
59299
- var codeActionsPlugin;
60273
+ var import_yaml2, DEFAULT_CONFIG_FILENAME, NEW_MACRO_ENTRY, codeActionsPlugin;
59300
60274
  var init_code_actions = __esm({
59301
60275
  "src/plugins/code-actions.ts"() {
59302
60276
  "use strict";
60277
+ import_yaml2 = __toESM(require_dist(), 1);
59303
60278
  init_diagnostic_codes();
60279
+ init_config_loader();
60280
+ DEFAULT_CONFIG_FILENAME = "spindle.config.yaml";
60281
+ NEW_MACRO_ENTRY = { description: "" };
59304
60282
  codeActionsPlugin = {
59305
60283
  id: "code-actions",
59306
60284
  capabilities: {
@@ -59323,7 +60301,8 @@ var init_code_actions = __esm({
59323
60301
  const actions = computeCodeActions(
59324
60302
  params.textDocument.uri,
59325
60303
  diagnostics,
59326
- ctx.workspace
60304
+ ctx.workspace,
60305
+ { workspaceRoot: ctx.config.workspaceRoot }
59327
60306
  );
59328
60307
  return actions.map((a) => ({
59329
60308
  title: a.title,
@@ -59331,7 +60310,16 @@ var init_code_actions = __esm({
59331
60310
  diagnostics: params.context.diagnostics.filter(
59332
60311
  (d) => a.diagnosticCodes.includes(String(d.code))
59333
60312
  ),
59334
- edit: {
60313
+ edit: a.createFile ? {
60314
+ // Resource operations are only expressible via documentChanges
60315
+ documentChanges: [
60316
+ { kind: "create", uri: a.createFile, options: { ignoreIfExists: true } },
60317
+ ...a.edits.map((e) => ({
60318
+ textDocument: { uri: e.uri, version: null },
60319
+ edits: [{ range: toLspRange6(e.range), newText: e.newText }]
60320
+ }))
60321
+ ]
60322
+ } : {
59335
60323
  changes: Object.fromEntries(
59336
60324
  a.edits.map((e) => [
59337
60325
  e.uri,
@@ -59371,7 +60359,7 @@ function computeDocumentSymbols(uri, workspace) {
59371
60359
  if (passage.tags?.includes("widget")) {
59372
60360
  const contentStart = passage.range.start.line + 1;
59373
60361
  const contentEnd = passage.range.end.line + 1;
59374
- const widgetDefRegex2 = /\{widget\s+"([^"]+)"[^}]*\}/gi;
60362
+ const widgetDefRegex2 = /\{widget\s+["']?([^\s"'}]+)["']?[^}]*\}/gi;
59375
60363
  for (let i = contentStart; i < contentEnd && i < lines.length; i++) {
59376
60364
  widgetDefRegex2.lastIndex = 0;
59377
60365
  let match2;
@@ -59476,14 +60464,19 @@ function searchWorkspaceSymbols(query, workspace) {
59476
60464
  range: widget.range
59477
60465
  });
59478
60466
  }
59479
- for (const [name, decl] of workspace.variables.getDeclared()) {
59480
- const displayName = "$" + name;
60467
+ const declarations = [
60468
+ ...workspace.variables.getDeclared().values(),
60469
+ ...workspace.variables.getDeclaredTransient().values()
60470
+ ];
60471
+ for (const decl of declarations) {
60472
+ const displayName = decl.sigil + decl.name;
59481
60473
  if (lowerQuery && !displayName.toLowerCase().includes(lowerQuery)) continue;
60474
+ if (!decl.declarationUri || !decl.declarationRange) continue;
59482
60475
  results.push({
59483
60476
  name: displayName,
59484
60477
  kind: import_vscode_languageserver3.SymbolKind.Variable,
59485
- uri: decl.declarationUri ?? "",
59486
- range: decl.declarationRange ?? { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
60478
+ uri: decl.declarationUri,
60479
+ range: decl.declarationRange
59487
60480
  });
59488
60481
  }
59489
60482
  return results;
@@ -59539,7 +60532,11 @@ function computeFoldingRanges(uri, workspace) {
59539
60532
  }
59540
60533
  }
59541
60534
  const macros = parseMacros(text);
59542
- pairMacros(macros, (name) => workspace.macros.isBlock(name));
60535
+ pairMacros(
60536
+ macros,
60537
+ (name) => workspace.macros.isBlock(name),
60538
+ passages.map((p) => p.range.start.line)
60539
+ );
59543
60540
  for (const macro of macros) {
59544
60541
  if (macro.open && macro.pair !== -1) {
59545
60542
  const closingMacro = macros[macro.pair];
@@ -59686,17 +60683,22 @@ var server_exports2 = {};
59686
60683
  __export(server_exports2, {
59687
60684
  startServer: () => startServer
59688
60685
  });
59689
- import { fileURLToPath as fileURLToPath6, pathToFileURL as pathToFileURL4 } from "node:url";
59690
- import { readFileSync as readFileSync7 } from "node:fs";
60686
+ import { fileURLToPath as fileURLToPath7, pathToFileURL as pathToFileURL5 } from "node:url";
60687
+ import { readFileSync as readFileSync8 } from "node:fs";
60688
+ import { basename as basename2 } from "node:path";
59691
60689
  function uriToFsPath(uri) {
59692
60690
  try {
59693
- return fileURLToPath6(uri);
60691
+ return fileURLToPath7(uri);
59694
60692
  } catch {
59695
60693
  return uri;
59696
60694
  }
59697
60695
  }
59698
60696
  function fsPathToUri(fsPath) {
59699
- return pathToFileURL4(fsPath).toString();
60697
+ return pathToFileURL5(fsPath).toString();
60698
+ }
60699
+ function isConfigFileUri(uri) {
60700
+ const name = basename2(uriToFsPath(uri));
60701
+ return name.startsWith("spindle.config.") || name.includes("twee-config.");
59700
60702
  }
59701
60703
  function startServer(_args) {
59702
60704
  const connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -59722,7 +60724,7 @@ function startServer(_args) {
59722
60724
  plugin.initialize({ connection, workspace, config: config2 });
59723
60725
  }
59724
60726
  if (Object.keys(projectConfig.macros).length > 0) {
59725
- workspace.macros.loadSupplements(projectConfig.macros);
60727
+ workspace.macros.loadConfig(projectConfig.macros);
59726
60728
  console.error("[spindle-lsp] loaded config macros:", Object.keys(projectConfig.macros).length);
59727
60729
  }
59728
60730
  return {
@@ -59746,6 +60748,9 @@ function startServer(_args) {
59746
60748
  try {
59747
60749
  const fileContents = await scanWorkspaceFiles(workspaceRoot);
59748
60750
  console.error("[spindle-lsp] scanned", fileContents.size, "files from root:", workspaceRoot ?? "undefined");
60751
+ for (const uri of documents.keys()) {
60752
+ fileContents.delete(uri);
60753
+ }
59749
60754
  workspace.initialize(fileContents);
59750
60755
  console.error("[spindle-lsp] workspace initialized, macros:", workspace.macros.getAllMacros().length);
59751
60756
  } catch (err) {
@@ -59785,23 +60790,40 @@ function startServer(_args) {
59785
60790
  if (workspace) {
59786
60791
  try {
59787
60792
  const fsPath = uriToFsPath(textDocument.uri);
59788
- const text = readFileSync7(fsPath, "utf-8");
60793
+ const text = readFileSync8(fsPath, "utf-8");
59789
60794
  workspace.documents.update(textDocument.uri, text);
59790
60795
  } catch {
59791
60796
  workspace.documents.close(textDocument.uri);
59792
60797
  }
59793
60798
  }
59794
60799
  });
60800
+ function reloadProjectConfig() {
60801
+ if (!workspaceRoot) return;
60802
+ try {
60803
+ const projectConfig = loadConfigFromDisk(workspaceRoot);
60804
+ workspace.macros.loadConfig(projectConfig.macros);
60805
+ console.error("[spindle-lsp] reloaded config macros:", Object.keys(projectConfig.macros).length);
60806
+ } catch (err) {
60807
+ console.error("[spindle-lsp] config reload failed:", err);
60808
+ return;
60809
+ }
60810
+ workspace.refresh();
60811
+ }
59795
60812
  connection.onDidChangeWatchedFiles(({ changes }) => {
59796
60813
  if (!workspace) return;
60814
+ let configChanged = false;
59797
60815
  for (const change of changes) {
60816
+ if (isConfigFileUri(change.uri)) {
60817
+ configChanged = true;
60818
+ continue;
60819
+ }
60820
+ if (documents.has(change.uri)) continue;
59798
60821
  if (change.type === import_node.FileChangeType.Deleted) {
59799
60822
  workspace.documents.close(change.uri);
59800
- documents.delete(change.uri);
59801
60823
  } else {
59802
60824
  try {
59803
60825
  const fsPath = uriToFsPath(change.uri);
59804
- const text = readFileSync7(fsPath, "utf-8");
60826
+ const text = readFileSync8(fsPath, "utf-8");
59805
60827
  if (workspace.documents.has(change.uri)) {
59806
60828
  workspace.documents.update(change.uri, text);
59807
60829
  } else {
@@ -59811,6 +60833,9 @@ function startServer(_args) {
59811
60833
  }
59812
60834
  }
59813
60835
  }
60836
+ if (configChanged) {
60837
+ reloadProjectConfig();
60838
+ }
59814
60839
  });
59815
60840
  connection.onShutdown(() => {
59816
60841
  for (const plugin of activePlugins) {
@@ -59831,7 +60856,7 @@ async function scanWorkspaceFiles(root) {
59831
60856
  });
59832
60857
  for (const filePath of files) {
59833
60858
  try {
59834
- const text = readFileSync7(filePath, "utf-8");
60859
+ const text = readFileSync8(filePath, "utf-8");
59835
60860
  const uri = fsPathToUri(filePath);
59836
60861
  contents.set(uri, text);
59837
60862
  } catch {
@@ -59858,7 +60883,7 @@ var init_server4 = __esm({
59858
60883
 
59859
60884
  // src/bin.ts
59860
60885
  var args = process.argv.slice(2);
59861
- var VERSION = "0.8.4";
60886
+ var VERSION = "0.9.1";
59862
60887
  var HELP = `spindle-lsp v${VERSION}
59863
60888
  Language Server Protocol server for Spindle story format
59864
60889