@rohal12/spindle-lsp 0.9.0 → 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 +1088 -436
  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
@@ -6647,6 +6749,17 @@ function inferLiteralType(expr) {
6647
6749
  if (first === "{") return "object";
6648
6750
  return "string";
6649
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
+ }
6650
6763
  var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, ARRAY_MEMBERS, VariableTracker;
6651
6764
  var init_variable_tracker = __esm({
6652
6765
  "src/core/workspace/variable-tracker.ts"() {
@@ -6705,7 +6818,7 @@ var init_variable_tracker = __esm({
6705
6818
  * Parse the StoryVariables passage content for declarations.
6706
6819
  * Each line like `$name = value` becomes a declaration.
6707
6820
  */
6708
- parseStoryVariables(content, contentStartLine = 0) {
6821
+ parseStoryVariables(content, contentStartLine = 0, uri) {
6709
6822
  this.declared.clear();
6710
6823
  this._hasStoryVariables = true;
6711
6824
  this._nullDeclarations = [];
@@ -6717,9 +6830,10 @@ var init_variable_tracker = __esm({
6717
6830
  if (!match2) continue;
6718
6831
  const name = match2[1];
6719
6832
  const expr = match2[2].trim();
6833
+ const absLine = contentStartLine + i;
6834
+ const location = declarationLocation(lines[i], name, absLine, uri);
6720
6835
  if (expr === "null") {
6721
6836
  const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
6722
- const absLine = contentStartLine + i;
6723
6837
  this._nullDeclarations.push({
6724
6838
  name,
6725
6839
  sigil: "$",
@@ -6728,10 +6842,10 @@ var init_variable_tracker = __esm({
6728
6842
  end: { line: absLine, character: charIdx + 4 }
6729
6843
  }
6730
6844
  });
6731
- this.declared.set(name, { name, sigil: "$" });
6845
+ this.declared.set(name, { name, sigil: "$", ...location });
6732
6846
  continue;
6733
6847
  }
6734
- const decl = { name, sigil: "$" };
6848
+ const decl = { name, sigil: "$", ...location };
6735
6849
  const type = inferLiteralType(expr);
6736
6850
  if (type) decl.type = type;
6737
6851
  if (expr.startsWith("{")) {
@@ -6748,11 +6862,23 @@ var init_variable_tracker = __esm({
6748
6862
  this.declared.set(name, decl);
6749
6863
  }
6750
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
+ }
6751
6877
  /**
6752
6878
  * Parse the StoryTransients passage content for declarations.
6753
6879
  * Each line like `%name = value` becomes a declaration.
6754
6880
  */
6755
- parseStoryTransients(content, contentStartLine = 0) {
6881
+ parseStoryTransients(content, contentStartLine = 0, uri) {
6756
6882
  this.declaredTransient.clear();
6757
6883
  this._hasStoryTransients = true;
6758
6884
  this._nullTransientDeclarations = [];
@@ -6764,9 +6890,10 @@ var init_variable_tracker = __esm({
6764
6890
  if (!match2) continue;
6765
6891
  const name = match2[1];
6766
6892
  const expr = match2[2].trim();
6893
+ const absLine = contentStartLine + i;
6894
+ const location = declarationLocation(lines[i], name, absLine, uri);
6767
6895
  if (expr === "null") {
6768
6896
  const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
6769
- const absLine = contentStartLine + i;
6770
6897
  this._nullTransientDeclarations.push({
6771
6898
  name,
6772
6899
  sigil: "%",
@@ -6775,10 +6902,10 @@ var init_variable_tracker = __esm({
6775
6902
  end: { line: absLine, character: charIdx + 4 }
6776
6903
  }
6777
6904
  });
6778
- this.declaredTransient.set(name, { name, sigil: "%" });
6905
+ this.declaredTransient.set(name, { name, sigil: "%", ...location });
6779
6906
  continue;
6780
6907
  }
6781
- const decl = { name, sigil: "%" };
6908
+ const decl = { name, sigil: "%", ...location };
6782
6909
  const type = inferLiteralType(expr);
6783
6910
  if (type) decl.type = type;
6784
6911
  if (expr.startsWith("{")) {
@@ -6809,19 +6936,20 @@ var init_variable_tracker = __esm({
6809
6936
  for (let i = 0; i < lines.length; i++) {
6810
6937
  const header = parsePassageHeader(lines[i], i);
6811
6938
  if (header) {
6812
- passageBoundaries.push({ name: header.name, startLine: i });
6939
+ passageBoundaries.push({ name: header.name, tags: header.tags, startLine: i });
6813
6940
  }
6814
6941
  }
6815
6942
  for (let pi = 0; pi < passageBoundaries.length; pi++) {
6816
6943
  const passage = passageBoundaries[pi];
6817
6944
  if (EXCLUDED_PASSAGES.has(passage.name)) continue;
6945
+ if (isScriptOrStylesheetPassage(passage)) continue;
6818
6946
  const contentStartLine = passage.startLine + 1;
6819
6947
  const contentEndLine = pi + 1 < passageBoundaries.length ? passageBoundaries[pi + 1].startLine : lines.length;
6820
6948
  const contentLines = lines.slice(contentStartLine, contentEndLine);
6821
6949
  const content = contentLines.join("\n");
6822
6950
  let cleaned = content;
6823
6951
  for (const pattern of CLEAN_PATTERNS) {
6824
- cleaned = cleaned.replace(pattern, (m) => " ".repeat(m.length));
6952
+ cleaned = cleaned.replace(pattern, (m) => m.replace(/[^\r\n]/g, " "));
6825
6953
  }
6826
6954
  const lineOffsets = [0];
6827
6955
  for (let i = 0; i < cleaned.length; i++) {
@@ -6976,14 +7104,114 @@ var init_variable_tracker = __esm({
6976
7104
  }
6977
7105
  });
6978
7106
 
7107
+ // src/core/parsing/macro-parser.ts
7108
+ function buildLineStarts(text) {
7109
+ const starts = [0];
7110
+ for (let i = 0; i < text.length; i++) {
7111
+ if (text[i] === "\n") {
7112
+ starts.push(i + 1);
7113
+ }
7114
+ }
7115
+ return starts;
7116
+ }
7117
+ function offsetToPosition(offset, lineStarts) {
7118
+ let low = 0;
7119
+ let high = lineStarts.length - 1;
7120
+ while (low < high) {
7121
+ const mid = low + high + 1 >> 1;
7122
+ if (lineStarts[mid] <= offset) {
7123
+ low = mid;
7124
+ } else {
7125
+ high = mid - 1;
7126
+ }
7127
+ }
7128
+ return { line: low, character: offset - lineStarts[low] };
7129
+ }
7130
+ function parseMacros(text) {
7131
+ const cleaned = text.replace(variableInterpolationRegex, (match3) => {
7132
+ return " ".repeat(match3.length);
7133
+ });
7134
+ const lineStarts = buildLineStarts(text);
7135
+ const macros = [];
7136
+ let id = 0;
7137
+ macroRegex.lastIndex = 0;
7138
+ let match2;
7139
+ while ((match2 = macroRegex.exec(cleaned)) !== null) {
7140
+ const closeSlash = match2[1];
7141
+ const cssPrefix = (match2[2] || "").trim();
7142
+ const macroName = match2[3];
7143
+ const rawArgs = match2[4];
7144
+ const open = closeSlash !== "/";
7145
+ const matchStart = match2.index;
7146
+ const matchEnd = matchStart + match2[0].length;
7147
+ const startPos = offsetToPosition(matchStart, lineStarts);
7148
+ const endPos = offsetToPosition(matchEnd, lineStarts);
7149
+ const range2 = {
7150
+ start: startPos,
7151
+ end: endPos
7152
+ };
7153
+ macros.push({
7154
+ id,
7155
+ pair: -1,
7156
+ name: macroName,
7157
+ open,
7158
+ range: range2,
7159
+ cssPrefix: cssPrefix || void 0,
7160
+ rawArgs: rawArgs || void 0
7161
+ });
7162
+ id++;
7163
+ }
7164
+ return macros;
7165
+ }
7166
+ function pairMacros(macros, isBlock, passageStartLines = []) {
7167
+ const boundaries = [...passageStartLines].sort((a, b) => a - b);
7168
+ let nextBoundary = 0;
7169
+ let stack = [];
7170
+ for (const macro of macros) {
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 = [];
7177
+ if (!isBlock(macro.name)) continue;
7178
+ if (macro.open) {
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;
7190
+ }
7191
+ }
7192
+ }
7193
+ }
7194
+ var variableInterpolationRegex, macroRegex;
7195
+ var init_macro_parser = __esm({
7196
+ "src/core/parsing/macro-parser.ts"() {
7197
+ "use strict";
7198
+ variableInterpolationRegex = /(?<!\\)\{([$_@%][A-Za-z_$][\w$.]*)\}/g;
7199
+ macroRegex = /(?<!\\)\{(\/)?(?:((?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*))(?:\s+((?:(?:`(?:\\.|[^`\\])*?`)|(?:"(?:\\.|[^"\\])*?")|(?:'(?:\\.|[^'\\])*?')|(?:\[(?:[<>]?[Ii][Mm][Gg])?\[[^\r\n]*?\]\]+)|[^}]|(?:}))*?))?\}/gmi;
7200
+ }
7201
+ });
7202
+
6979
7203
  // src/core/workspace/widget-registry.ts
6980
- var widgetDefRegex, paramRegex, WidgetRegistry;
7204
+ function parseParams(argString) {
7205
+ return argString.trim().split(/\s+/).filter((t) => /^[$_@]/.test(t) && t !== "@children");
7206
+ }
7207
+ var widgetDefRegex, WidgetRegistry;
6981
7208
  var init_widget_registry = __esm({
6982
7209
  "src/core/workspace/widget-registry.ts"() {
6983
7210
  "use strict";
6984
- widgetDefRegex = /\{widget\s+"([^"]+)"((?:\s+@[A-Za-z_$][\w$]*)*)\s*\}/gi;
6985
- paramRegex = /@([A-Za-z_$][\w$]*)/g;
7211
+ init_macro_parser();
7212
+ widgetDefRegex = /(\{widget\s+["']?)([^\s"'}]+)["']?([^}]*)\}/gi;
6986
7213
  WidgetRegistry = class {
7214
+ /** Widgets keyed by lower-cased name; Spindle resolves widgets case-insensitively. */
6987
7215
  widgets = /* @__PURE__ */ new Map();
6988
7216
  /** Per-URI set of lower-cased macro names opened in that document. */
6989
7217
  invokedByUri = /* @__PURE__ */ new Map();
@@ -7009,40 +7237,43 @@ var init_widget_registry = __esm({
7009
7237
  break;
7010
7238
  }
7011
7239
  }
7012
- const contentLines = lines.slice(contentStartLine, contentEndLine);
7013
- for (let i = 0; i < contentLines.length; i++) {
7014
- const line = contentLines[i];
7015
- widgetDefRegex.lastIndex = 0;
7016
- let match2;
7017
- while ((match2 = widgetDefRegex.exec(line)) !== null) {
7018
- const widgetName = match2[1];
7019
- const paramString = match2[2] || "";
7020
- const params = [];
7021
- let paramMatch;
7022
- paramRegex.lastIndex = 0;
7023
- while ((paramMatch = paramRegex.exec(paramString)) !== null) {
7024
- params.push(paramMatch[1]);
7025
- }
7026
- const lineNum = contentStartLine + i;
7027
- const charStart = match2.index;
7028
- const charEnd = charStart + match2[0].length;
7029
- const range2 = {
7030
- start: { line: lineNum, character: charStart },
7031
- end: { line: lineNum, character: charEnd }
7032
- };
7033
- this.widgets.set(widgetName, {
7034
- name: widgetName,
7035
- params,
7036
- uri: passage.uri,
7037
- range: range2
7038
- });
7039
- }
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
+ });
7040
7271
  }
7041
7272
  }
7042
7273
  }
7043
- /** Get a widget definition by name. */
7274
+ /** Get a widget definition by name (case-insensitive). */
7044
7275
  getWidget(name) {
7045
- return this.widgets.get(name);
7276
+ return this.widgets.get(name.toLowerCase());
7046
7277
  }
7047
7278
  /** Get all registered widget definitions. */
7048
7279
  getAllWidgets() {
@@ -7076,89 +7307,111 @@ var init_widget_registry = __esm({
7076
7307
  }
7077
7308
  });
7078
7309
 
7079
- // src/core/parsing/macro-parser.ts
7080
- function buildLineStarts(text) {
7081
- const starts = [0];
7082
- for (let i = 0; i < text.length; i++) {
7083
- if (text[i] === "\n") {
7084
- starts.push(i + 1);
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
+ }
7085
7345
  }
7086
7346
  }
7087
- return starts;
7347
+ return null;
7088
7348
  }
7089
- function offsetToPosition(offset, lineStarts) {
7090
- let low = 0;
7091
- let high = lineStarts.length - 1;
7092
- while (low < high) {
7093
- const mid = low + high + 1 >> 1;
7094
- if (lineStarts[mid] <= offset) {
7095
- low = mid;
7096
- } else {
7097
- high = mid - 1;
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";
7098
7366
  }
7099
7367
  }
7100
- return { line: low, character: offset - lineStarts[low] };
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;
7101
7382
  }
7102
- function parseMacros(text) {
7103
- const cleaned = text.replace(variableInterpolationRegex, (match3) => {
7104
- return " ".repeat(match3.length);
7105
- });
7106
- const lineStarts = buildLineStarts(text);
7383
+ function discoverMacrosFromSource(source) {
7107
7384
  const macros = [];
7108
- let id = 0;
7109
- macroRegex.lastIndex = 0;
7385
+ defineMacroRegex.lastIndex = 0;
7110
7386
  let match2;
7111
- while ((match2 = macroRegex.exec(cleaned)) !== null) {
7112
- const closeSlash = match2[1];
7113
- const cssPrefix = (match2[2] || "").trim();
7114
- const macroName = match2[3];
7115
- const rawArgs = match2[4];
7116
- const open = closeSlash !== "/";
7117
- const matchStart = match2.index;
7118
- const matchEnd = matchStart + match2[0].length;
7119
- const startPos = offsetToPosition(matchStart, lineStarts);
7120
- const endPos = offsetToPosition(matchEnd, lineStarts);
7121
- const range2 = {
7122
- start: startPos,
7123
- end: endPos
7124
- };
7125
- macros.push({
7126
- id,
7127
- pair: -1,
7128
- name: macroName,
7129
- open,
7130
- range: range2,
7131
- cssPrefix: cssPrefix || void 0,
7132
- rawArgs: rawArgs || void 0
7133
- });
7134
- id++;
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
+ }
7135
7395
  }
7136
7396
  return macros;
7137
7397
  }
7138
- function pairMacros(macros, isBlock) {
7139
- const stacks = {};
7140
- for (const macro of macros) {
7141
- const name = macro.name.toLowerCase();
7142
- if (!isBlock(macro.name)) continue;
7143
- if (macro.open) {
7144
- if (!stacks[name]) stacks[name] = [];
7145
- stacks[name].push(macro.id);
7146
- } else {
7147
- const stack = stacks[name];
7148
- if (stack && stack.length > 0) {
7149
- const openId = stack.pop();
7150
- macros[openId].pair = macro.id;
7151
- macro.pair = openId;
7152
- }
7153
- }
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);
7154
7406
  }
7407
+ return macros;
7155
7408
  }
7156
- var variableInterpolationRegex, macroRegex;
7157
- var init_macro_parser = __esm({
7158
- "src/core/parsing/macro-parser.ts"() {
7409
+ var defineMacroRegex, doBlockRegex;
7410
+ var init_macro_discovery = __esm({
7411
+ "src/core/parsing/macro-discovery.ts"() {
7159
7412
  "use strict";
7160
- variableInterpolationRegex = /(?<!\\)\{([$_@%][A-Za-z_$][\w$.]*)\}/g;
7161
- macroRegex = /(?<!\\)\{(\/)?(?:((?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*))(?:\s+((?:(?:`(?:\\.|[^`\\])*?`)|(?:"(?:\\.|[^"\\])*?")|(?:'(?:\\.|[^'\\])*?')|(?:\[(?:[<>]?[Ii][Mm][Gg])?\[[^\r\n]*?\]\]+)|[^}]|(?:}))*?))?\}/gmi;
7413
+ defineMacroRegex = /Story\.defineMacro\s*\(\s*\{/g;
7414
+ doBlockRegex = /\{do\}([\s\S]*?)\{\/do\}/g;
7162
7415
  }
7163
7416
  });
7164
7417
 
@@ -7352,7 +7605,7 @@ var init_macro_supplements = __esm({
7352
7605
  },
7353
7606
  include: {
7354
7607
  name: "include",
7355
- 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}`',
7356
7609
  parameters: ["text"]
7357
7610
  },
7358
7611
  passage: {
@@ -7362,28 +7615,28 @@ var init_macro_supplements = __esm({
7362
7615
  },
7363
7616
  textbox: {
7364
7617
  name: "textbox",
7365
- description: 'Creates a text input box.\n\nUsage: `{textbox "$receiver" defaultValue}`',
7366
- 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"]
7367
7620
  },
7368
7621
  numberbox: {
7369
7622
  name: "numberbox",
7370
- description: 'Creates a number input box.\n\nUsage: `{numberbox "$receiver" defaultValue}`',
7371
- 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"]
7372
7625
  },
7373
7626
  textarea: {
7374
7627
  name: "textarea",
7375
- description: 'Creates a multi-line text input area.\n\nUsage: `{textarea "$receiver" defaultValue}`',
7376
- 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"]
7377
7630
  },
7378
7631
  checkbox: {
7379
7632
  name: "checkbox",
7380
- description: 'Creates a checkbox input.\n\nUsage: `{checkbox "$receiver" uncheckedValue checkedValue}`',
7381
- parameters: ["receiver &+ text"]
7633
+ description: 'Creates a checkbox that toggles a boolean story variable.\n\nUsage: `{checkbox $receiver "label"}`',
7634
+ parameters: ["receiver |+ text"]
7382
7635
  },
7383
7636
  radiobutton: {
7384
7637
  name: "radiobutton",
7385
- description: 'Creates a radio button input.\n\nUsage: `{radiobutton "$receiver" value}`',
7386
- 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"]
7387
7640
  },
7388
7641
  quicksave: {
7389
7642
  name: "quicksave",
@@ -7413,8 +7666,8 @@ var init_macro_supplements = __esm({
7413
7666
  dialog: {
7414
7667
  name: "dialog",
7415
7668
  container: true,
7416
- description: 'Opens a dialog with its contents.\n\nUsage:\n\n```\n{dialog "title"}\n ...\n{/dialog}\n```',
7417
- 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'"]
7418
7671
  },
7419
7672
  nobr: {
7420
7673
  name: "nobr",
@@ -7470,6 +7723,7 @@ var init_workspace_model = __esm({
7470
7723
  init_variable_tracker();
7471
7724
  init_widget_registry();
7472
7725
  init_macro_parser();
7726
+ init_macro_discovery();
7473
7727
  init_macro_supplements();
7474
7728
  WorkspaceModel = class extends EventEmitter3 {
7475
7729
  documents;
@@ -7517,6 +7771,14 @@ var init_workspace_model = __esm({
7517
7771
  this.initialized = true;
7518
7772
  this.scheduleModelReady();
7519
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
+ }
7520
7782
  /** Clean up listeners and timers. */
7521
7783
  dispose() {
7522
7784
  this.documents.removeListener("documentChanged", this.onDocumentChanged);
@@ -7534,6 +7796,7 @@ var init_workspace_model = __esm({
7534
7796
  if (text !== void 0) {
7535
7797
  this.passages.rebuild(uri, text);
7536
7798
  }
7799
+ this.refreshDiscoveredMacros();
7537
7800
  this.cascade();
7538
7801
  this.emit("documentChanged", uri);
7539
7802
  this.emit("passagesUpdated", uri);
@@ -7542,6 +7805,7 @@ var init_workspace_model = __esm({
7542
7805
  /** Handle a document close: remove its passages and cascade. */
7543
7806
  handleDocumentClose(uri) {
7544
7807
  this.passages.remove(uri);
7808
+ this.refreshDiscoveredMacros();
7545
7809
  this.cascade();
7546
7810
  this.emit("documentClosed", uri);
7547
7811
  this.emit("passagesUpdated", uri);
@@ -7555,8 +7819,39 @@ var init_workspace_model = __esm({
7555
7819
  this.passages.rebuild(uri, text);
7556
7820
  }
7557
7821
  }
7822
+ this.refreshDiscoveredMacros();
7558
7823
  this.cascade();
7559
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
+ }
7560
7855
  /**
7561
7856
  * Cascade: rescan variables and widgets based on current passages.
7562
7857
  * Called after any passage index update.
@@ -7576,8 +7871,10 @@ var init_workspace_model = __esm({
7576
7871
  }
7577
7872
  }
7578
7873
  const content = lines.slice(contentStart, contentEnd).join("\n");
7579
- this.variables.parseStoryVariables(content, contentStart);
7874
+ this.variables.parseStoryVariables(content, contentStart, storyVars.uri);
7580
7875
  }
7876
+ } else {
7877
+ this.variables.clearStoryVariables();
7581
7878
  }
7582
7879
  const storyTransients = this.passages.getStoryTransients();
7583
7880
  if (storyTransients) {
@@ -7593,8 +7890,10 @@ var init_workspace_model = __esm({
7593
7890
  }
7594
7891
  }
7595
7892
  const content = lines.slice(contentStart, contentEnd).join("\n");
7596
- this.variables.parseStoryTransients(content, contentStart);
7893
+ this.variables.parseStoryTransients(content, contentStart, storyTransients.uri);
7597
7894
  }
7895
+ } else {
7896
+ this.variables.clearStoryTransients();
7598
7897
  }
7599
7898
  this.widgets.clearInvocations();
7600
7899
  for (const uri of this.documents.getUris()) {
@@ -8353,6 +8652,30 @@ var init_parameter_validator = __esm({
8353
8652
  return arg.type === 4 /* Variable */ ? null : new TypeError2("Argument is not a variable");
8354
8653
  }
8355
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
+ },
8356
8679
  {
8357
8680
  name: ["passage"],
8358
8681
  validate(arg, _args, _index, state) {
@@ -8505,58 +8828,175 @@ var init_parameter_validator = __esm({
8505
8828
  });
8506
8829
 
8507
8830
  // src/core/parsing/link-parser.ts
8508
- function buildLineStarts2(text) {
8509
- const starts = [0];
8510
- for (let i = 0; i < text.length; i++) {
8511
- if (text[i] === "\n") {
8512
- 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;
8513
8842
  }
8514
8843
  }
8515
- return starts;
8844
+ return i;
8516
8845
  }
8517
- function offsetToPosition2(offset, lineStarts) {
8518
- let low = 0;
8519
- let high = lineStarts.length - 1;
8520
- while (low < high) {
8521
- const mid = low + high + 1 >> 1;
8522
- if (lineStarts[mid] <= offset) {
8523
- low = mid;
8524
- } else {
8525
- high = mid - 1;
8526
- }
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;
8527
8858
  }
8528
- 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 };
8529
8862
  }
8530
8863
  function parseLinks(text, lineOffset = 0) {
8531
- const lineStarts = buildLineStarts2(text);
8864
+ const lineStarts = buildLineStarts(text);
8532
8865
  const refs = [];
8533
- linkRegex.lastIndex = 0;
8534
- let match2;
8535
- while ((match2 = linkRegex.exec(text)) !== null) {
8536
- const passageName = match2[2].trim();
8537
- if (!passageName) continue;
8538
- const nameStartInMatch = match2[0].indexOf(passageName);
8539
- const nameStart = match2.index + nameStartInMatch;
8540
- const nameEnd = nameStart + passageName.length;
8541
- const startPos = offsetToPosition2(nameStart, lineStarts);
8542
- const endPos = offsetToPosition2(nameEnd, lineStarts);
8543
- const range2 = {
8544
- start: { line: startPos.line + lineOffset, character: startPos.character },
8545
- end: { line: endPos.line + lineOffset, character: endPos.character }
8546
- };
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);
8547
8962
  refs.push({
8548
- name: passageName,
8549
- range: range2,
8550
- 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"
8551
8969
  });
8552
8970
  }
8553
8971
  return refs;
8554
8972
  }
8555
- var linkRegex;
8973
+ var EXPRESSION_BUILTINS, interpolationRegex, temporaryRefRegex, macroTargetResolvers;
8556
8974
  var init_link_parser = __esm({
8557
8975
  "src/core/parsing/link-parser.ts"() {
8558
8976
  "use strict";
8559
- 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
+ };
8560
9000
  }
8561
9001
  });
8562
9002
 
@@ -8569,8 +9009,13 @@ function computeDiagnostics(uri, workspace, options) {
8569
9009
  const passages = workspace.passages.getPassagesInDocument(uri);
8570
9010
  if (passages.length === 0) return [];
8571
9011
  const diagnostics = [];
8572
- const macros = parseMacros(text);
8573
- 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
+ );
8574
9019
  const allPassages = workspace.passages.getAllPassages();
8575
9020
  const passageNames = new Set(allPassages.map((p) => p.name));
8576
9021
  try {
@@ -8586,7 +9031,7 @@ function computeDiagnostics(uri, workspace, options) {
8586
9031
  } catch {
8587
9032
  }
8588
9033
  try {
8589
- validateLinks(text, passages, passageNames, diagnostics);
9034
+ validateLinks(markupText, passages, passageNames, diagnostics);
8590
9035
  } catch {
8591
9036
  }
8592
9037
  try {
@@ -8620,6 +9065,18 @@ function computeDiagnostics(uri, workspace, options) {
8620
9065
  return [];
8621
9066
  }
8622
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
+ }
8623
9080
  function validateMacros(macros, workspace, diagnostics) {
8624
9081
  for (let curIndex = 0; curIndex < macros.length; curIndex++) {
8625
9082
  const macro = macros[curIndex];
@@ -8732,8 +9189,13 @@ function validateArguments(macros, workspace, passageNames, diagnostics) {
8732
9189
  if (info.skipArgs) continue;
8733
9190
  if (!info.parameters) continue;
8734
9191
  const rawArgs = macro.rawArgs ?? "";
8735
- const args2 = lexArguments(rawArgs);
8736
- 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
+ }
8737
9199
  if (params.isEmpty()) {
8738
9200
  if (args2.length > 0) {
8739
9201
  diagnostics.push(makeDiag(
@@ -8773,6 +9235,16 @@ function validateArguments(macros, workspace, passageNames, diagnostics) {
8773
9235
  }
8774
9236
  }
8775
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
+ }
8776
9248
  function validateVariables(uri, workspace, diagnostics) {
8777
9249
  if (!workspace.variables.hasStoryVariables()) {
8778
9250
  const undeclared = workspace.variables.getUndeclared(uri);
@@ -8874,7 +9346,7 @@ function validateArrayMemberAccess(uri, workspace, diagnostics) {
8874
9346
  ));
8875
9347
  }
8876
9348
  }
8877
- function temporaryRefRegex(name) {
9349
+ function temporaryRefRegex2(name) {
8878
9350
  const ident = name ? escapeRegex(name) : "[A-Za-z_$][\\w$]*";
8879
9351
  return new RegExp(`(?<![.\\w$@%])_(${ident})(?![\\w$])`, "g");
8880
9352
  }
@@ -8950,7 +9422,7 @@ function setTemporaryTargets(rawArgs) {
8950
9422
  while ((m = assignRe.exec(code)) !== null) {
8951
9423
  const name = m[1];
8952
9424
  const rhs = code.slice(m.index + m[0].length, statementEnd(code, m.index + m[0].length));
8953
- if (temporaryRefRegex(name).test(rhs)) continue;
9425
+ if (temporaryRefRegex2(name).test(rhs)) continue;
8954
9426
  targets.push({ name, offset: m.index });
8955
9427
  }
8956
9428
  return targets;
@@ -9019,7 +9491,7 @@ function validateLoopTemporaries(uri, text, macros, workspace, diagnostics) {
9019
9491
  }
9020
9492
  }
9021
9493
  function isReadInBody(body, bodyOffset, name, macroStart, macroEnd) {
9022
- const re = temporaryRefRegex(name);
9494
+ const re = temporaryRefRegex2(name);
9023
9495
  let m;
9024
9496
  while ((m = re.exec(body)) !== null) {
9025
9497
  const offset = bodyOffset + m.index;
@@ -9030,16 +9502,16 @@ function isReadInBody(body, bodyOffset, name, macroStart, macroEnd) {
9030
9502
  }
9031
9503
  return false;
9032
9504
  }
9033
- function resolveIncludeTarget(rawArgs) {
9034
- const expr = rawArgs.replace(/\binline\b/, "").trim();
9505
+ function resolveIncludeTarget2(rawArgs) {
9506
+ const expr = includeExpression(rawArgs);
9035
9507
  if (expr === "") return null;
9036
9508
  const literal2 = /^(["'`])((?:\\.|(?!\1)[^\\])*)\1$/s.exec(expr);
9037
9509
  if (literal2) {
9038
9510
  if (literal2[1] === "`" && literal2[2].includes("${")) return null;
9039
9511
  return literal2[2].replace(/\\(.)/g, "$1");
9040
9512
  }
9041
- if (/[$@%"'`(]/.test(expr) || temporaryRefRegex().test(expr)) return null;
9042
- if (EXPRESSION_BUILTINS.has(expr)) return null;
9513
+ if (/[$@%"'`(]/.test(expr) || temporaryRefRegex2().test(expr)) return null;
9514
+ if (EXPRESSION_BUILTINS2.has(expr)) return null;
9043
9515
  return expr;
9044
9516
  }
9045
9517
  function widgetDefinitionNames(content) {
@@ -9058,7 +9530,7 @@ function validateWidgetIncludes(text, macros, workspace, diagnostics) {
9058
9530
  for (const macro of macros) {
9059
9531
  if (!macro.open || !macro.rawArgs || macro.name.toLowerCase() !== "include") continue;
9060
9532
  lineStarts ??= buildLineStarts(text);
9061
- const target = resolveIncludeTarget(sourceArgs(macro, text, lineStarts).args);
9533
+ const target = resolveIncludeTarget2(sourceArgs(macro, text, lineStarts).args);
9062
9534
  if (target === null) continue;
9063
9535
  const passage = workspace.passages.getPassage(target);
9064
9536
  if (!passage?.tags?.includes("widget")) continue;
@@ -9084,10 +9556,15 @@ function validateUnusedWidgets(uri, workspace, diagnostics) {
9084
9556
  }
9085
9557
  }
9086
9558
  function validateLineLength(text, passages, maxLength, diagnostics) {
9087
- const excludedTags = /* @__PURE__ */ new Set(["script", "stylesheet"]);
9088
- 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
+ }
9089
9565
  const lines = text.split("\n");
9090
9566
  for (let i = 0; i < lines.length; i++) {
9567
+ if (excludedLines.has(i)) continue;
9091
9568
  const line = lines[i];
9092
9569
  if (/^::\s+/.test(line)) continue;
9093
9570
  if (/^\s*<[a-zA-Z]/.test(line)) continue;
@@ -9130,7 +9607,7 @@ function toLspDiagnostic(d) {
9130
9607
  message: d.message
9131
9608
  };
9132
9609
  }
9133
- var DEFERRED_CONTAINERS, EXPRESSION_BUILTINS, diagnosticsPlugin;
9610
+ var DEFERRED_CONTAINERS, EXPRESSION_BUILTINS2, diagnosticsPlugin;
9134
9611
  var init_diagnostics = __esm({
9135
9612
  "src/plugins/diagnostics.ts"() {
9136
9613
  "use strict";
@@ -9139,8 +9616,9 @@ var init_diagnostics = __esm({
9139
9616
  init_argument_lexer();
9140
9617
  init_parameter_validator();
9141
9618
  init_link_parser();
9619
+ init_passage_parser();
9142
9620
  DEFERRED_CONTAINERS = /* @__PURE__ */ new Set(["link", "button"]);
9143
- EXPRESSION_BUILTINS = /* @__PURE__ */ new Set([
9621
+ EXPRESSION_BUILTINS2 = /* @__PURE__ */ new Set([
9144
9622
  "currentPassage",
9145
9623
  "previousPassage",
9146
9624
  "visited",
@@ -9158,22 +9636,22 @@ var init_diagnostics = __esm({
9158
9636
  id: "diagnostics",
9159
9637
  capabilities: {},
9160
9638
  initialize(ctx) {
9161
- const publishDiagnostics = () => {
9162
- for (const uri of ctx.workspace.documents.getUris()) {
9163
- const diags = computeDiagnostics(uri, ctx.workspace);
9164
- ctx.connection.sendDiagnostics({
9165
- uri,
9166
- diagnostics: diags.map((d) => toLspDiagnostic(d))
9167
- });
9168
- }
9169
- };
9170
- ctx.workspace.on("modelReady", publishDiagnostics);
9171
- ctx.workspace.on("documentChanged", (uri) => {
9639
+ const isEnabled = (d) => ctx.config.diagnostics?.[d.code] !== false;
9640
+ const publishFor = (uri) => {
9172
9641
  const diags = computeDiagnostics(uri, ctx.workspace);
9173
9642
  ctx.connection.sendDiagnostics({
9174
9643
  uri,
9175
- diagnostics: diags.map((d) => toLspDiagnostic(d))
9644
+ diagnostics: diags.filter(isEnabled).map((d) => toLspDiagnostic(d))
9176
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: [] });
9177
9655
  });
9178
9656
  }
9179
9657
  };
@@ -9193,9 +9671,9 @@ var require_identity = __commonJS({
9193
9671
  var NODE_TYPE = Symbol.for("yaml.node.type");
9194
9672
  var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
9195
9673
  var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
9196
- var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
9674
+ var isMap2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
9197
9675
  var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
9198
- var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
9676
+ var isScalar2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
9199
9677
  var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
9200
9678
  function isCollection(node) {
9201
9679
  if (node && typeof node === "object")
@@ -9217,7 +9695,7 @@ var require_identity = __commonJS({
9217
9695
  }
9218
9696
  return false;
9219
9697
  }
9220
- var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
9698
+ var hasAnchor = (node) => (isScalar2(node) || isCollection(node)) && !!node.anchor;
9221
9699
  exports.ALIAS = ALIAS;
9222
9700
  exports.DOC = DOC;
9223
9701
  exports.MAP = MAP;
@@ -9229,10 +9707,10 @@ var require_identity = __commonJS({
9229
9707
  exports.isAlias = isAlias;
9230
9708
  exports.isCollection = isCollection;
9231
9709
  exports.isDocument = isDocument;
9232
- exports.isMap = isMap;
9710
+ exports.isMap = isMap2;
9233
9711
  exports.isNode = isNode;
9234
9712
  exports.isPair = isPair;
9235
- exports.isScalar = isScalar;
9713
+ exports.isScalar = isScalar2;
9236
9714
  exports.isSeq = isSeq;
9237
9715
  }
9238
9716
  });
@@ -13334,9 +13812,9 @@ var require_resolve_flow_collection = __commonJS({
13334
13812
  var blockMsg = "Block collections are not allowed within flow collections";
13335
13813
  var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq");
13336
13814
  function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
13337
- const isMap = fc.start.source === "{";
13338
- const fcName = isMap ? "flow map" : "flow sequence";
13339
- 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);
13340
13818
  const coll = new NodeClass(ctx.schema);
13341
13819
  coll.flow = true;
13342
13820
  const atRoot = ctx.atRoot;
@@ -13372,7 +13850,7 @@ var require_resolve_flow_collection = __commonJS({
13372
13850
  offset = props.end;
13373
13851
  continue;
13374
13852
  }
13375
- if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key))
13853
+ if (!isMap2 && ctx.options.strict && utilContainsNewline.containsNewline(key))
13376
13854
  onError(
13377
13855
  key,
13378
13856
  // checked by containsNewline()
@@ -13412,7 +13890,7 @@ var require_resolve_flow_collection = __commonJS({
13412
13890
  }
13413
13891
  }
13414
13892
  }
13415
- if (!isMap && !sep2 && !props.found) {
13893
+ if (!isMap2 && !sep2 && !props.found) {
13416
13894
  const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
13417
13895
  coll.items.push(valueNode);
13418
13896
  offset = valueNode.range[2];
@@ -13435,7 +13913,7 @@ var require_resolve_flow_collection = __commonJS({
13435
13913
  startOnNewline: false
13436
13914
  });
13437
13915
  if (valueProps.found) {
13438
- if (!isMap && !props.found && ctx.options.strict) {
13916
+ if (!isMap2 && !props.found && ctx.options.strict) {
13439
13917
  if (sep2)
13440
13918
  for (const st of sep2) {
13441
13919
  if (st === valueProps.found)
@@ -13467,7 +13945,7 @@ var require_resolve_flow_collection = __commonJS({
13467
13945
  const pair = new Pair.Pair(keyNode, valueNode);
13468
13946
  if (ctx.options.keepSourceTokens)
13469
13947
  pair.srcToken = collItem;
13470
- if (isMap) {
13948
+ if (isMap2) {
13471
13949
  const map2 = coll;
13472
13950
  if (utilMapIncludes.mapIncludes(ctx, map2.items, keyNode))
13473
13951
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
@@ -13483,7 +13961,7 @@ var require_resolve_flow_collection = __commonJS({
13483
13961
  offset = valueNode ? valueNode.range[2] : valueProps.end;
13484
13962
  }
13485
13963
  }
13486
- const expectedEnd = isMap ? "}" : "]";
13964
+ const expectedEnd = isMap2 ? "}" : "]";
13487
13965
  const [ce, ...ee] = fc.end;
13488
13966
  let cePos = offset;
13489
13967
  if (ce?.source === expectedEnd)
@@ -14766,7 +15244,7 @@ var require_cst = __commonJS({
14766
15244
  var FLOW_END = "";
14767
15245
  var SCALAR = "";
14768
15246
  var isCollection = (token) => !!token && "items" in token;
14769
- 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");
14770
15248
  function prettyToken(token) {
14771
15249
  switch (token) {
14772
15250
  case BOM:
@@ -14850,7 +15328,7 @@ var require_cst = __commonJS({
14850
15328
  exports.FLOW_END = FLOW_END;
14851
15329
  exports.SCALAR = SCALAR;
14852
15330
  exports.isCollection = isCollection;
14853
- exports.isScalar = isScalar;
15331
+ exports.isScalar = isScalar2;
14854
15332
  exports.prettyToken = prettyToken;
14855
15333
  exports.tokenType = tokenType;
14856
15334
  }
@@ -16363,7 +16841,7 @@ var require_public_api = __commonJS({
16363
16841
  return docs;
16364
16842
  return Object.assign([], { empty: true }, composer$1.streamInfo());
16365
16843
  }
16366
- function parseDocument(source, options = {}) {
16844
+ function parseDocument2(source, options = {}) {
16367
16845
  const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
16368
16846
  const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
16369
16847
  const composer$1 = new composer.Composer(options);
@@ -16389,7 +16867,7 @@ var require_public_api = __commonJS({
16389
16867
  } else if (options === void 0 && reviver && typeof reviver === "object") {
16390
16868
  options = reviver;
16391
16869
  }
16392
- const doc = parseDocument(src, options);
16870
+ const doc = parseDocument2(src, options);
16393
16871
  if (!doc)
16394
16872
  return null;
16395
16873
  doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
@@ -16425,7 +16903,7 @@ var require_public_api = __commonJS({
16425
16903
  }
16426
16904
  exports.parse = parse3;
16427
16905
  exports.parseAllDocuments = parseAllDocuments;
16428
- exports.parseDocument = parseDocument;
16906
+ exports.parseDocument = parseDocument2;
16429
16907
  exports.stringify = stringify;
16430
16908
  }
16431
16909
  });
@@ -16515,6 +16993,22 @@ function findConfigFile(workspaceRoot) {
16515
16993
  }
16516
16994
  return null;
16517
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
+ }
16518
17012
  function loadConfigFromDisk(workspaceRoot) {
16519
17013
  const configPath = findConfigFile(workspaceRoot);
16520
17014
  if (!configPath) {
@@ -16782,6 +17276,15 @@ function filterBySeverity(diagnostics, minSeverity) {
16782
17276
  async function runCheck(args2) {
16783
17277
  const options = parseArgs(args2);
16784
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
+ }
16785
17288
  const files = [];
16786
17289
  for (const pattern of options.patterns) {
16787
17290
  const matches = await glob(pattern, {
@@ -16802,12 +17305,12 @@ async function runCheck(args2) {
16802
17305
  }
16803
17306
  return 0;
16804
17307
  }
16805
- let configRoot;
16806
- if (options.configPath) {
16807
- configRoot = resolve(cwd, options.configPath, "..");
17308
+ let projectConfig;
17309
+ if (explicitConfig) {
17310
+ projectConfig = explicitConfig;
16808
17311
  } else {
16809
17312
  const dirs = uniqueFiles.map((f) => resolve(f, ".."));
16810
- configRoot = dirs.reduce((a, b) => {
17313
+ let configRoot = dirs.reduce((a, b) => {
16811
17314
  while (!b.startsWith(a)) a = resolve(a, "..");
16812
17315
  return a;
16813
17316
  });
@@ -16821,8 +17324,8 @@ async function runCheck(args2) {
16821
17324
  if (parent === search) break;
16822
17325
  search = parent;
16823
17326
  }
17327
+ projectConfig = loadConfigFromDisk(configRoot);
16824
17328
  }
16825
- const projectConfig = loadConfigFromDisk(configRoot);
16826
17329
  const workspace = new WorkspaceModel();
16827
17330
  const fileContents = /* @__PURE__ */ new Map();
16828
17331
  for (const filePath of uniqueFiles) {
@@ -16835,7 +17338,7 @@ async function runCheck(args2) {
16835
17338
  }
16836
17339
  workspace.initialize(fileContents);
16837
17340
  if (Object.keys(projectConfig.macros).length > 0) {
16838
- workspace.macros.loadSupplements(projectConfig.macros);
17341
+ workspace.macros.loadConfig(projectConfig.macros);
16839
17342
  }
16840
17343
  const diagOptions = {};
16841
17344
  if (options.maxLineLength !== null) {
@@ -16922,7 +17425,7 @@ function splitPassages(text) {
16922
17425
  function classifyPassage(header) {
16923
17426
  const tagMatch = header.match(/\[([^\]]*)\]/g);
16924
17427
  if (!tagMatch) return "normal";
16925
- const tags = tagMatch.map((t) => t.slice(1, -1).trim().toLowerCase());
17428
+ const tags = tagMatch.flatMap((t) => t.slice(1, -1).toLowerCase().split(/\s+/));
16926
17429
  if (tags.includes("script")) return "script";
16927
17430
  if (tags.includes("stylesheet")) return "stylesheet";
16928
17431
  return "normal";
@@ -16941,6 +17444,10 @@ function segmentRegions(body) {
16941
17444
  if (/^<script(\s|>)/i.test(trimmed)) {
16942
17445
  const scriptLines = [line];
16943
17446
  i++;
17447
+ if (/<\/script\s*>/i.test(line)) {
17448
+ regions.push({ type: "script", lines: scriptLines });
17449
+ continue;
17450
+ }
16944
17451
  while (i < lines.length && !/<\/script>/i.test(lines[i])) {
16945
17452
  scriptLines.push(lines[i]);
16946
17453
  i++;
@@ -17165,7 +17672,7 @@ function replaceSvgBlocks(html) {
17165
17672
  function restoreSvgBlocks(text, tokens) {
17166
17673
  let result = text;
17167
17674
  for (let i = 0; i < tokens.length; i++) {
17168
- result = result.replace(`<!--SVG:${i}-->`, tokens[i]);
17675
+ result = result.replace(`<!--SVG:${i}-->`, () => tokens[i]);
17169
17676
  }
17170
17677
  return result;
17171
17678
  }
@@ -17213,8 +17720,8 @@ function replaceSpindleTokens(html) {
17213
17720
  function restoreSpindleTokens(text, tokens) {
17214
17721
  let result = text;
17215
17722
  for (let i = 0; i < tokens.length; i++) {
17216
- result = result.replace(`<!--SP:${i}-->`, tokens[i]);
17217
- result = result.replace(`__SP${i}__`, tokens[i]);
17723
+ result = result.replace(`<!--SP:${i}-->`, () => tokens[i]);
17724
+ result = result.replace(`__SP${i}__`, () => tokens[i]);
17218
17725
  }
17219
17726
  return result;
17220
17727
  }
@@ -17257,6 +17764,7 @@ async function formatDocument(text, options) {
17257
17764
  const isDedenting = options?.isDedentingSubMacro ?? ((name) => DEFAULT_DEDENTING.has(name.toLowerCase()));
17258
17765
  const passages = splitPassages(text);
17259
17766
  const resultLines = [];
17767
+ const hardBreakLines = /* @__PURE__ */ new Set();
17260
17768
  for (let pi = 0; pi < passages.length; pi++) {
17261
17769
  const passage = passages[pi];
17262
17770
  if (pi > 0 && passage.header) {
@@ -17285,6 +17793,10 @@ async function formatDocument(text, options) {
17285
17793
  const regions = segmentRegions(passage.body);
17286
17794
  for (const region of regions) {
17287
17795
  if (region.type === "script") {
17796
+ if (region.lines.length === 1) {
17797
+ resultLines.push(region.lines[0]);
17798
+ continue;
17799
+ }
17288
17800
  const firstLine = region.lines[0];
17289
17801
  const lastLine = region.lines[region.lines.length - 1];
17290
17802
  const innerLines = region.lines.slice(1, region.lines.length - 1);
@@ -17319,49 +17831,92 @@ async function formatDocument(text, options) {
17319
17831
  if (options?.maxLineLength) {
17320
17832
  indented = wrapLines(indented, options.maxLineLength);
17321
17833
  }
17322
- resultLines.push(...indented);
17834
+ for (const line of indented) {
17835
+ if (line.endsWith(" ")) hardBreakLines.add(resultLines.length);
17836
+ resultLines.push(line);
17837
+ }
17323
17838
  }
17324
17839
  }
17325
- 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");
17326
17841
  output = output.replace(/\n*$/, "\n");
17327
17842
  return output;
17328
17843
  }
17329
17844
  function indentMacros(lines, isBlock, isDedenting) {
17330
- const result = [];
17331
- let indentLevel = 0;
17332
- for (const rawLine of lines) {
17333
- const line = rawLine.replace(/\s+$/, "");
17334
- 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();
17335
17861
  if (trimmed === "") {
17336
- result.push("");
17862
+ info.push(null);
17337
17863
  continue;
17338
17864
  }
17865
+ const col = indentColumn(lines[i]);
17339
17866
  const dedentMatch = trimmed.match(MACRO_OPEN_REGEX);
17340
17867
  if (dedentMatch && isDedenting(dedentMatch[1])) {
17341
- indentLevel = Math.max(0, indentLevel - 1);
17342
- result.push(indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed);
17343
- 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);
17344
17872
  continue;
17345
17873
  }
17346
- const closeMatch = trimmed.match(MACRO_CLOSE_REGEX);
17347
- if (closeMatch) {
17348
- indentLevel = Math.max(0, indentLevel - 1);
17349
- }
17350
- 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 });
17351
17877
  const openMatch = trimmed.match(MACRO_OPEN_REGEX);
17352
17878
  if (openMatch && isBlock(openMatch[1])) {
17353
- indentLevel++;
17879
+ openBody(i);
17354
17880
  }
17355
17881
  const closeGlobal = /\{\/[A-Za-z][\w-]*\s*\}/g;
17356
17882
  let cm;
17357
17883
  while ((cm = closeGlobal.exec(trimmed)) !== null) {
17358
- if (cm.index > 0) {
17359
- indentLevel = Math.max(0, indentLevel - 1);
17360
- }
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);
17361
17904
  }
17905
+ outCol.push(col);
17906
+ const hardBreak = / {2,}$/.test(lines[i]) && info[i + 1] ? " " : "";
17907
+ result.push(" ".repeat(col) + line.text + hardBreak);
17362
17908
  }
17363
17909
  return result;
17364
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
+ }
17365
17920
  async function formatRange(text, _range, options) {
17366
17921
  return formatDocument(text, options);
17367
17922
  }
@@ -17383,6 +17938,7 @@ function wrapLines(lines, maxLen) {
17383
17938
  const indentMatch = line.match(/^(\s*)/);
17384
17939
  const indent = indentMatch ? indentMatch[1] : "";
17385
17940
  const wrapped = wordWrap(trimmed, maxLen, indent);
17941
+ if (line.endsWith(" ")) wrapped[wrapped.length - 1] += " ";
17386
17942
  result.push(...wrapped);
17387
17943
  }
17388
17944
  return result;
@@ -17443,7 +17999,7 @@ function normalizePassageHeader(line) {
17443
17999
  }
17444
18000
  return `:: ${name}${suffix}`;
17445
18001
  }
17446
- 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;
17447
18003
  var init_format = __esm({
17448
18004
  "src/plugins/format.ts"() {
17449
18005
  "use strict";
@@ -17455,6 +18011,8 @@ var init_format = __esm({
17455
18011
  MACRO_OPEN_REGEX = /^\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\b/;
17456
18012
  MACRO_CLOSE_REGEX = /^\{\/([A-Za-z][\w-]*)\b/;
17457
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,})/;
17458
18016
  formatPlugin = {
17459
18017
  id: "format",
17460
18018
  capabilities: {
@@ -48795,7 +49353,7 @@ function createWorkspace(files) {
48795
49353
  }
48796
49354
  workspace.initialize(fileContents);
48797
49355
  if (Object.keys(projectConfig.macros).length > 0) {
48798
- workspace.macros.loadSupplements(projectConfig.macros);
49356
+ workspace.macros.loadConfig(projectConfig.macros);
48799
49357
  }
48800
49358
  return workspace;
48801
49359
  }
@@ -58061,7 +58619,7 @@ function getCompletions(uri, position, triggerChar, workspace) {
58061
58619
  if (position.line >= lines.length) return [];
58062
58620
  const lineText = lines[position.line].substring(0, position.character);
58063
58621
  if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
58064
- return getClosingMacroCompletions(text, position, workspace);
58622
+ return getClosingMacroCompletions(uri, text, position, workspace);
58065
58623
  }
58066
58624
  const transientDotPathMatch = /%([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
58067
58625
  if (transientDotPathMatch) {
@@ -58091,11 +58649,18 @@ function getCompletions(uri, position, triggerChar, workspace) {
58091
58649
  }
58092
58650
  return [];
58093
58651
  }
58094
- function getClosingMacroCompletions(text, position, workspace) {
58652
+ function getClosingMacroCompletions(uri, text, position, workspace) {
58653
+ const passages = workspace.passages.getPassagesInDocument(uri);
58095
58654
  const macros = parseMacros(text);
58096
- 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;
58097
58661
  const openStack = [];
58098
58662
  for (const macro of macros) {
58663
+ if (macro.range.start.line < passageStart) continue;
58099
58664
  if (macro.range.start.line > position.line || macro.range.start.line === position.line && macro.range.start.character >= position.character) {
58100
58665
  break;
58101
58666
  }
@@ -58146,7 +58711,7 @@ function getMacroCompletions(workspace) {
58146
58711
  kind: 3,
58147
58712
  // CompletionItemKind.Function
58148
58713
  detail: `(widget) ${widget.name}`,
58149
- 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
58150
58715
  });
58151
58716
  }
58152
58717
  return items;
@@ -58421,7 +58986,7 @@ function getWidgetHover(line, position, workspace) {
58421
58986
  return null;
58422
58987
  }
58423
58988
  function buildWidgetHover(widget, line, nameStart, nameEnd) {
58424
- 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";
58425
58990
  return {
58426
58991
  contents: `**Widget** \`${widget.name}\`
58427
58992
 
@@ -58495,7 +59060,7 @@ function getSignatureHelp(uri, position, workspace) {
58495
59060
  }
58496
59061
  const widget = workspace.widgets.getWidget(macroName);
58497
59062
  if (widget && widget.params.length > 0) {
58498
- const paramLabels = widget.params.map((p) => `@${p}`);
59063
+ const paramLabels = widget.params;
58499
59064
  return {
58500
59065
  signatures: [{
58501
59066
  label: `{${macroName} ${paramLabels.join(", ")}}`,
@@ -58551,53 +59116,21 @@ function getDefinition(uri, position, workspace) {
58551
59116
  const lines = text.split("\n");
58552
59117
  if (position.line >= lines.length) return null;
58553
59118
  const line = lines[position.line];
58554
- const linkResult = getPassageLinkDefinition(text, position, workspace);
58555
- if (linkResult) return linkResult;
58556
- const macroPassageResult = getMacroPassageDefinition(line, position, workspace);
58557
- if (macroPassageResult) return macroPassageResult;
59119
+ const passageResult = getPassageRefDefinition(text, position, workspace);
59120
+ if (passageResult) return passageResult;
58558
59121
  const widgetResult = getWidgetDefinition(line, position, workspace);
58559
59122
  if (widgetResult) return widgetResult;
58560
59123
  return null;
58561
59124
  }
58562
- function getPassageLinkDefinition(text, position, workspace) {
58563
- const links = parseLinks(text);
58564
- for (const link of links) {
58565
- if (link.range.start.line === position.line && position.character >= link.range.start.character && position.character <= link.range.end.character) {
58566
- const passage = workspace.passages.getPassage(link.name);
58567
- if (passage) {
58568
- return {
58569
- uri: passage.uri,
58570
- range: passage.headerEnd
58571
- };
58572
- }
58573
- }
58574
- }
58575
- return null;
58576
- }
58577
- function getMacroPassageDefinition(line, position, workspace) {
58578
- const patterns = [
58579
- /\{(?:goto|include)\s+"([^"]+)"\s*\}/gi,
58580
- /\{(?:link|button)\s+"[^"]*"\s+"([^"]+)"\s*\}/gi
58581
- ];
58582
- for (const pattern of patterns) {
58583
- pattern.lastIndex = 0;
58584
- let match2;
58585
- while ((match2 = pattern.exec(line)) !== null) {
58586
- const passageName = match2[1];
58587
- const nameStart = match2.index + match2[0].indexOf(passageName);
58588
- const nameEnd = nameStart + passageName.length;
58589
- if (position.character >= nameStart && position.character <= nameEnd) {
58590
- const passage = workspace.passages.getPassage(passageName);
58591
- if (passage) {
58592
- return {
58593
- uri: passage.uri,
58594
- range: passage.headerEnd
58595
- };
58596
- }
58597
- }
58598
- }
58599
- }
58600
- 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
+ };
58601
59134
  }
58602
59135
  function getWidgetDefinition(line, position, workspace) {
58603
59136
  const re = /\{([A-Za-z_$][\w$]*)/g;
@@ -58697,19 +59230,25 @@ function findReferences(uri, position, workspace, includeDeclaration) {
58697
59230
  }
58698
59231
  }
58699
59232
  {
58700
- const widgetRegex = /\{([A-Za-z_$][\w$]*)/g;
59233
+ const widgetRegex = /\{\/?([A-Za-z_$][\w$]*)/g;
58701
59234
  let match2;
58702
59235
  while ((match2 = widgetRegex.exec(line)) !== null) {
58703
59236
  const name = match2[1];
58704
- const nameStart = match2.index + 1;
59237
+ const nameStart = match2.index + match2[0].length - name.length;
58705
59238
  const nameEnd = nameStart + name.length;
58706
59239
  if (position.character >= nameStart && position.character <= nameEnd) {
58707
- 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)) {
58708
59243
  return findWidgetReferences(name, workspace, includeDeclaration);
58709
59244
  }
58710
59245
  }
58711
59246
  }
58712
59247
  }
59248
+ const passageRef = findPassageRefAt(text, position);
59249
+ if (passageRef) {
59250
+ return findPassageReferences(passageRef.name, workspace, includeDeclaration);
59251
+ }
58713
59252
  {
58714
59253
  const allPassages = workspace.passages.getAllPassages();
58715
59254
  const passageNames = new Set(allPassages.map((p) => p.name));
@@ -58735,39 +59274,16 @@ function findPassageReferences(passageName, workspace, includeDeclaration) {
58735
59274
  if (passage) {
58736
59275
  locations.push({
58737
59276
  uri: passage.uri,
58738
- range: passage.headerEnd
59277
+ range: passage.nameRange
58739
59278
  });
58740
59279
  }
58741
59280
  }
58742
- const gotoRegex = /\{(?:goto|include)\s+"([^"]+)"\s*\}/gi;
58743
- const linkMacroRegex = /\{(?:link|button)\s+"[^"]*"\s+"([^"]+)"\s*\}/gi;
58744
59281
  for (const docUri of workspace.documents.getUris()) {
58745
59282
  const docText = workspace.documents.getText(docUri);
58746
59283
  if (!docText) continue;
58747
- const links = parseLinks(docText);
58748
- for (const link of links) {
58749
- if (link.name === passageName) {
58750
- locations.push({ uri: docUri, range: link.range });
58751
- }
58752
- }
58753
- const lines = docText.split("\n");
58754
- for (let lineNum = 0; lineNum < lines.length; lineNum++) {
58755
- const line = lines[lineNum];
58756
- for (const regex of [gotoRegex, linkMacroRegex]) {
58757
- regex.lastIndex = 0;
58758
- let match2;
58759
- while ((match2 = regex.exec(line)) !== null) {
58760
- if (match2[1] === passageName) {
58761
- const nameStart = match2.index + match2[0].indexOf(match2[1]);
58762
- locations.push({
58763
- uri: docUri,
58764
- range: {
58765
- start: { line: lineNum, character: nameStart },
58766
- end: { line: lineNum, character: nameStart + match2[1].length }
58767
- }
58768
- });
58769
- }
58770
- }
59284
+ for (const ref of [...parseLinks(docText), ...parseMacroPassageRefs(docText)]) {
59285
+ if (ref.name === passageName) {
59286
+ locations.push({ uri: docUri, range: ref.range });
58771
59287
  }
58772
59288
  }
58773
59289
  }
@@ -58806,30 +59322,30 @@ function findTransientReferences(varName, workspace, includeDeclaration) {
58806
59322
  }
58807
59323
  function findWidgetReferences(widgetName, workspace, includeDeclaration) {
58808
59324
  const locations = [];
58809
- if (includeDeclaration) {
58810
- const widget = workspace.widgets.getWidget(widgetName);
58811
- if (widget) {
58812
- locations.push({ uri: widget.uri, range: widget.range });
58813
- }
59325
+ const widget = workspace.widgets.getWidget(widgetName);
59326
+ if (includeDeclaration && widget) {
59327
+ locations.push({ uri: widget.uri, range: widget.range });
58814
59328
  }
58815
- 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;
58816
59332
  for (const docUri of workspace.documents.getUris()) {
58817
59333
  const docText = workspace.documents.getText(docUri);
58818
59334
  if (!docText) continue;
58819
59335
  const lines = docText.split("\n");
58820
59336
  for (let lineNum = 0; lineNum < lines.length; lineNum++) {
58821
59337
  const line = lines[lineNum];
58822
- if (/\{widget\s+"/i.test(line)) continue;
58823
59338
  widgetInvocationRegex.lastIndex = 0;
58824
59339
  let match2;
58825
59340
  while ((match2 = widgetInvocationRegex.exec(line)) !== null) {
58826
- if (match2[1] === widgetName) {
58827
- 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);
58828
59344
  locations.push({
58829
59345
  uri: docUri,
58830
59346
  range: {
58831
59347
  start: { line: lineNum, character: nameStart },
58832
- end: { line: lineNum, character: nameStart + widgetName.length }
59348
+ end: { line: lineNum, character: nameStart + match2[2].length }
58833
59349
  }
58834
59350
  });
58835
59351
  }
@@ -58897,10 +59413,13 @@ function computeRename(uri, position, newName, workspace) {
58897
59413
  }
58898
59414
  case "variable": {
58899
59415
  const bareName = newName.startsWith("$") ? newName.slice(1) : newName.startsWith("%") ? newName.slice(1) : newName;
58900
- const isTransient = workspace.variables.getDeclaredTransient().has(symbol2.name);
58901
- 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);
58902
59417
  for (const ref of refs) {
58903
- 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);
58904
59423
  }
58905
59424
  break;
58906
59425
  }
@@ -58911,23 +59430,7 @@ function computeRename(uri, position, newName, workspace) {
58911
59430
  }
58912
59431
  const widget = workspace.widgets.getWidget(symbol2.name);
58913
59432
  if (widget) {
58914
- const docText = workspace.documents.getText(widget.uri);
58915
- if (docText) {
58916
- const lines = docText.split("\n");
58917
- const defLine = lines[widget.range.start.line];
58918
- if (defLine) {
58919
- const defSlice = defLine.slice(widget.range.start.character);
58920
- const nameMatch = /\{widget\s+"([^"]+)"/.exec(defSlice);
58921
- if (nameMatch) {
58922
- const nameOffsetInSlice = defSlice.indexOf(nameMatch[1]);
58923
- const nameStart = widget.range.start.character + nameOffsetInSlice;
58924
- addEdit(widget.uri, {
58925
- start: { line: widget.range.start.line, character: nameStart },
58926
- end: { line: widget.range.start.line, character: nameStart + symbol2.name.length }
58927
- }, newName);
58928
- }
58929
- }
58930
- }
59433
+ addEdit(widget.uri, widget.nameRange, newName);
58931
59434
  }
58932
59435
  break;
58933
59436
  }
@@ -58969,6 +59472,7 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58969
59472
  return {
58970
59473
  kind: "variable",
58971
59474
  name: match2[1],
59475
+ sigil: "$",
58972
59476
  range: {
58973
59477
  start: { line: position.line, character: start },
58974
59478
  end: { line: position.line, character: end }
@@ -58987,6 +59491,7 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58987
59491
  return {
58988
59492
  kind: "variable",
58989
59493
  name: match2[1],
59494
+ sigil: "%",
58990
59495
  range: {
58991
59496
  start: { line: position.line, character: start },
58992
59497
  end: { line: position.line, character: end }
@@ -58995,34 +59500,23 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58995
59500
  }
58996
59501
  }
58997
59502
  }
58998
- {
58999
- const widgetDefRegex2 = /\{widget\s+"([^"]+)"/gi;
59000
- let match2;
59001
- while ((match2 = widgetDefRegex2.exec(line)) !== null) {
59002
- const name = match2[1];
59003
- const nameStart = match2.index + match2[0].indexOf(name);
59004
- const nameEnd = nameStart + name.length;
59005
- if (position.character >= nameStart && position.character <= nameEnd) {
59006
- return {
59007
- kind: "widget",
59008
- name,
59009
- range: {
59010
- start: { line: position.line, character: nameStart },
59011
- end: { line: position.line, character: nameEnd }
59012
- }
59013
- };
59014
- }
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 };
59015
59507
  }
59016
59508
  }
59017
59509
  {
59018
- const re = /\{([A-Za-z_$][\w$]*)/g;
59510
+ const re = /\{\/?([A-Za-z_$][\w$]*)/g;
59019
59511
  let match2;
59020
59512
  while ((match2 = re.exec(line)) !== null) {
59021
59513
  const name = match2[1];
59022
- const nameStart = match2.index + 1;
59514
+ const nameStart = match2.index + match2[0].length - name.length;
59023
59515
  const nameEnd = nameStart + name.length;
59024
59516
  if (position.character >= nameStart && position.character <= nameEnd) {
59025
- 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)) {
59026
59520
  return {
59027
59521
  kind: "widget",
59028
59522
  name,
@@ -59116,7 +59610,7 @@ function computeCodeLenses(uri, workspace) {
59116
59610
  });
59117
59611
  continue;
59118
59612
  }
59119
- const widgetMatch = line.match(/\{widget\s+"([^"]+)"/);
59613
+ const widgetMatch = line.match(/\{widget\s+["']?([^\s"'}]+)/);
59120
59614
  if (widgetMatch) {
59121
59615
  const widgetName = widgetMatch[1];
59122
59616
  const refs = findWidgetReferences(widgetName, workspace, true);
@@ -59196,34 +59690,24 @@ function computeInlayHints(uri, range2, workspace) {
59196
59690
  return hints;
59197
59691
  }
59198
59692
  function addWidgetParamHints(text, range2, workspace, hints) {
59199
- const allWidgets = workspace.widgets.getAllWidgets();
59200
- if (allWidgets.length === 0) return;
59201
- const widgetMap = new Map(allWidgets.map((w) => [w.name, w]));
59693
+ if (workspace.widgets.getAllWidgets().length === 0) return;
59202
59694
  const macros = parseMacros(text);
59695
+ const lineStarts = buildLineStarts(text);
59203
59696
  for (const macro of macros) {
59204
59697
  if (!macro.open) continue;
59205
59698
  if (macro.range.start.line < range2.start.line || macro.range.start.line > range2.end.line) continue;
59206
- const widget = widgetMap.get(macro.name);
59699
+ const widget = workspace.widgets.getWidget(macro.name);
59207
59700
  if (!widget || widget.params.length === 0) continue;
59208
59701
  if (!macro.rawArgs || macro.rawArgs.trim() === "") continue;
59209
59702
  const args2 = lexArguments(macro.rawArgs);
59210
- const lines = text.split("\n");
59211
- const macroLine = lines[macro.range.start.line];
59212
- if (!macroLine) continue;
59213
- const macroText = macroLine.substring(macro.range.start.character, macro.range.end.character);
59214
- const nameEnd = macroText.indexOf(macro.name) + macro.name.length;
59215
- const argsText = macroText.substring(nameEnd).replace(/^\s+/, "").replace(/\}$/, "");
59216
- if (!argsText) continue;
59703
+ const macroEnd = lineStarts[macro.range.end.line] + macro.range.end.character;
59704
+ const argsStart = macroEnd - 1 - macro.rawArgs.length;
59217
59705
  for (let i = 0; i < Math.min(args2.length, widget.params.length); i++) {
59218
- const argText = args2[i].text;
59219
- const idx = macroLine.indexOf(argText, macro.range.start.character + nameEnd);
59220
- if (idx >= 0) {
59221
- hints.push({
59222
- position: { line: macro.range.start.line, character: idx },
59223
- label: `@${widget.params[i]}:`,
59224
- kind: "parameter"
59225
- });
59226
- }
59706
+ hints.push({
59707
+ position: offsetToPosition(argsStart + args2[i].start, lineStarts),
59708
+ label: `${widget.params[i]}:`,
59709
+ kind: "parameter"
59710
+ });
59227
59711
  }
59228
59712
  }
59229
59713
  }
@@ -59506,12 +59990,15 @@ var init_semantic_tokens = __esm({
59506
59990
  });
59507
59991
 
59508
59992
  // src/plugins/code-actions.ts
59509
- 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 = {}) {
59510
59997
  const actions = [];
59511
59998
  for (const diag of diagnostics) {
59512
59999
  switch (diag.code) {
59513
60000
  case DiagnosticCode.UndefinedMacro: {
59514
- const action = fixUndefinedMacro(diag);
60001
+ const action = fixUndefinedMacro(uri, diag, workspace, options);
59515
60002
  if (action) actions.push(action);
59516
60003
  break;
59517
60004
  }
@@ -59539,27 +60026,141 @@ function computeCodeActions(uri, diagnostics, workspace) {
59539
60026
  }
59540
60027
  return actions;
59541
60028
  }
59542
- function fixUndefinedMacro(diag) {
60029
+ function fixUndefinedMacro(uri, diag, workspace, options) {
59543
60030
  const match2 = diag.message.match(/\{(\w[\w-]*)\}/);
59544
60031
  if (!match2) return null;
59545
60032
  const macroName = match2[1];
59546
- const configUri = "file:///spindle.config.yaml";
59547
- const yamlSnippet = `
59548
- ${macroName}:
59549
- description: ""
59550
- `;
59551
- return {
59552
- 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)}`,
59553
60040
  kind: "quickfix",
59554
- diagnosticCodes: [DiagnosticCode.UndefinedMacro],
59555
- edits: [{
59556
- uri: configUri,
59557
- range: {
59558
- start: { line: Number.MAX_SAFE_INTEGER, character: 0 },
59559
- end: { line: Number.MAX_SAFE_INTEGER, character: 0 }
59560
- },
59561
- newText: yamlSnippet
59562
- }]
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)
59563
60164
  };
59564
60165
  }
59565
60166
  function fixUndeclaredVariable(diag, workspace) {
@@ -59669,11 +60270,15 @@ function toLspRange6(r) {
59669
60270
  end: { line: r.end.line, character: r.end.character }
59670
60271
  };
59671
60272
  }
59672
- var codeActionsPlugin;
60273
+ var import_yaml2, DEFAULT_CONFIG_FILENAME, NEW_MACRO_ENTRY, codeActionsPlugin;
59673
60274
  var init_code_actions = __esm({
59674
60275
  "src/plugins/code-actions.ts"() {
59675
60276
  "use strict";
60277
+ import_yaml2 = __toESM(require_dist(), 1);
59676
60278
  init_diagnostic_codes();
60279
+ init_config_loader();
60280
+ DEFAULT_CONFIG_FILENAME = "spindle.config.yaml";
60281
+ NEW_MACRO_ENTRY = { description: "" };
59677
60282
  codeActionsPlugin = {
59678
60283
  id: "code-actions",
59679
60284
  capabilities: {
@@ -59696,7 +60301,8 @@ var init_code_actions = __esm({
59696
60301
  const actions = computeCodeActions(
59697
60302
  params.textDocument.uri,
59698
60303
  diagnostics,
59699
- ctx.workspace
60304
+ ctx.workspace,
60305
+ { workspaceRoot: ctx.config.workspaceRoot }
59700
60306
  );
59701
60307
  return actions.map((a) => ({
59702
60308
  title: a.title,
@@ -59704,7 +60310,16 @@ var init_code_actions = __esm({
59704
60310
  diagnostics: params.context.diagnostics.filter(
59705
60311
  (d) => a.diagnosticCodes.includes(String(d.code))
59706
60312
  ),
59707
- 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
+ } : {
59708
60323
  changes: Object.fromEntries(
59709
60324
  a.edits.map((e) => [
59710
60325
  e.uri,
@@ -59744,7 +60359,7 @@ function computeDocumentSymbols(uri, workspace) {
59744
60359
  if (passage.tags?.includes("widget")) {
59745
60360
  const contentStart = passage.range.start.line + 1;
59746
60361
  const contentEnd = passage.range.end.line + 1;
59747
- const widgetDefRegex2 = /\{widget\s+"([^"]+)"[^}]*\}/gi;
60362
+ const widgetDefRegex2 = /\{widget\s+["']?([^\s"'}]+)["']?[^}]*\}/gi;
59748
60363
  for (let i = contentStart; i < contentEnd && i < lines.length; i++) {
59749
60364
  widgetDefRegex2.lastIndex = 0;
59750
60365
  let match2;
@@ -59849,14 +60464,19 @@ function searchWorkspaceSymbols(query, workspace) {
59849
60464
  range: widget.range
59850
60465
  });
59851
60466
  }
59852
- for (const [name, decl] of workspace.variables.getDeclared()) {
59853
- 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;
59854
60473
  if (lowerQuery && !displayName.toLowerCase().includes(lowerQuery)) continue;
60474
+ if (!decl.declarationUri || !decl.declarationRange) continue;
59855
60475
  results.push({
59856
60476
  name: displayName,
59857
60477
  kind: import_vscode_languageserver3.SymbolKind.Variable,
59858
- uri: decl.declarationUri ?? "",
59859
- range: decl.declarationRange ?? { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
60478
+ uri: decl.declarationUri,
60479
+ range: decl.declarationRange
59860
60480
  });
59861
60481
  }
59862
60482
  return results;
@@ -59912,7 +60532,11 @@ function computeFoldingRanges(uri, workspace) {
59912
60532
  }
59913
60533
  }
59914
60534
  const macros = parseMacros(text);
59915
- 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
+ );
59916
60540
  for (const macro of macros) {
59917
60541
  if (macro.open && macro.pair !== -1) {
59918
60542
  const closingMacro = macros[macro.pair];
@@ -60059,17 +60683,22 @@ var server_exports2 = {};
60059
60683
  __export(server_exports2, {
60060
60684
  startServer: () => startServer
60061
60685
  });
60062
- import { fileURLToPath as fileURLToPath6, pathToFileURL as pathToFileURL4 } from "node:url";
60063
- 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";
60064
60689
  function uriToFsPath(uri) {
60065
60690
  try {
60066
- return fileURLToPath6(uri);
60691
+ return fileURLToPath7(uri);
60067
60692
  } catch {
60068
60693
  return uri;
60069
60694
  }
60070
60695
  }
60071
60696
  function fsPathToUri(fsPath) {
60072
- 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.");
60073
60702
  }
60074
60703
  function startServer(_args) {
60075
60704
  const connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -60095,7 +60724,7 @@ function startServer(_args) {
60095
60724
  plugin.initialize({ connection, workspace, config: config2 });
60096
60725
  }
60097
60726
  if (Object.keys(projectConfig.macros).length > 0) {
60098
- workspace.macros.loadSupplements(projectConfig.macros);
60727
+ workspace.macros.loadConfig(projectConfig.macros);
60099
60728
  console.error("[spindle-lsp] loaded config macros:", Object.keys(projectConfig.macros).length);
60100
60729
  }
60101
60730
  return {
@@ -60119,6 +60748,9 @@ function startServer(_args) {
60119
60748
  try {
60120
60749
  const fileContents = await scanWorkspaceFiles(workspaceRoot);
60121
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
+ }
60122
60754
  workspace.initialize(fileContents);
60123
60755
  console.error("[spindle-lsp] workspace initialized, macros:", workspace.macros.getAllMacros().length);
60124
60756
  } catch (err) {
@@ -60158,23 +60790,40 @@ function startServer(_args) {
60158
60790
  if (workspace) {
60159
60791
  try {
60160
60792
  const fsPath = uriToFsPath(textDocument.uri);
60161
- const text = readFileSync7(fsPath, "utf-8");
60793
+ const text = readFileSync8(fsPath, "utf-8");
60162
60794
  workspace.documents.update(textDocument.uri, text);
60163
60795
  } catch {
60164
60796
  workspace.documents.close(textDocument.uri);
60165
60797
  }
60166
60798
  }
60167
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
+ }
60168
60812
  connection.onDidChangeWatchedFiles(({ changes }) => {
60169
60813
  if (!workspace) return;
60814
+ let configChanged = false;
60170
60815
  for (const change of changes) {
60816
+ if (isConfigFileUri(change.uri)) {
60817
+ configChanged = true;
60818
+ continue;
60819
+ }
60820
+ if (documents.has(change.uri)) continue;
60171
60821
  if (change.type === import_node.FileChangeType.Deleted) {
60172
60822
  workspace.documents.close(change.uri);
60173
- documents.delete(change.uri);
60174
60823
  } else {
60175
60824
  try {
60176
60825
  const fsPath = uriToFsPath(change.uri);
60177
- const text = readFileSync7(fsPath, "utf-8");
60826
+ const text = readFileSync8(fsPath, "utf-8");
60178
60827
  if (workspace.documents.has(change.uri)) {
60179
60828
  workspace.documents.update(change.uri, text);
60180
60829
  } else {
@@ -60184,6 +60833,9 @@ function startServer(_args) {
60184
60833
  }
60185
60834
  }
60186
60835
  }
60836
+ if (configChanged) {
60837
+ reloadProjectConfig();
60838
+ }
60187
60839
  });
60188
60840
  connection.onShutdown(() => {
60189
60841
  for (const plugin of activePlugins) {
@@ -60204,7 +60856,7 @@ async function scanWorkspaceFiles(root) {
60204
60856
  });
60205
60857
  for (const filePath of files) {
60206
60858
  try {
60207
- const text = readFileSync7(filePath, "utf-8");
60859
+ const text = readFileSync8(filePath, "utf-8");
60208
60860
  const uri = fsPathToUri(filePath);
60209
60861
  contents.set(uri, text);
60210
60862
  } catch {
@@ -60231,7 +60883,7 @@ var init_server4 = __esm({
60231
60883
 
60232
60884
  // src/bin.ts
60233
60885
  var args = process.argv.slice(2);
60234
- var VERSION = "0.9.0";
60886
+ var VERSION = "0.9.1";
60235
60887
  var HELP = `spindle-lsp v${VERSION}
60236
60888
  Language Server Protocol server for Spindle story format
60237
60889