@rohal12/spindle-lsp 0.9.0 → 0.9.2

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 +1174 -529
  2. package/dist/bin.js.map +4 -4
  3. package/package.json +5 -2
package/dist/bin.js CHANGED
@@ -3185,10 +3185,10 @@ var init_esm4 = __esm({
3185
3185
  * Return a void Promise that resolves once the stream ends.
3186
3186
  */
3187
3187
  async promise() {
3188
- return new Promise((resolve3, reject) => {
3188
+ return new Promise((resolve4, reject) => {
3189
3189
  this.on(DESTROYED, () => reject(new Error("stream destroyed")));
3190
3190
  this.on("error", (er) => reject(er));
3191
- this.on("end", () => resolve3());
3191
+ this.on("end", () => resolve4());
3192
3192
  });
3193
3193
  }
3194
3194
  /**
@@ -3212,7 +3212,7 @@ var init_esm4 = __esm({
3212
3212
  return Promise.resolve({ done: false, value: res });
3213
3213
  if (this[EOF])
3214
3214
  return stop();
3215
- let resolve3;
3215
+ let resolve4;
3216
3216
  let reject;
3217
3217
  const onerr = (er) => {
3218
3218
  this.off("data", ondata);
@@ -3226,19 +3226,19 @@ var init_esm4 = __esm({
3226
3226
  this.off("end", onend);
3227
3227
  this.off(DESTROYED, ondestroy);
3228
3228
  this.pause();
3229
- resolve3({ value, done: !!this[EOF] });
3229
+ resolve4({ value, done: !!this[EOF] });
3230
3230
  };
3231
3231
  const onend = () => {
3232
3232
  this.off("error", onerr);
3233
3233
  this.off("data", ondata);
3234
3234
  this.off(DESTROYED, ondestroy);
3235
3235
  stop();
3236
- resolve3({ done: true, value: void 0 });
3236
+ resolve4({ done: true, value: void 0 });
3237
3237
  };
3238
3238
  const ondestroy = () => onerr(new Error("stream destroyed"));
3239
3239
  return new Promise((res2, rej) => {
3240
3240
  reject = rej;
3241
- resolve3 = res2;
3241
+ resolve4 = res2;
3242
3242
  this.once(DESTROYED, ondestroy);
3243
3243
  this.once("error", onerr);
3244
3244
  this.once("end", onend);
@@ -4226,9 +4226,9 @@ var init_esm5 = __esm({
4226
4226
  if (this.#asyncReaddirInFlight) {
4227
4227
  await this.#asyncReaddirInFlight;
4228
4228
  } else {
4229
- let resolve3 = () => {
4229
+ let resolve4 = () => {
4230
4230
  };
4231
- this.#asyncReaddirInFlight = new Promise((res) => resolve3 = res);
4231
+ this.#asyncReaddirInFlight = new Promise((res) => resolve4 = res);
4232
4232
  try {
4233
4233
  for (const e of await this.#fs.promises.readdir(fullpath, {
4234
4234
  withFileTypes: true
@@ -4241,7 +4241,7 @@ var init_esm5 = __esm({
4241
4241
  children.provisional = 0;
4242
4242
  }
4243
4243
  this.#asyncReaddirInFlight = void 0;
4244
- resolve3();
4244
+ resolve4();
4245
4245
  }
4246
4246
  return children.slice(0, children.provisional);
4247
4247
  }
@@ -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
@@ -6434,27 +6439,55 @@ var init_passage_index = __esm({
6434
6439
 
6435
6440
  // src/core/workspace/macro-registry.ts
6436
6441
  import { existsSync, readFileSync as readFileSync2 } from "node:fs";
6437
- import { createRequire } from "node:module";
6438
- import { dirname, join, parse as parsePath } from "node:path";
6442
+ import { dirname, join, resolve } from "node:path";
6439
6443
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6444
+ function findRegistryPath(startDir) {
6445
+ const target = join("node_modules", "@rohal12", "spindle", "dist", "pkg", "macro-registry.json");
6446
+ let dir = resolve(startDir);
6447
+ for (; ; ) {
6448
+ const candidate = join(dir, target);
6449
+ if (existsSync(candidate)) return candidate;
6450
+ const parent = dirname(dir);
6451
+ if (parent === dir) return null;
6452
+ dir = parent;
6453
+ }
6454
+ }
6440
6455
  var MacroRegistry;
6441
6456
  var init_macro_registry = __esm({
6442
6457
  "src/core/workspace/macro-registry.ts"() {
6443
6458
  "use strict";
6444
6459
  MacroRegistry = class {
6445
6460
  macros = /* @__PURE__ */ new Map();
6461
+ /** Entries as they were before the current user config was applied (undefined = absent). */
6462
+ configSnapshot = /* @__PURE__ */ new Map();
6463
+ /** Tier 3: macros discovered from Story.defineMacro() calls, keyed by lowercase name. */
6464
+ discovered = /* @__PURE__ */ new Map();
6465
+ /** Tier 4: user config entries, kept so they can be re-applied over discovered macros. */
6466
+ configEntries = /* @__PURE__ */ new Map();
6467
+ /** Warnings collected during loadBuiltins for logging. */
6468
+ warnings = [];
6469
+ /** Path of the macro-registry.json the builtins were loaded from, if any. */
6470
+ builtinsPath = null;
6446
6471
  /**
6447
6472
  * Load built-in macro metadata from @rohal12/spindle's macro-registry.json.
6448
6473
  * This is the base layer that provides name, block, subMacros, flags, source.
6474
+ *
6475
+ * The project's own Spindle is authoritative: the registry is resolved from
6476
+ * `workspaceRoot` first, and from this package's install location (a
6477
+ * development copy, or one installed for the peer dependency) only when the
6478
+ * workspace has none.
6449
6479
  */
6450
- /** Warnings collected during loadBuiltins for logging. */
6451
- warnings = [];
6452
- loadBuiltins() {
6480
+ loadBuiltins(workspaceRoot) {
6453
6481
  let builtinMacros = [];
6454
6482
  try {
6455
6483
  const thisDir = dirname(fileURLToPath3(import.meta.url));
6456
- const registryPath = this.resolveRegistryPathViaRequire(thisDir) ?? this.resolveRegistryPath(thisDir);
6457
- if (registryPath) {
6484
+ const registryPath = (workspaceRoot ? findRegistryPath(workspaceRoot) : null) ?? findRegistryPath(thisDir);
6485
+ if (!registryPath) {
6486
+ this.warnings.push(
6487
+ "macro-registry.json not found: install @rohal12/spindle in the project. Continuing with supplements only."
6488
+ );
6489
+ } else {
6490
+ this.builtinsPath = registryPath;
6458
6491
  const raw = readFileSync2(registryPath, "utf-8");
6459
6492
  const parsed = JSON.parse(raw);
6460
6493
  if (!Array.isArray(parsed)) {
@@ -6499,10 +6532,67 @@ var init_macro_registry = __esm({
6499
6532
  /**
6500
6533
  * Overlay user config onto the registry.
6501
6534
  * Same format as supplements; applied last so it wins.
6535
+ *
6536
+ * Replaces any previously loaded config: entries from the last call are
6537
+ * reverted first, so macros removed from the config disappear (or fall
6538
+ * back to their builtin/supplement definition).
6502
6539
  */
6503
6540
  loadConfig(config2) {
6541
+ for (const [key, previous] of this.configSnapshot) {
6542
+ if (previous) {
6543
+ this.macros.set(key, previous);
6544
+ } else {
6545
+ this.macros.delete(key);
6546
+ }
6547
+ }
6548
+ this.configSnapshot.clear();
6549
+ this.configEntries.clear();
6550
+ for (const rawKey of Object.keys(config2)) {
6551
+ const key = rawKey.toLowerCase();
6552
+ if (!this.configSnapshot.has(key)) {
6553
+ const existing = this.macros.get(key);
6554
+ this.configSnapshot.set(key, existing ? { ...existing } : void 0);
6555
+ }
6556
+ this.configEntries.set(key, { ...this.configEntries.get(key), ...config2[rawKey] });
6557
+ }
6504
6558
  this.mergeEntries(config2);
6505
6559
  }
6560
+ /**
6561
+ * Replace the set of macros discovered from Story.defineMacro() calls.
6562
+ * Previously discovered definitions that are no longer present are dropped,
6563
+ * restoring whatever the other tiers define for that name.
6564
+ *
6565
+ * Mirrors Story.defineMacro(): a macro is a block when `block: true`, or
6566
+ * when it declares sub-macros and does not set `block: false`. Each
6567
+ * sub-macro becomes a known macro that may only appear inside its parent.
6568
+ */
6569
+ setDiscoveredMacros(macros) {
6570
+ this.discovered.clear();
6571
+ for (const m of macros) {
6572
+ const subMacros = m.subMacros ?? [];
6573
+ this.discovered.set(m.name.toLowerCase(), {
6574
+ name: m.name,
6575
+ block: m.block === true || m.block !== false && subMacros.length > 0,
6576
+ subMacros,
6577
+ storeVar: m.storeVar,
6578
+ interpolate: m.interpolate,
6579
+ merged: m.merged,
6580
+ description: m.description,
6581
+ source: "user"
6582
+ });
6583
+ }
6584
+ for (const m of macros) {
6585
+ for (const sub of m.subMacros ?? []) {
6586
+ const key = sub.toLowerCase();
6587
+ const existing = this.discovered.get(key);
6588
+ if (existing) {
6589
+ existing.parents = [...existing.parents ?? [], m.name];
6590
+ } else {
6591
+ this.discovered.set(key, { name: sub, block: false, subMacros: [], source: "user", parents: [m.name] });
6592
+ }
6593
+ }
6594
+ }
6595
+ }
6506
6596
  /** Add or replace a single macro entry. */
6507
6597
  addMacro(info) {
6508
6598
  const key = info.name.toLowerCase();
@@ -6524,51 +6614,54 @@ var init_macro_registry = __esm({
6524
6614
  }
6525
6615
  /** Get a macro by name (case-insensitive). */
6526
6616
  getMacro(name) {
6527
- return this.macros.get(name.toLowerCase());
6617
+ return this.resolve(name.toLowerCase());
6528
6618
  }
6529
6619
  /** Check if a macro is a block (container) macro. */
6530
6620
  isBlock(name) {
6531
- return this.macros.get(name.toLowerCase())?.block ?? false;
6621
+ return this.getMacro(name)?.block ?? false;
6532
6622
  }
6533
6623
  /** Check if a macro is a sub-macro (has parents). */
6534
6624
  isSubMacro(name) {
6535
- const info = this.macros.get(name.toLowerCase());
6625
+ const info = this.getMacro(name);
6536
6626
  return info?.parents != null && info.parents.length > 0;
6537
6627
  }
6538
6628
  /** Get all registered macros. */
6539
6629
  getAllMacros() {
6540
- return Array.from(this.macros.values());
6630
+ const keys = /* @__PURE__ */ new Set([...this.macros.keys(), ...this.discovered.keys()]);
6631
+ return Array.from(keys, (key) => this.resolve(key));
6541
6632
  }
6542
6633
  /**
6543
- * Walk up directory tree from `startDir` to find the
6544
- * @rohal12/spindle/dist/pkg/macro-registry.json file.
6634
+ * Compose the effective entry for a lowercase key: builtins/supplements/
6635
+ * config (stored in `macros`), overlaid by discovered metadata, with user
6636
+ * config re-applied on top so it keeps precedence.
6545
6637
  */
6546
- /**
6547
- * Resolve macro-registry.json using Node's require resolution.
6548
- * Works in bundled contexts where import.meta.url may not be near node_modules.
6549
- */
6550
- resolveRegistryPathViaRequire(startDir) {
6551
- try {
6552
- const require2 = createRequire(join(startDir, "_"));
6553
- const toolingPath = require2.resolve("@rohal12/spindle/tooling");
6554
- const candidate = join(dirname(toolingPath), "macro-registry.json");
6555
- if (existsSync(candidate)) return candidate;
6556
- } catch {
6557
- }
6558
- return null;
6559
- }
6560
- resolveRegistryPath(startDir) {
6561
- const target = join("node_modules", "@rohal12", "spindle", "dist", "pkg", "macro-registry.json");
6562
- let dir = startDir;
6563
- const { root } = parsePath(dir);
6564
- while (dir !== root) {
6565
- const candidate = join(dir, target);
6566
- if (existsSync(candidate)) {
6567
- return candidate;
6568
- }
6569
- dir = dirname(dir);
6638
+ resolve(key) {
6639
+ const base = this.macros.get(key);
6640
+ const found = this.discovered.get(key);
6641
+ if (!found) return base;
6642
+ const info = {
6643
+ ...base,
6644
+ name: base?.name ?? found.name,
6645
+ block: found.block ?? base?.block ?? false,
6646
+ subMacros: found.subMacros?.length ? found.subMacros : base?.subMacros ?? [],
6647
+ source: "user",
6648
+ storeVar: found.storeVar ?? base?.storeVar,
6649
+ interpolate: found.interpolate ?? base?.interpolate,
6650
+ merged: found.merged ?? base?.merged,
6651
+ description: found.description ?? base?.description,
6652
+ // A discovered sub-macro may also belong to other (e.g. built-in) parents
6653
+ parents: found.parents ? [.../* @__PURE__ */ new Set([...base?.parents ?? [], ...found.parents])] : base?.parents
6654
+ };
6655
+ const config2 = this.configEntries.get(key);
6656
+ if (config2) {
6657
+ if (config2.description !== void 0) info.description = config2.description;
6658
+ if (config2.parameters !== void 0) info.parameters = config2.parameters;
6659
+ if (config2.children !== void 0) info.children = config2.children;
6660
+ if (config2.parents !== void 0) info.parents = config2.parents;
6661
+ if (config2.skipArgs !== void 0) info.skipArgs = config2.skipArgs;
6662
+ if (config2.container !== void 0) info.block = config2.container;
6570
6663
  }
6571
- return null;
6664
+ return info;
6572
6665
  }
6573
6666
  /** Merge a set of supplement/config entries into the registry. */
6574
6667
  mergeEntries(entries) {
@@ -6647,6 +6740,17 @@ function inferLiteralType(expr) {
6647
6740
  if (first === "{") return "object";
6648
6741
  return "string";
6649
6742
  }
6743
+ function declarationLocation(line, name, absLine, uri) {
6744
+ const start = line.length - line.trimStart().length;
6745
+ const location = {
6746
+ declarationRange: {
6747
+ start: { line: absLine, character: start },
6748
+ end: { line: absLine, character: start + 1 + name.length }
6749
+ }
6750
+ };
6751
+ if (uri !== void 0) location.declarationUri = uri;
6752
+ return location;
6753
+ }
6650
6754
  var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, ARRAY_MEMBERS, VariableTracker;
6651
6755
  var init_variable_tracker = __esm({
6652
6756
  "src/core/workspace/variable-tracker.ts"() {
@@ -6705,7 +6809,7 @@ var init_variable_tracker = __esm({
6705
6809
  * Parse the StoryVariables passage content for declarations.
6706
6810
  * Each line like `$name = value` becomes a declaration.
6707
6811
  */
6708
- parseStoryVariables(content, contentStartLine = 0) {
6812
+ parseStoryVariables(content, contentStartLine = 0, uri) {
6709
6813
  this.declared.clear();
6710
6814
  this._hasStoryVariables = true;
6711
6815
  this._nullDeclarations = [];
@@ -6717,9 +6821,10 @@ var init_variable_tracker = __esm({
6717
6821
  if (!match2) continue;
6718
6822
  const name = match2[1];
6719
6823
  const expr = match2[2].trim();
6824
+ const absLine = contentStartLine + i;
6825
+ const location = declarationLocation(lines[i], name, absLine, uri);
6720
6826
  if (expr === "null") {
6721
6827
  const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
6722
- const absLine = contentStartLine + i;
6723
6828
  this._nullDeclarations.push({
6724
6829
  name,
6725
6830
  sigil: "$",
@@ -6728,10 +6833,10 @@ var init_variable_tracker = __esm({
6728
6833
  end: { line: absLine, character: charIdx + 4 }
6729
6834
  }
6730
6835
  });
6731
- this.declared.set(name, { name, sigil: "$" });
6836
+ this.declared.set(name, { name, sigil: "$", ...location });
6732
6837
  continue;
6733
6838
  }
6734
- const decl = { name, sigil: "$" };
6839
+ const decl = { name, sigil: "$", ...location };
6735
6840
  const type = inferLiteralType(expr);
6736
6841
  if (type) decl.type = type;
6737
6842
  if (expr.startsWith("{")) {
@@ -6748,11 +6853,23 @@ var init_variable_tracker = __esm({
6748
6853
  this.declared.set(name, decl);
6749
6854
  }
6750
6855
  }
6856
+ /** Forget StoryVariables declarations, e.g. after the passage was removed. */
6857
+ clearStoryVariables() {
6858
+ this.declared.clear();
6859
+ this._hasStoryVariables = false;
6860
+ this._nullDeclarations = [];
6861
+ }
6862
+ /** Forget StoryTransients declarations, e.g. after the passage was removed. */
6863
+ clearStoryTransients() {
6864
+ this.declaredTransient.clear();
6865
+ this._hasStoryTransients = false;
6866
+ this._nullTransientDeclarations = [];
6867
+ }
6751
6868
  /**
6752
6869
  * Parse the StoryTransients passage content for declarations.
6753
6870
  * Each line like `%name = value` becomes a declaration.
6754
6871
  */
6755
- parseStoryTransients(content, contentStartLine = 0) {
6872
+ parseStoryTransients(content, contentStartLine = 0, uri) {
6756
6873
  this.declaredTransient.clear();
6757
6874
  this._hasStoryTransients = true;
6758
6875
  this._nullTransientDeclarations = [];
@@ -6764,9 +6881,10 @@ var init_variable_tracker = __esm({
6764
6881
  if (!match2) continue;
6765
6882
  const name = match2[1];
6766
6883
  const expr = match2[2].trim();
6884
+ const absLine = contentStartLine + i;
6885
+ const location = declarationLocation(lines[i], name, absLine, uri);
6767
6886
  if (expr === "null") {
6768
6887
  const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
6769
- const absLine = contentStartLine + i;
6770
6888
  this._nullTransientDeclarations.push({
6771
6889
  name,
6772
6890
  sigil: "%",
@@ -6775,10 +6893,10 @@ var init_variable_tracker = __esm({
6775
6893
  end: { line: absLine, character: charIdx + 4 }
6776
6894
  }
6777
6895
  });
6778
- this.declaredTransient.set(name, { name, sigil: "%" });
6896
+ this.declaredTransient.set(name, { name, sigil: "%", ...location });
6779
6897
  continue;
6780
6898
  }
6781
- const decl = { name, sigil: "%" };
6899
+ const decl = { name, sigil: "%", ...location };
6782
6900
  const type = inferLiteralType(expr);
6783
6901
  if (type) decl.type = type;
6784
6902
  if (expr.startsWith("{")) {
@@ -6809,19 +6927,20 @@ var init_variable_tracker = __esm({
6809
6927
  for (let i = 0; i < lines.length; i++) {
6810
6928
  const header = parsePassageHeader(lines[i], i);
6811
6929
  if (header) {
6812
- passageBoundaries.push({ name: header.name, startLine: i });
6930
+ passageBoundaries.push({ name: header.name, tags: header.tags, startLine: i });
6813
6931
  }
6814
6932
  }
6815
6933
  for (let pi = 0; pi < passageBoundaries.length; pi++) {
6816
6934
  const passage = passageBoundaries[pi];
6817
6935
  if (EXCLUDED_PASSAGES.has(passage.name)) continue;
6936
+ if (isScriptOrStylesheetPassage(passage)) continue;
6818
6937
  const contentStartLine = passage.startLine + 1;
6819
6938
  const contentEndLine = pi + 1 < passageBoundaries.length ? passageBoundaries[pi + 1].startLine : lines.length;
6820
6939
  const contentLines = lines.slice(contentStartLine, contentEndLine);
6821
6940
  const content = contentLines.join("\n");
6822
6941
  let cleaned = content;
6823
6942
  for (const pattern of CLEAN_PATTERNS) {
6824
- cleaned = cleaned.replace(pattern, (m) => " ".repeat(m.length));
6943
+ cleaned = cleaned.replace(pattern, (m) => m.replace(/[^\r\n]/g, " "));
6825
6944
  }
6826
6945
  const lineOffsets = [0];
6827
6946
  for (let i = 0; i < cleaned.length; i++) {
@@ -6976,14 +7095,114 @@ var init_variable_tracker = __esm({
6976
7095
  }
6977
7096
  });
6978
7097
 
7098
+ // src/core/parsing/macro-parser.ts
7099
+ function buildLineStarts(text) {
7100
+ const starts = [0];
7101
+ for (let i = 0; i < text.length; i++) {
7102
+ if (text[i] === "\n") {
7103
+ starts.push(i + 1);
7104
+ }
7105
+ }
7106
+ return starts;
7107
+ }
7108
+ function offsetToPosition(offset, lineStarts) {
7109
+ let low = 0;
7110
+ let high = lineStarts.length - 1;
7111
+ while (low < high) {
7112
+ const mid = low + high + 1 >> 1;
7113
+ if (lineStarts[mid] <= offset) {
7114
+ low = mid;
7115
+ } else {
7116
+ high = mid - 1;
7117
+ }
7118
+ }
7119
+ return { line: low, character: offset - lineStarts[low] };
7120
+ }
7121
+ function parseMacros(text) {
7122
+ const cleaned = text.replace(variableInterpolationRegex, (match3) => {
7123
+ return " ".repeat(match3.length);
7124
+ });
7125
+ const lineStarts = buildLineStarts(text);
7126
+ const macros = [];
7127
+ let id = 0;
7128
+ macroRegex.lastIndex = 0;
7129
+ let match2;
7130
+ while ((match2 = macroRegex.exec(cleaned)) !== null) {
7131
+ const closeSlash = match2[1];
7132
+ const cssPrefix = (match2[2] || "").trim();
7133
+ const macroName = match2[3];
7134
+ const rawArgs = match2[4];
7135
+ const open = closeSlash !== "/";
7136
+ const matchStart = match2.index;
7137
+ const matchEnd = matchStart + match2[0].length;
7138
+ const startPos = offsetToPosition(matchStart, lineStarts);
7139
+ const endPos = offsetToPosition(matchEnd, lineStarts);
7140
+ const range2 = {
7141
+ start: startPos,
7142
+ end: endPos
7143
+ };
7144
+ macros.push({
7145
+ id,
7146
+ pair: -1,
7147
+ name: macroName,
7148
+ open,
7149
+ range: range2,
7150
+ cssPrefix: cssPrefix || void 0,
7151
+ rawArgs: rawArgs || void 0
7152
+ });
7153
+ id++;
7154
+ }
7155
+ return macros;
7156
+ }
7157
+ function pairMacros(macros, isBlock, passageStartLines = []) {
7158
+ const boundaries = [...passageStartLines].sort((a, b) => a - b);
7159
+ let nextBoundary = 0;
7160
+ let stack = [];
7161
+ for (const macro of macros) {
7162
+ let crossed = false;
7163
+ while (nextBoundary < boundaries.length && macro.range.start.line >= boundaries[nextBoundary]) {
7164
+ nextBoundary++;
7165
+ crossed = true;
7166
+ }
7167
+ if (crossed) stack = [];
7168
+ if (!isBlock(macro.name)) continue;
7169
+ if (macro.open) {
7170
+ stack.push(macro);
7171
+ continue;
7172
+ }
7173
+ const name = macro.name.toLowerCase();
7174
+ for (let i = stack.length - 1; i >= 0; i--) {
7175
+ if (stack[i].name.toLowerCase() === name) {
7176
+ const opener = stack[i];
7177
+ opener.pair = macro.id;
7178
+ macro.pair = opener.id;
7179
+ stack.length = i;
7180
+ break;
7181
+ }
7182
+ }
7183
+ }
7184
+ }
7185
+ var variableInterpolationRegex, macroRegex;
7186
+ var init_macro_parser = __esm({
7187
+ "src/core/parsing/macro-parser.ts"() {
7188
+ "use strict";
7189
+ variableInterpolationRegex = /(?<!\\)\{([$_@%][A-Za-z_$][\w$.]*)\}/g;
7190
+ macroRegex = /(?<!\\)\{(\/)?(?:((?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*))(?:\s+((?:(?:`(?:\\.|[^`\\])*?`)|(?:"(?:\\.|[^"\\])*?")|(?:'(?:\\.|[^'\\])*?')|(?:\[(?:[<>]?[Ii][Mm][Gg])?\[[^\r\n]*?\]\]+)|[^}]|(?:}))*?))?\}/gmi;
7191
+ }
7192
+ });
7193
+
6979
7194
  // src/core/workspace/widget-registry.ts
6980
- var widgetDefRegex, paramRegex, WidgetRegistry;
7195
+ function parseParams(argString) {
7196
+ return argString.trim().split(/\s+/).filter((t) => /^[$_@]/.test(t) && t !== "@children");
7197
+ }
7198
+ var widgetDefRegex, WidgetRegistry;
6981
7199
  var init_widget_registry = __esm({
6982
7200
  "src/core/workspace/widget-registry.ts"() {
6983
7201
  "use strict";
6984
- widgetDefRegex = /\{widget\s+"([^"]+)"((?:\s+@[A-Za-z_$][\w$]*)*)\s*\}/gi;
6985
- paramRegex = /@([A-Za-z_$][\w$]*)/g;
7202
+ init_macro_parser();
7203
+ widgetDefRegex = /(\{widget\s+["']?)([^\s"'}]+)["']?([^}]*)\}/gi;
6986
7204
  WidgetRegistry = class {
7205
+ /** Widgets keyed by lower-cased name; Spindle resolves widgets case-insensitively. */
6987
7206
  widgets = /* @__PURE__ */ new Map();
6988
7207
  /** Per-URI set of lower-cased macro names opened in that document. */
6989
7208
  invokedByUri = /* @__PURE__ */ new Map();
@@ -7009,40 +7228,43 @@ var init_widget_registry = __esm({
7009
7228
  break;
7010
7229
  }
7011
7230
  }
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
- }
7231
+ const content = lines.slice(contentStartLine, contentEndLine).join("\n");
7232
+ const lineStarts = buildLineStarts(content);
7233
+ const toPosition = (offset) => {
7234
+ const pos = offsetToPosition(offset, lineStarts);
7235
+ return { line: contentStartLine + pos.line, character: pos.character };
7236
+ };
7237
+ widgetDefRegex.lastIndex = 0;
7238
+ let match2;
7239
+ while ((match2 = widgetDefRegex.exec(content)) !== null) {
7240
+ const widgetName = match2[2];
7241
+ const nameStart = match2.index + match2[1].length;
7242
+ const range2 = {
7243
+ start: toPosition(match2.index),
7244
+ end: toPosition(match2.index + match2[0].length)
7245
+ };
7246
+ const nameRange = {
7247
+ start: toPosition(nameStart),
7248
+ end: toPosition(nameStart + widgetName.length)
7249
+ };
7250
+ const bodyStart = match2.index + match2[0].length;
7251
+ const rest = content.slice(bodyStart);
7252
+ const closeIdx = rest.search(/\{\/widget\}/i);
7253
+ const body = closeIdx >= 0 ? rest.slice(0, closeIdx) : rest;
7254
+ this.widgets.set(widgetName.toLowerCase(), {
7255
+ name: widgetName,
7256
+ params: parseParams(match2[3]),
7257
+ uri: passage.uri,
7258
+ range: range2,
7259
+ nameRange,
7260
+ block: /\{@children\}/.test(body)
7261
+ });
7040
7262
  }
7041
7263
  }
7042
7264
  }
7043
- /** Get a widget definition by name. */
7265
+ /** Get a widget definition by name (case-insensitive). */
7044
7266
  getWidget(name) {
7045
- return this.widgets.get(name);
7267
+ return this.widgets.get(name.toLowerCase());
7046
7268
  }
7047
7269
  /** Get all registered widget definitions. */
7048
7270
  getAllWidgets() {
@@ -7076,89 +7298,111 @@ var init_widget_registry = __esm({
7076
7298
  }
7077
7299
  });
7078
7300
 
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);
7301
+ // src/core/parsing/macro-discovery.ts
7302
+ function extractBalancedBraces(source, start) {
7303
+ let depth = 0;
7304
+ let inSingleQuote = false;
7305
+ let inDoubleQuote = false;
7306
+ let inBacktick = false;
7307
+ for (let i = start; i < source.length; i++) {
7308
+ const ch = source[i];
7309
+ const prev = i > start ? source[i - 1] : "";
7310
+ if (prev === "\\") continue;
7311
+ if (inSingleQuote) {
7312
+ if (ch === "'") inSingleQuote = false;
7313
+ continue;
7314
+ }
7315
+ if (inDoubleQuote) {
7316
+ if (ch === '"') inDoubleQuote = false;
7317
+ continue;
7318
+ }
7319
+ if (inBacktick) {
7320
+ if (ch === "`") inBacktick = false;
7321
+ continue;
7322
+ }
7323
+ if (ch === "'") {
7324
+ inSingleQuote = true;
7325
+ } else if (ch === '"') {
7326
+ inDoubleQuote = true;
7327
+ } else if (ch === "`") {
7328
+ inBacktick = true;
7329
+ } else if (ch === "{") {
7330
+ depth++;
7331
+ } else if (ch === "}") {
7332
+ depth--;
7333
+ if (depth === 0) {
7334
+ return source.slice(start, i + 1);
7335
+ }
7085
7336
  }
7086
7337
  }
7087
- return starts;
7338
+ return null;
7088
7339
  }
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;
7340
+ function parseConfigFields(config2) {
7341
+ const nameMatch = config2.match(/name:\s*['"]([^'"]+)['"]/);
7342
+ if (!nameMatch) return null;
7343
+ const macro = {
7344
+ name: nameMatch[1]
7345
+ };
7346
+ const boolFields = [
7347
+ "block",
7348
+ "storeVar",
7349
+ "merged",
7350
+ "interpolate"
7351
+ ];
7352
+ for (const field of boolFields) {
7353
+ const re = new RegExp(`${field}:\\s*(true|false)`);
7354
+ const m = config2.match(re);
7355
+ if (m) {
7356
+ macro[field] = m[1] === "true";
7098
7357
  }
7099
7358
  }
7100
- return { line: low, character: offset - lineStarts[low] };
7359
+ const descMatch = config2.match(/description:\s*['"]([^'"]+)['"]/);
7360
+ if (descMatch) {
7361
+ macro.description = descMatch[1];
7362
+ }
7363
+ const subMatch = config2.match(/subMacros:\s*\[([^\]]*)\]/);
7364
+ if (subMatch) {
7365
+ const inner = subMatch[1].trim();
7366
+ if (inner) {
7367
+ macro.subMacros = inner.split(",").map(
7368
+ (s) => s.trim().replace(/^['"]|['"]$/g, "")
7369
+ ).filter((s) => s.length > 0);
7370
+ }
7371
+ }
7372
+ return macro;
7101
7373
  }
7102
- function parseMacros(text) {
7103
- const cleaned = text.replace(variableInterpolationRegex, (match3) => {
7104
- return " ".repeat(match3.length);
7105
- });
7106
- const lineStarts = buildLineStarts(text);
7374
+ function discoverMacrosFromSource(source) {
7107
7375
  const macros = [];
7108
- let id = 0;
7109
- macroRegex.lastIndex = 0;
7376
+ defineMacroRegex.lastIndex = 0;
7110
7377
  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++;
7378
+ while ((match2 = defineMacroRegex.exec(source)) !== null) {
7379
+ const braceStart = match2.index + match2[0].length - 1;
7380
+ const config2 = extractBalancedBraces(source, braceStart);
7381
+ if (!config2) continue;
7382
+ const macro = parseConfigFields(config2);
7383
+ if (macro) {
7384
+ macros.push(macro);
7385
+ }
7135
7386
  }
7136
7387
  return macros;
7137
7388
  }
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
- }
7389
+ function discoverMacrosFromStoryInit(passageContent2) {
7390
+ const macros = [];
7391
+ doBlockRegex.lastIndex = 0;
7392
+ let doMatch;
7393
+ while ((doMatch = doBlockRegex.exec(passageContent2)) !== null) {
7394
+ const jsContent = doMatch[1];
7395
+ const found = discoverMacrosFromSource(jsContent);
7396
+ macros.push(...found);
7154
7397
  }
7398
+ return macros;
7155
7399
  }
7156
- var variableInterpolationRegex, macroRegex;
7157
- var init_macro_parser = __esm({
7158
- "src/core/parsing/macro-parser.ts"() {
7400
+ var defineMacroRegex, doBlockRegex;
7401
+ var init_macro_discovery = __esm({
7402
+ "src/core/parsing/macro-discovery.ts"() {
7159
7403
  "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;
7404
+ defineMacroRegex = /Story\.defineMacro\s*\(\s*\{/g;
7405
+ doBlockRegex = /\{do\}([\s\S]*?)\{\/do\}/g;
7162
7406
  }
7163
7407
  });
7164
7408
 
@@ -7352,7 +7596,7 @@ var init_macro_supplements = __esm({
7352
7596
  },
7353
7597
  include: {
7354
7598
  name: "include",
7355
- description: 'Outputs the contents of the specified passage.\n\nUsage: `{include "passage"}`',
7599
+ 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
7600
  parameters: ["text"]
7357
7601
  },
7358
7602
  passage: {
@@ -7362,28 +7606,28 @@ var init_macro_supplements = __esm({
7362
7606
  },
7363
7607
  textbox: {
7364
7608
  name: "textbox",
7365
- description: 'Creates a text input box.\n\nUsage: `{textbox "$receiver" defaultValue}`',
7366
- parameters: ["receiver &+ text"]
7609
+ description: 'Creates a text input box bound to a story variable.\n\nUsage: `{textbox $receiver}` or `{textbox $receiver "placeholder"}`',
7610
+ parameters: ["receiver |+ text"]
7367
7611
  },
7368
7612
  numberbox: {
7369
7613
  name: "numberbox",
7370
- description: 'Creates a number input box.\n\nUsage: `{numberbox "$receiver" defaultValue}`',
7371
- parameters: ["receiver &+ text"]
7614
+ description: 'Creates a number input box bound to a story variable.\n\nUsage: `{numberbox $receiver}` or `{numberbox $receiver "placeholder"}`',
7615
+ parameters: ["receiver |+ text"]
7372
7616
  },
7373
7617
  textarea: {
7374
7618
  name: "textarea",
7375
- description: 'Creates a multi-line text input area.\n\nUsage: `{textarea "$receiver" defaultValue}`',
7376
- parameters: ["receiver &+ text"]
7619
+ description: 'Creates a multi-line text input area bound to a story variable.\n\nUsage: `{textarea $receiver}` or `{textarea $receiver "placeholder"}`',
7620
+ parameters: ["receiver |+ text"]
7377
7621
  },
7378
7622
  checkbox: {
7379
7623
  name: "checkbox",
7380
- description: 'Creates a checkbox input.\n\nUsage: `{checkbox "$receiver" uncheckedValue checkedValue}`',
7381
- parameters: ["receiver &+ text"]
7624
+ description: 'Creates a checkbox that toggles a boolean story variable.\n\nUsage: `{checkbox $receiver "label"}`',
7625
+ parameters: ["receiver |+ text"]
7382
7626
  },
7383
7627
  radiobutton: {
7384
7628
  name: "radiobutton",
7385
- description: 'Creates a radio button input.\n\nUsage: `{radiobutton "$receiver" value}`',
7386
- parameters: ["receiver &+ text &+ text"]
7629
+ 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"}`',
7630
+ parameters: ["receiver &+ text |+ text"]
7387
7631
  },
7388
7632
  quicksave: {
7389
7633
  name: "quicksave",
@@ -7413,8 +7657,8 @@ var init_macro_supplements = __esm({
7413
7657
  dialog: {
7414
7658
  name: "dialog",
7415
7659
  container: true,
7416
- description: 'Opens a dialog with its contents.\n\nUsage:\n\n```\n{dialog "title"}\n ...\n{/dialog}\n```',
7417
- parameters: ["text"]
7660
+ 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```',
7661
+ parameters: ["text |+ 'noclose'"]
7418
7662
  },
7419
7663
  nobr: {
7420
7664
  name: "nobr",
@@ -7470,6 +7714,7 @@ var init_workspace_model = __esm({
7470
7714
  init_variable_tracker();
7471
7715
  init_widget_registry();
7472
7716
  init_macro_parser();
7717
+ init_macro_discovery();
7473
7718
  init_macro_supplements();
7474
7719
  WorkspaceModel = class extends EventEmitter3 {
7475
7720
  documents;
@@ -7492,7 +7737,7 @@ var init_workspace_model = __esm({
7492
7737
  this.macros = new MacroRegistry();
7493
7738
  this.variables = new VariableTracker();
7494
7739
  this.widgets = new WidgetRegistry();
7495
- this.macros.loadBuiltins();
7740
+ this.macros.loadBuiltins(config2?.workspaceRoot);
7496
7741
  this.macros.loadSupplements(macro_supplements_default);
7497
7742
  this.onDocumentChanged = (uri) => this.handleDocumentChange(uri);
7498
7743
  this.onDocumentOpened = (uri) => this.handleDocumentChange(uri);
@@ -7517,6 +7762,14 @@ var init_workspace_model = __esm({
7517
7762
  this.initialized = true;
7518
7763
  this.scheduleModelReady();
7519
7764
  }
7765
+ /**
7766
+ * Re-run cross-document analysis and schedule 'modelReady', e.g. after
7767
+ * the macro configuration changed without any document changing.
7768
+ */
7769
+ refresh() {
7770
+ this.cascade();
7771
+ this.scheduleModelReady();
7772
+ }
7520
7773
  /** Clean up listeners and timers. */
7521
7774
  dispose() {
7522
7775
  this.documents.removeListener("documentChanged", this.onDocumentChanged);
@@ -7534,6 +7787,7 @@ var init_workspace_model = __esm({
7534
7787
  if (text !== void 0) {
7535
7788
  this.passages.rebuild(uri, text);
7536
7789
  }
7790
+ this.refreshDiscoveredMacros();
7537
7791
  this.cascade();
7538
7792
  this.emit("documentChanged", uri);
7539
7793
  this.emit("passagesUpdated", uri);
@@ -7542,6 +7796,7 @@ var init_workspace_model = __esm({
7542
7796
  /** Handle a document close: remove its passages and cascade. */
7543
7797
  handleDocumentClose(uri) {
7544
7798
  this.passages.remove(uri);
7799
+ this.refreshDiscoveredMacros();
7545
7800
  this.cascade();
7546
7801
  this.emit("documentClosed", uri);
7547
7802
  this.emit("passagesUpdated", uri);
@@ -7555,8 +7810,39 @@ var init_workspace_model = __esm({
7555
7810
  this.passages.rebuild(uri, text);
7556
7811
  }
7557
7812
  }
7813
+ this.refreshDiscoveredMacros();
7558
7814
  this.cascade();
7559
7815
  }
7816
+ /**
7817
+ * Re-run static macro discovery over the workspace and replace the
7818
+ * registry's discovered tier (dropping definitions whose source is gone).
7819
+ *
7820
+ * Sources scanned for Story.defineMacro({...}) calls:
7821
+ * - `{do}` blocks in StoryInit passages
7822
+ * - passages tagged `script` (Story JavaScript)
7823
+ * - JavaScript/TypeScript documents in the workspace
7824
+ */
7825
+ refreshDiscoveredMacros() {
7826
+ const found = [];
7827
+ for (const uri of this.documents.getUris()) {
7828
+ const text = this.documents.getText(uri);
7829
+ if (!text) continue;
7830
+ if (/\.[cm]?[jt]s$/i.test(uri)) {
7831
+ found.push(...discoverMacrosFromSource(text));
7832
+ continue;
7833
+ }
7834
+ let lines;
7835
+ for (const passage of this.passages.getPassagesInDocument(uri)) {
7836
+ const isStoryInit = passage.name === "StoryInit";
7837
+ const isScript = passage.tags?.includes("script") ?? false;
7838
+ if (!isStoryInit && !isScript) continue;
7839
+ lines ??= text.split("\n");
7840
+ const content = lines.slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
7841
+ found.push(...isScript ? discoverMacrosFromSource(content) : discoverMacrosFromStoryInit(content));
7842
+ }
7843
+ }
7844
+ this.macros.setDiscoveredMacros(found);
7845
+ }
7560
7846
  /**
7561
7847
  * Cascade: rescan variables and widgets based on current passages.
7562
7848
  * Called after any passage index update.
@@ -7576,8 +7862,10 @@ var init_workspace_model = __esm({
7576
7862
  }
7577
7863
  }
7578
7864
  const content = lines.slice(contentStart, contentEnd).join("\n");
7579
- this.variables.parseStoryVariables(content, contentStart);
7865
+ this.variables.parseStoryVariables(content, contentStart, storyVars.uri);
7580
7866
  }
7867
+ } else {
7868
+ this.variables.clearStoryVariables();
7581
7869
  }
7582
7870
  const storyTransients = this.passages.getStoryTransients();
7583
7871
  if (storyTransients) {
@@ -7593,8 +7881,10 @@ var init_workspace_model = __esm({
7593
7881
  }
7594
7882
  }
7595
7883
  const content = lines.slice(contentStart, contentEnd).join("\n");
7596
- this.variables.parseStoryTransients(content, contentStart);
7884
+ this.variables.parseStoryTransients(content, contentStart, storyTransients.uri);
7597
7885
  }
7886
+ } else {
7887
+ this.variables.clearStoryTransients();
7598
7888
  }
7599
7889
  this.widgets.clearInvocations();
7600
7890
  for (const uri of this.documents.getUris()) {
@@ -8353,6 +8643,30 @@ var init_parameter_validator = __esm({
8353
8643
  return arg.type === 4 /* Variable */ ? null : new TypeError2("Argument is not a variable");
8354
8644
  }
8355
8645
  },
8646
+ {
8647
+ // The story variable a form control binds to: `$name` or `"$name"`.
8648
+ // Spindle strips the quotes and the `$` sigil from the first argument
8649
+ // and reads the rest as a dot path into the story variables.
8650
+ name: ["receiver"],
8651
+ validate(arg) {
8652
+ let name;
8653
+ if (arg.type === 0 /* String */) {
8654
+ name = arg.text.slice(1, -1);
8655
+ } else if (arg.type === 8 /* Bareword */) {
8656
+ name = arg.text;
8657
+ }
8658
+ if (name === void 0) {
8659
+ return new TypeError2("Argument is not a variable receiver");
8660
+ }
8661
+ if (/^[_@%]/.test(name)) {
8662
+ return new TypeError2(`Variable receiver must be a story variable ($name), got '${name}'`);
8663
+ }
8664
+ if (!/^\$?[A-Za-z_][\w$]*(?:\.[\w$]+)*$/.test(name)) {
8665
+ return new TypeError2(`Argument is not a valid variable receiver: '${name}'`);
8666
+ }
8667
+ return null;
8668
+ }
8669
+ },
8356
8670
  {
8357
8671
  name: ["passage"],
8358
8672
  validate(arg, _args, _index, state) {
@@ -8505,58 +8819,175 @@ var init_parameter_validator = __esm({
8505
8819
  });
8506
8820
 
8507
8821
  // 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);
8822
+ function skipSelectors(text, i) {
8823
+ while (text[i] === "." || text[i] === "#") {
8824
+ i++;
8825
+ for (; ; ) {
8826
+ if (/[a-zA-Z0-9_-]/.test(text[i] ?? "")) {
8827
+ i++;
8828
+ continue;
8829
+ }
8830
+ const interpolation = /^\{[$_@][\w.]*\}/.exec(text.slice(i));
8831
+ if (!interpolation) break;
8832
+ i += interpolation[0].length;
8513
8833
  }
8514
8834
  }
8515
- return starts;
8835
+ return i;
8516
8836
  }
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
- }
8837
+ function locateTarget(inner) {
8838
+ let start = 0;
8839
+ let end = inner.length;
8840
+ const pipeIdx = inner.indexOf("|");
8841
+ const arrowIdx = inner.indexOf("->");
8842
+ const revIdx = inner.indexOf("<-");
8843
+ if (pipeIdx !== -1) {
8844
+ start = pipeIdx + 1;
8845
+ } else if (arrowIdx !== -1) {
8846
+ start = arrowIdx + 2;
8847
+ } else if (revIdx !== -1) {
8848
+ end = revIdx;
8527
8849
  }
8528
- return { line: low, character: offset - lineStarts[low] };
8850
+ while (start < end && /\s/.test(inner[start])) start++;
8851
+ while (end > start && /\s/.test(inner[end - 1])) end--;
8852
+ return { start, end };
8529
8853
  }
8530
8854
  function parseLinks(text, lineOffset = 0) {
8531
- const lineStarts = buildLineStarts2(text);
8855
+ const lineStarts = buildLineStarts(text);
8532
8856
  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
- };
8857
+ let i = text.indexOf("[[");
8858
+ while (i !== -1) {
8859
+ const linkStart = i;
8860
+ i += 2;
8861
+ if (text[i] === "." || text[i] === "#") {
8862
+ i = skipSelectors(text, i);
8863
+ if (text[i] === " ") i++;
8864
+ }
8865
+ const innerStart = i;
8866
+ let depth = 1;
8867
+ while (i < text.length) {
8868
+ if (text.startsWith("[[", i)) {
8869
+ depth++;
8870
+ i += 2;
8871
+ } else if (text.startsWith("]]", i)) {
8872
+ if (--depth === 0) break;
8873
+ i += 2;
8874
+ } else {
8875
+ i++;
8876
+ }
8877
+ }
8878
+ if (depth !== 0) {
8879
+ i = text.indexOf("[[", linkStart + 2);
8880
+ continue;
8881
+ }
8882
+ const target = locateTarget(text.slice(innerStart, i));
8883
+ if (target.end > target.start) {
8884
+ const nameStart = innerStart + target.start;
8885
+ const nameEnd = innerStart + target.end;
8886
+ const startPos = offsetToPosition(nameStart, lineStarts);
8887
+ const endPos = offsetToPosition(nameEnd, lineStarts);
8888
+ refs.push({
8889
+ name: text.slice(nameStart, nameEnd),
8890
+ range: {
8891
+ start: { line: startPos.line + lineOffset, character: startPos.character },
8892
+ end: { line: endPos.line + lineOffset, character: endPos.character }
8893
+ },
8894
+ source: "link"
8895
+ });
8896
+ }
8897
+ i = text.indexOf("[[", i + 2);
8898
+ }
8899
+ return refs;
8900
+ }
8901
+ function findPassageRefAt(text, position) {
8902
+ const { line, character } = position;
8903
+ 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));
8904
+ }
8905
+ function resolveExpressionTarget(args2) {
8906
+ const lead = args2.length - args2.trimStart().length;
8907
+ const expr = args2.trim();
8908
+ if (expr === "") return null;
8909
+ const lexed = lexArguments(expr);
8910
+ if (lexed.length === 1 && lexed[0].start === 0 && lexed[0].end === expr.length && (lexed[0].type === 0 /* String */ || lexed[0].type === 7 /* Expression */)) {
8911
+ const raw = expr.slice(1, -1);
8912
+ if (interpolationRegex.test(raw) || raw.includes("${")) return null;
8913
+ return { name: raw.replace(/\\(.)/g, "$1"), start: lead + 1, end: lead + expr.length - 1 };
8914
+ }
8915
+ if (/[$@%"'`(]/.test(expr) || temporaryRefRegex.test(expr)) return null;
8916
+ if (EXPRESSION_BUILTINS.has(expr)) return null;
8917
+ return { name: expr, start: lead, end: lead + expr.length };
8918
+ }
8919
+ function resolveIncludeTarget(args2) {
8920
+ const inline = /\binline\b/.exec(args2);
8921
+ if (!inline) return resolveExpressionTarget(args2);
8922
+ const cut = inline.index;
8923
+ const width = inline[0].length;
8924
+ const target = resolveExpressionTarget(args2.slice(0, cut) + args2.slice(cut + width));
8925
+ if (!target || target.start < cut && cut < target.end) return null;
8926
+ return {
8927
+ name: target.name,
8928
+ start: target.start < cut ? target.start : target.start + width,
8929
+ end: target.end <= cut ? target.end : target.end + width
8930
+ };
8931
+ }
8932
+ function resolveLinkMacroTarget(args2) {
8933
+ const strings = lexArguments(args2).filter((a) => a.type === 0 /* String */);
8934
+ if (strings.length < 2) return null;
8935
+ const target = strings[1];
8936
+ const name = target.text.slice(1, -1);
8937
+ if (name.includes("\\") || interpolationRegex.test(name)) return null;
8938
+ return { name, start: target.start + 1, end: target.end - 1 };
8939
+ }
8940
+ function parseMacroPassageRefs(text, lineOffset = 0) {
8941
+ const lineStarts = buildLineStarts(text);
8942
+ const refs = [];
8943
+ for (const macro of parseMacros(text)) {
8944
+ if (!macro.open || !macro.rawArgs) continue;
8945
+ const resolve4 = macroTargetResolvers[macro.name.toLowerCase()];
8946
+ if (!resolve4) continue;
8947
+ const argsEnd = lineStarts[macro.range.end.line] + macro.range.end.character - 1;
8948
+ const argsStart = argsEnd - macro.rawArgs.length;
8949
+ const target = resolve4(text.slice(argsStart, argsEnd));
8950
+ if (!target || target.name === "") continue;
8951
+ const startPos = offsetToPosition(argsStart + target.start, lineStarts);
8952
+ const endPos = offsetToPosition(argsStart + target.end, lineStarts);
8547
8953
  refs.push({
8548
- name: passageName,
8549
- range: range2,
8550
- source: "link"
8954
+ name: target.name,
8955
+ range: {
8956
+ start: { line: startPos.line + lineOffset, character: startPos.character },
8957
+ end: { line: endPos.line + lineOffset, character: endPos.character }
8958
+ },
8959
+ source: "macro"
8551
8960
  });
8552
8961
  }
8553
8962
  return refs;
8554
8963
  }
8555
- var linkRegex;
8964
+ var EXPRESSION_BUILTINS, interpolationRegex, temporaryRefRegex, macroTargetResolvers;
8556
8965
  var init_link_parser = __esm({
8557
8966
  "src/core/parsing/link-parser.ts"() {
8558
8967
  "use strict";
8559
- linkRegex = /\[\[([^\]]*?\|)?([^\]]+?)\]\]/g;
8968
+ init_macro_parser();
8969
+ init_argument_lexer();
8970
+ EXPRESSION_BUILTINS = /* @__PURE__ */ new Set([
8971
+ "currentPassage",
8972
+ "previousPassage",
8973
+ "visited",
8974
+ "hasVisited",
8975
+ "hasVisitedAny",
8976
+ "hasVisitedAll",
8977
+ "rendered",
8978
+ "hasRendered",
8979
+ "hasRenderedAny",
8980
+ "hasRenderedAll",
8981
+ "random",
8982
+ "randomInt"
8983
+ ]);
8984
+ interpolationRegex = /\{[$_@%][A-Za-z_$]/;
8985
+ temporaryRefRegex = /(?<![.\w$@%])_[A-Za-z_$][\w$]*(?![\w$])/;
8986
+ macroTargetResolvers = {
8987
+ goto: resolveExpressionTarget,
8988
+ include: resolveIncludeTarget,
8989
+ link: resolveLinkMacroTarget
8990
+ };
8560
8991
  }
8561
8992
  });
8562
8993
 
@@ -8569,8 +9000,13 @@ function computeDiagnostics(uri, workspace, options) {
8569
9000
  const passages = workspace.passages.getPassagesInDocument(uri);
8570
9001
  if (passages.length === 0) return [];
8571
9002
  const diagnostics = [];
8572
- const macros = parseMacros(text);
8573
- pairMacros(macros, (name) => workspace.macros.isBlock(name));
9003
+ const markupText = maskScriptAndStylesheetPassages(text, passages);
9004
+ const macros = parseMacros(markupText);
9005
+ pairMacros(
9006
+ macros,
9007
+ (name) => workspace.macros.isBlock(name),
9008
+ passages.map((p) => p.range.start.line)
9009
+ );
8574
9010
  const allPassages = workspace.passages.getAllPassages();
8575
9011
  const passageNames = new Set(allPassages.map((p) => p.name));
8576
9012
  try {
@@ -8586,7 +9022,7 @@ function computeDiagnostics(uri, workspace, options) {
8586
9022
  } catch {
8587
9023
  }
8588
9024
  try {
8589
- validateLinks(text, passages, passageNames, diagnostics);
9025
+ validateLinks(markupText, passages, passageNames, diagnostics);
8590
9026
  } catch {
8591
9027
  }
8592
9028
  try {
@@ -8620,6 +9056,18 @@ function computeDiagnostics(uri, workspace, options) {
8620
9056
  return [];
8621
9057
  }
8622
9058
  }
9059
+ function maskScriptAndStylesheetPassages(text, passages) {
9060
+ const excluded = passages.filter(isScriptOrStylesheetPassage);
9061
+ if (excluded.length === 0) return text;
9062
+ const lines = text.split("\n");
9063
+ for (const passage of excluded) {
9064
+ const last = Math.min(passage.range.end.line, lines.length - 1);
9065
+ for (let i = passage.range.start.line + 1; i <= last; i++) {
9066
+ lines[i] = lines[i].replace(/[^\r]/g, " ");
9067
+ }
9068
+ }
9069
+ return lines.join("\n");
9070
+ }
8623
9071
  function validateMacros(macros, workspace, diagnostics) {
8624
9072
  for (let curIndex = 0; curIndex < macros.length; curIndex++) {
8625
9073
  const macro = macros[curIndex];
@@ -8732,8 +9180,13 @@ function validateArguments(macros, workspace, passageNames, diagnostics) {
8732
9180
  if (info.skipArgs) continue;
8733
9181
  if (!info.parameters) continue;
8734
9182
  const rawArgs = macro.rawArgs ?? "";
8735
- const args2 = lexArguments(rawArgs);
8736
- const params = new Parameters(info.parameters);
9183
+ const args2 = macro.name.toLowerCase() === "include" ? includeArguments(rawArgs) : lexArguments(rawArgs);
9184
+ let params;
9185
+ try {
9186
+ params = new Parameters(info.parameters);
9187
+ } catch {
9188
+ continue;
9189
+ }
8737
9190
  if (params.isEmpty()) {
8738
9191
  if (args2.length > 0) {
8739
9192
  diagnostics.push(makeDiag(
@@ -8773,6 +9226,16 @@ function validateArguments(macros, workspace, passageNames, diagnostics) {
8773
9226
  }
8774
9227
  }
8775
9228
  }
9229
+ function includeArguments(rawArgs) {
9230
+ const expr = includeExpression(rawArgs);
9231
+ if (expr === "") return [];
9232
+ const args2 = lexArguments(expr);
9233
+ if (args2.length === 1) return args2;
9234
+ return [{ type: 7 /* Expression */, text: expr, start: 0, end: expr.length }];
9235
+ }
9236
+ function includeExpression(rawArgs) {
9237
+ return rawArgs.replace(/\binline\b/, "").trim();
9238
+ }
8776
9239
  function validateVariables(uri, workspace, diagnostics) {
8777
9240
  if (!workspace.variables.hasStoryVariables()) {
8778
9241
  const undeclared = workspace.variables.getUndeclared(uri);
@@ -8874,7 +9337,7 @@ function validateArrayMemberAccess(uri, workspace, diagnostics) {
8874
9337
  ));
8875
9338
  }
8876
9339
  }
8877
- function temporaryRefRegex(name) {
9340
+ function temporaryRefRegex2(name) {
8878
9341
  const ident = name ? escapeRegex(name) : "[A-Za-z_$][\\w$]*";
8879
9342
  return new RegExp(`(?<![.\\w$@%])_(${ident})(?![\\w$])`, "g");
8880
9343
  }
@@ -8950,7 +9413,7 @@ function setTemporaryTargets(rawArgs) {
8950
9413
  while ((m = assignRe.exec(code)) !== null) {
8951
9414
  const name = m[1];
8952
9415
  const rhs = code.slice(m.index + m[0].length, statementEnd(code, m.index + m[0].length));
8953
- if (temporaryRefRegex(name).test(rhs)) continue;
9416
+ if (temporaryRefRegex2(name).test(rhs)) continue;
8954
9417
  targets.push({ name, offset: m.index });
8955
9418
  }
8956
9419
  return targets;
@@ -9019,7 +9482,7 @@ function validateLoopTemporaries(uri, text, macros, workspace, diagnostics) {
9019
9482
  }
9020
9483
  }
9021
9484
  function isReadInBody(body, bodyOffset, name, macroStart, macroEnd) {
9022
- const re = temporaryRefRegex(name);
9485
+ const re = temporaryRefRegex2(name);
9023
9486
  let m;
9024
9487
  while ((m = re.exec(body)) !== null) {
9025
9488
  const offset = bodyOffset + m.index;
@@ -9030,16 +9493,16 @@ function isReadInBody(body, bodyOffset, name, macroStart, macroEnd) {
9030
9493
  }
9031
9494
  return false;
9032
9495
  }
9033
- function resolveIncludeTarget(rawArgs) {
9034
- const expr = rawArgs.replace(/\binline\b/, "").trim();
9496
+ function resolveIncludeTarget2(rawArgs) {
9497
+ const expr = includeExpression(rawArgs);
9035
9498
  if (expr === "") return null;
9036
9499
  const literal2 = /^(["'`])((?:\\.|(?!\1)[^\\])*)\1$/s.exec(expr);
9037
9500
  if (literal2) {
9038
9501
  if (literal2[1] === "`" && literal2[2].includes("${")) return null;
9039
9502
  return literal2[2].replace(/\\(.)/g, "$1");
9040
9503
  }
9041
- if (/[$@%"'`(]/.test(expr) || temporaryRefRegex().test(expr)) return null;
9042
- if (EXPRESSION_BUILTINS.has(expr)) return null;
9504
+ if (/[$@%"'`(]/.test(expr) || temporaryRefRegex2().test(expr)) return null;
9505
+ if (EXPRESSION_BUILTINS2.has(expr)) return null;
9043
9506
  return expr;
9044
9507
  }
9045
9508
  function widgetDefinitionNames(content) {
@@ -9058,7 +9521,7 @@ function validateWidgetIncludes(text, macros, workspace, diagnostics) {
9058
9521
  for (const macro of macros) {
9059
9522
  if (!macro.open || !macro.rawArgs || macro.name.toLowerCase() !== "include") continue;
9060
9523
  lineStarts ??= buildLineStarts(text);
9061
- const target = resolveIncludeTarget(sourceArgs(macro, text, lineStarts).args);
9524
+ const target = resolveIncludeTarget2(sourceArgs(macro, text, lineStarts).args);
9062
9525
  if (target === null) continue;
9063
9526
  const passage = workspace.passages.getPassage(target);
9064
9527
  if (!passage?.tags?.includes("widget")) continue;
@@ -9084,10 +9547,15 @@ function validateUnusedWidgets(uri, workspace, diagnostics) {
9084
9547
  }
9085
9548
  }
9086
9549
  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));
9550
+ const excludedLines = /* @__PURE__ */ new Set();
9551
+ for (const passage of passages.filter(isScriptOrStylesheetPassage)) {
9552
+ for (let i = passage.range.start.line; i <= passage.range.end.line; i++) {
9553
+ excludedLines.add(i);
9554
+ }
9555
+ }
9089
9556
  const lines = text.split("\n");
9090
9557
  for (let i = 0; i < lines.length; i++) {
9558
+ if (excludedLines.has(i)) continue;
9091
9559
  const line = lines[i];
9092
9560
  if (/^::\s+/.test(line)) continue;
9093
9561
  if (/^\s*<[a-zA-Z]/.test(line)) continue;
@@ -9130,7 +9598,7 @@ function toLspDiagnostic(d) {
9130
9598
  message: d.message
9131
9599
  };
9132
9600
  }
9133
- var DEFERRED_CONTAINERS, EXPRESSION_BUILTINS, diagnosticsPlugin;
9601
+ var DEFERRED_CONTAINERS, EXPRESSION_BUILTINS2, diagnosticsPlugin;
9134
9602
  var init_diagnostics = __esm({
9135
9603
  "src/plugins/diagnostics.ts"() {
9136
9604
  "use strict";
@@ -9139,8 +9607,9 @@ var init_diagnostics = __esm({
9139
9607
  init_argument_lexer();
9140
9608
  init_parameter_validator();
9141
9609
  init_link_parser();
9610
+ init_passage_parser();
9142
9611
  DEFERRED_CONTAINERS = /* @__PURE__ */ new Set(["link", "button"]);
9143
- EXPRESSION_BUILTINS = /* @__PURE__ */ new Set([
9612
+ EXPRESSION_BUILTINS2 = /* @__PURE__ */ new Set([
9144
9613
  "currentPassage",
9145
9614
  "previousPassage",
9146
9615
  "visited",
@@ -9158,22 +9627,22 @@ var init_diagnostics = __esm({
9158
9627
  id: "diagnostics",
9159
9628
  capabilities: {},
9160
9629
  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) => {
9630
+ const isEnabled = (d) => ctx.config.diagnostics?.[d.code] !== false;
9631
+ const publishFor = (uri) => {
9172
9632
  const diags = computeDiagnostics(uri, ctx.workspace);
9173
9633
  ctx.connection.sendDiagnostics({
9174
9634
  uri,
9175
- diagnostics: diags.map((d) => toLspDiagnostic(d))
9635
+ diagnostics: diags.filter(isEnabled).map((d) => toLspDiagnostic(d))
9176
9636
  });
9637
+ };
9638
+ ctx.workspace.on("modelReady", () => {
9639
+ for (const uri of ctx.workspace.documents.getUris()) {
9640
+ publishFor(uri);
9641
+ }
9642
+ });
9643
+ ctx.workspace.on("documentChanged", publishFor);
9644
+ ctx.workspace.on("documentClosed", (uri) => {
9645
+ ctx.connection.sendDiagnostics({ uri, diagnostics: [] });
9177
9646
  });
9178
9647
  }
9179
9648
  };
@@ -9193,9 +9662,9 @@ var require_identity = __commonJS({
9193
9662
  var NODE_TYPE = Symbol.for("yaml.node.type");
9194
9663
  var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
9195
9664
  var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
9196
- var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
9665
+ var isMap2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
9197
9666
  var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
9198
- var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
9667
+ var isScalar2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
9199
9668
  var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
9200
9669
  function isCollection(node) {
9201
9670
  if (node && typeof node === "object")
@@ -9217,7 +9686,7 @@ var require_identity = __commonJS({
9217
9686
  }
9218
9687
  return false;
9219
9688
  }
9220
- var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
9689
+ var hasAnchor = (node) => (isScalar2(node) || isCollection(node)) && !!node.anchor;
9221
9690
  exports.ALIAS = ALIAS;
9222
9691
  exports.DOC = DOC;
9223
9692
  exports.MAP = MAP;
@@ -9229,10 +9698,10 @@ var require_identity = __commonJS({
9229
9698
  exports.isAlias = isAlias;
9230
9699
  exports.isCollection = isCollection;
9231
9700
  exports.isDocument = isDocument;
9232
- exports.isMap = isMap;
9701
+ exports.isMap = isMap2;
9233
9702
  exports.isNode = isNode;
9234
9703
  exports.isPair = isPair;
9235
- exports.isScalar = isScalar;
9704
+ exports.isScalar = isScalar2;
9236
9705
  exports.isSeq = isSeq;
9237
9706
  }
9238
9707
  });
@@ -13334,9 +13803,9 @@ var require_resolve_flow_collection = __commonJS({
13334
13803
  var blockMsg = "Block collections are not allowed within flow collections";
13335
13804
  var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq");
13336
13805
  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);
13806
+ const isMap2 = fc.start.source === "{";
13807
+ const fcName = isMap2 ? "flow map" : "flow sequence";
13808
+ const NodeClass = tag?.nodeClass ?? (isMap2 ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
13340
13809
  const coll = new NodeClass(ctx.schema);
13341
13810
  coll.flow = true;
13342
13811
  const atRoot = ctx.atRoot;
@@ -13372,7 +13841,7 @@ var require_resolve_flow_collection = __commonJS({
13372
13841
  offset = props.end;
13373
13842
  continue;
13374
13843
  }
13375
- if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key))
13844
+ if (!isMap2 && ctx.options.strict && utilContainsNewline.containsNewline(key))
13376
13845
  onError(
13377
13846
  key,
13378
13847
  // checked by containsNewline()
@@ -13412,7 +13881,7 @@ var require_resolve_flow_collection = __commonJS({
13412
13881
  }
13413
13882
  }
13414
13883
  }
13415
- if (!isMap && !sep2 && !props.found) {
13884
+ if (!isMap2 && !sep2 && !props.found) {
13416
13885
  const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
13417
13886
  coll.items.push(valueNode);
13418
13887
  offset = valueNode.range[2];
@@ -13435,7 +13904,7 @@ var require_resolve_flow_collection = __commonJS({
13435
13904
  startOnNewline: false
13436
13905
  });
13437
13906
  if (valueProps.found) {
13438
- if (!isMap && !props.found && ctx.options.strict) {
13907
+ if (!isMap2 && !props.found && ctx.options.strict) {
13439
13908
  if (sep2)
13440
13909
  for (const st of sep2) {
13441
13910
  if (st === valueProps.found)
@@ -13467,7 +13936,7 @@ var require_resolve_flow_collection = __commonJS({
13467
13936
  const pair = new Pair.Pair(keyNode, valueNode);
13468
13937
  if (ctx.options.keepSourceTokens)
13469
13938
  pair.srcToken = collItem;
13470
- if (isMap) {
13939
+ if (isMap2) {
13471
13940
  const map2 = coll;
13472
13941
  if (utilMapIncludes.mapIncludes(ctx, map2.items, keyNode))
13473
13942
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
@@ -13483,7 +13952,7 @@ var require_resolve_flow_collection = __commonJS({
13483
13952
  offset = valueNode ? valueNode.range[2] : valueProps.end;
13484
13953
  }
13485
13954
  }
13486
- const expectedEnd = isMap ? "}" : "]";
13955
+ const expectedEnd = isMap2 ? "}" : "]";
13487
13956
  const [ce, ...ee] = fc.end;
13488
13957
  let cePos = offset;
13489
13958
  if (ce?.source === expectedEnd)
@@ -14766,7 +15235,7 @@ var require_cst = __commonJS({
14766
15235
  var FLOW_END = "";
14767
15236
  var SCALAR = "";
14768
15237
  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");
15238
+ var isScalar2 = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar");
14770
15239
  function prettyToken(token) {
14771
15240
  switch (token) {
14772
15241
  case BOM:
@@ -14850,7 +15319,7 @@ var require_cst = __commonJS({
14850
15319
  exports.FLOW_END = FLOW_END;
14851
15320
  exports.SCALAR = SCALAR;
14852
15321
  exports.isCollection = isCollection;
14853
- exports.isScalar = isScalar;
15322
+ exports.isScalar = isScalar2;
14854
15323
  exports.prettyToken = prettyToken;
14855
15324
  exports.tokenType = tokenType;
14856
15325
  }
@@ -16363,7 +16832,7 @@ var require_public_api = __commonJS({
16363
16832
  return docs;
16364
16833
  return Object.assign([], { empty: true }, composer$1.streamInfo());
16365
16834
  }
16366
- function parseDocument(source, options = {}) {
16835
+ function parseDocument2(source, options = {}) {
16367
16836
  const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
16368
16837
  const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
16369
16838
  const composer$1 = new composer.Composer(options);
@@ -16389,7 +16858,7 @@ var require_public_api = __commonJS({
16389
16858
  } else if (options === void 0 && reviver && typeof reviver === "object") {
16390
16859
  options = reviver;
16391
16860
  }
16392
- const doc = parseDocument(src, options);
16861
+ const doc = parseDocument2(src, options);
16393
16862
  if (!doc)
16394
16863
  return null;
16395
16864
  doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
@@ -16425,7 +16894,7 @@ var require_public_api = __commonJS({
16425
16894
  }
16426
16895
  exports.parse = parse3;
16427
16896
  exports.parseAllDocuments = parseAllDocuments;
16428
- exports.parseDocument = parseDocument;
16897
+ exports.parseDocument = parseDocument2;
16429
16898
  exports.stringify = stringify;
16430
16899
  }
16431
16900
  });
@@ -16515,6 +16984,22 @@ function findConfigFile(workspaceRoot) {
16515
16984
  }
16516
16985
  return null;
16517
16986
  }
16987
+ function loadConfigFile(configPath) {
16988
+ let content;
16989
+ try {
16990
+ content = readFileSync3(configPath, "utf-8");
16991
+ } catch (err) {
16992
+ const reason = err?.code === "ENOENT" ? "file not found" : err instanceof Error ? err.message : String(err);
16993
+ throw new Error(`Cannot read config file '${configPath}': ${reason}`);
16994
+ }
16995
+ const format = configPath.toLowerCase().endsWith(".json") ? "json" : "yaml";
16996
+ try {
16997
+ return parseConfig(content, format);
16998
+ } catch (err) {
16999
+ const reason = err instanceof Error ? err.message : String(err);
17000
+ throw new Error(`Invalid ${format.toUpperCase()} in config file '${configPath}': ${reason}`);
17001
+ }
17002
+ }
16518
17003
  function loadConfigFromDisk(workspaceRoot) {
16519
17004
  const configPath = findConfigFile(workspaceRoot);
16520
17005
  if (!configPath) {
@@ -16709,7 +17194,7 @@ __export(check_exports, {
16709
17194
  runCheck: () => runCheck
16710
17195
  });
16711
17196
  import { readFileSync as readFileSync4 } from "node:fs";
16712
- import { resolve } from "node:path";
17197
+ import { resolve as resolve2 } from "node:path";
16713
17198
  import { pathToFileURL as pathToFileURL2 } from "node:url";
16714
17199
  function parseArgs(args2) {
16715
17200
  const options = {
@@ -16782,6 +17267,15 @@ function filterBySeverity(diagnostics, minSeverity) {
16782
17267
  async function runCheck(args2) {
16783
17268
  const options = parseArgs(args2);
16784
17269
  const cwd = process.cwd();
17270
+ let explicitConfig = null;
17271
+ if (options.configPath) {
17272
+ try {
17273
+ explicitConfig = loadConfigFile(resolve2(cwd, options.configPath));
17274
+ } catch (err) {
17275
+ console.error(`spindle-lsp check: ${err instanceof Error ? err.message : String(err)}`);
17276
+ return 2;
17277
+ }
17278
+ }
16785
17279
  const files = [];
16786
17280
  for (const pattern of options.patterns) {
16787
17281
  const matches = await glob(pattern, {
@@ -16802,28 +17296,28 @@ async function runCheck(args2) {
16802
17296
  }
16803
17297
  return 0;
16804
17298
  }
16805
- let configRoot;
16806
- if (options.configPath) {
16807
- configRoot = resolve(cwd, options.configPath, "..");
17299
+ const commonDir = uniqueFiles.map((f) => resolve2(f, "..")).reduce((a, b) => {
17300
+ while (!b.startsWith(a)) a = resolve2(a, "..");
17301
+ return a;
17302
+ });
17303
+ let projectConfig;
17304
+ if (explicitConfig) {
17305
+ projectConfig = explicitConfig;
16808
17306
  } else {
16809
- const dirs = uniqueFiles.map((f) => resolve(f, ".."));
16810
- configRoot = dirs.reduce((a, b) => {
16811
- while (!b.startsWith(a)) a = resolve(a, "..");
16812
- return a;
16813
- });
17307
+ let configRoot = commonDir;
16814
17308
  let search = configRoot;
16815
17309
  for (let i = 0; i < 10; i++) {
16816
17310
  if (findConfigFile(search)) {
16817
17311
  configRoot = search;
16818
17312
  break;
16819
17313
  }
16820
- const parent = resolve(search, "..");
17314
+ const parent = resolve2(search, "..");
16821
17315
  if (parent === search) break;
16822
17316
  search = parent;
16823
17317
  }
17318
+ projectConfig = loadConfigFromDisk(configRoot);
16824
17319
  }
16825
- const projectConfig = loadConfigFromDisk(configRoot);
16826
- const workspace = new WorkspaceModel();
17320
+ const workspace = new WorkspaceModel({ workspaceRoot: commonDir });
16827
17321
  const fileContents = /* @__PURE__ */ new Map();
16828
17322
  for (const filePath of uniqueFiles) {
16829
17323
  try {
@@ -16835,7 +17329,7 @@ async function runCheck(args2) {
16835
17329
  }
16836
17330
  workspace.initialize(fileContents);
16837
17331
  if (Object.keys(projectConfig.macros).length > 0) {
16838
- workspace.macros.loadSupplements(projectConfig.macros);
17332
+ workspace.macros.loadConfig(projectConfig.macros);
16839
17333
  }
16840
17334
  const diagOptions = {};
16841
17335
  if (options.maxLineLength !== null) {
@@ -16922,7 +17416,7 @@ function splitPassages(text) {
16922
17416
  function classifyPassage(header) {
16923
17417
  const tagMatch = header.match(/\[([^\]]*)\]/g);
16924
17418
  if (!tagMatch) return "normal";
16925
- const tags = tagMatch.map((t) => t.slice(1, -1).trim().toLowerCase());
17419
+ const tags = tagMatch.flatMap((t) => t.slice(1, -1).toLowerCase().split(/\s+/));
16926
17420
  if (tags.includes("script")) return "script";
16927
17421
  if (tags.includes("stylesheet")) return "stylesheet";
16928
17422
  return "normal";
@@ -16941,6 +17435,10 @@ function segmentRegions(body) {
16941
17435
  if (/^<script(\s|>)/i.test(trimmed)) {
16942
17436
  const scriptLines = [line];
16943
17437
  i++;
17438
+ if (/<\/script\s*>/i.test(line)) {
17439
+ regions.push({ type: "script", lines: scriptLines });
17440
+ continue;
17441
+ }
16944
17442
  while (i < lines.length && !/<\/script>/i.test(lines[i])) {
16945
17443
  scriptLines.push(lines[i]);
16946
17444
  i++;
@@ -17165,7 +17663,7 @@ function replaceSvgBlocks(html) {
17165
17663
  function restoreSvgBlocks(text, tokens) {
17166
17664
  let result = text;
17167
17665
  for (let i = 0; i < tokens.length; i++) {
17168
- result = result.replace(`<!--SVG:${i}-->`, tokens[i]);
17666
+ result = result.replace(`<!--SVG:${i}-->`, () => tokens[i]);
17169
17667
  }
17170
17668
  return result;
17171
17669
  }
@@ -17213,8 +17711,8 @@ function replaceSpindleTokens(html) {
17213
17711
  function restoreSpindleTokens(text, tokens) {
17214
17712
  let result = text;
17215
17713
  for (let i = 0; i < tokens.length; i++) {
17216
- result = result.replace(`<!--SP:${i}-->`, tokens[i]);
17217
- result = result.replace(`__SP${i}__`, tokens[i]);
17714
+ result = result.replace(`<!--SP:${i}-->`, () => tokens[i]);
17715
+ result = result.replace(`__SP${i}__`, () => tokens[i]);
17218
17716
  }
17219
17717
  return result;
17220
17718
  }
@@ -17257,6 +17755,7 @@ async function formatDocument(text, options) {
17257
17755
  const isDedenting = options?.isDedentingSubMacro ?? ((name) => DEFAULT_DEDENTING.has(name.toLowerCase()));
17258
17756
  const passages = splitPassages(text);
17259
17757
  const resultLines = [];
17758
+ const hardBreakLines = /* @__PURE__ */ new Set();
17260
17759
  for (let pi = 0; pi < passages.length; pi++) {
17261
17760
  const passage = passages[pi];
17262
17761
  if (pi > 0 && passage.header) {
@@ -17285,6 +17784,10 @@ async function formatDocument(text, options) {
17285
17784
  const regions = segmentRegions(passage.body);
17286
17785
  for (const region of regions) {
17287
17786
  if (region.type === "script") {
17787
+ if (region.lines.length === 1) {
17788
+ resultLines.push(region.lines[0]);
17789
+ continue;
17790
+ }
17288
17791
  const firstLine = region.lines[0];
17289
17792
  const lastLine = region.lines[region.lines.length - 1];
17290
17793
  const innerLines = region.lines.slice(1, region.lines.length - 1);
@@ -17319,49 +17822,92 @@ async function formatDocument(text, options) {
17319
17822
  if (options?.maxLineLength) {
17320
17823
  indented = wrapLines(indented, options.maxLineLength);
17321
17824
  }
17322
- resultLines.push(...indented);
17825
+ for (const line of indented) {
17826
+ if (line.endsWith(" ")) hardBreakLines.add(resultLines.length);
17827
+ resultLines.push(line);
17828
+ }
17323
17829
  }
17324
17830
  }
17325
- let output = resultLines.map((l) => l.replace(/\s+$/, "")).join("\n");
17831
+ let output = resultLines.map((l, i) => hardBreakLines.has(i) ? l : l.replace(/\s+$/, "")).join("\n");
17326
17832
  output = output.replace(/\n*$/, "\n");
17327
17833
  return output;
17328
17834
  }
17329
17835
  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();
17836
+ const bodies = [{ openLine: -1, minCol: Infinity, keepRelative: false }];
17837
+ const stack = [0];
17838
+ const info = [];
17839
+ const openBody = (line) => {
17840
+ bodies.push({ openLine: line, minCol: Infinity, keepRelative: false });
17841
+ stack.push(bodies.length - 1);
17842
+ };
17843
+ const closeBody = () => stack.length > 1 ? stack.pop() : void 0;
17844
+ const addLine = (col, text) => {
17845
+ const body = bodies[stack[stack.length - 1]];
17846
+ body.minCol = Math.min(body.minCol, col);
17847
+ if (LIST_ITEM_REGEX.test(text) || CODE_FENCE_REGEX.test(text)) body.keepRelative = true;
17848
+ info.push({ body: stack[stack.length - 1], col, text });
17849
+ };
17850
+ for (let i = 0; i < lines.length; i++) {
17851
+ const trimmed = lines[i].trim();
17335
17852
  if (trimmed === "") {
17336
- result.push("");
17853
+ info.push(null);
17337
17854
  continue;
17338
17855
  }
17856
+ const col = indentColumn(lines[i]);
17339
17857
  const dedentMatch = trimmed.match(MACRO_OPEN_REGEX);
17340
17858
  if (dedentMatch && isDedenting(dedentMatch[1])) {
17341
- indentLevel = Math.max(0, indentLevel - 1);
17342
- result.push(indentLevel > 0 ? " ".repeat(indentLevel) + trimmed : trimmed);
17343
- indentLevel++;
17859
+ const closed2 = closeBody();
17860
+ if (closed2 === void 0) addLine(col, trimmed);
17861
+ else info.push({ body: stack[stack.length - 1], closes: closed2, col, text: trimmed });
17862
+ openBody(i);
17344
17863
  continue;
17345
17864
  }
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);
17865
+ const closed = MACRO_CLOSE_REGEX.test(trimmed) ? closeBody() : void 0;
17866
+ if (closed === void 0) addLine(col, trimmed);
17867
+ else info.push({ body: stack[stack.length - 1], closes: closed, col, text: trimmed });
17351
17868
  const openMatch = trimmed.match(MACRO_OPEN_REGEX);
17352
17869
  if (openMatch && isBlock(openMatch[1])) {
17353
- indentLevel++;
17870
+ openBody(i);
17354
17871
  }
17355
17872
  const closeGlobal = /\{\/[A-Za-z][\w-]*\s*\}/g;
17356
17873
  let cm;
17357
17874
  while ((cm = closeGlobal.exec(trimmed)) !== null) {
17358
- if (cm.index > 0) {
17359
- indentLevel = Math.max(0, indentLevel - 1);
17360
- }
17875
+ if (cm.index > 0) closeBody();
17361
17876
  }
17362
17877
  }
17878
+ const outCol = [];
17879
+ const result = [];
17880
+ for (let i = 0; i < info.length; i++) {
17881
+ const line = info[i];
17882
+ if (!line) {
17883
+ outCol.push(0);
17884
+ result.push("");
17885
+ continue;
17886
+ }
17887
+ let col;
17888
+ if (line.closes !== void 0) {
17889
+ col = outCol[bodies[line.closes].openLine];
17890
+ } else {
17891
+ const body = bodies[line.body];
17892
+ const isRoot = body.openLine === -1;
17893
+ const base = isRoot ? 0 : outCol[body.openLine] + 2;
17894
+ col = base + (body.keepRelative ? line.col - (isRoot ? 0 : body.minCol) : 0);
17895
+ }
17896
+ outCol.push(col);
17897
+ const hardBreak = / {2,}$/.test(lines[i]) && info[i + 1] ? " " : "";
17898
+ result.push(" ".repeat(col) + line.text + hardBreak);
17899
+ }
17363
17900
  return result;
17364
17901
  }
17902
+ function indentColumn(line) {
17903
+ let col = 0;
17904
+ for (const ch of line) {
17905
+ if (ch === " ") col++;
17906
+ else if (ch === " ") col += 4 - col % 4;
17907
+ else break;
17908
+ }
17909
+ return col;
17910
+ }
17365
17911
  async function formatRange(text, _range, options) {
17366
17912
  return formatDocument(text, options);
17367
17913
  }
@@ -17383,6 +17929,7 @@ function wrapLines(lines, maxLen) {
17383
17929
  const indentMatch = line.match(/^(\s*)/);
17384
17930
  const indent = indentMatch ? indentMatch[1] : "";
17385
17931
  const wrapped = wordWrap(trimmed, maxLen, indent);
17932
+ if (line.endsWith(" ")) wrapped[wrapped.length - 1] += " ";
17386
17933
  result.push(...wrapped);
17387
17934
  }
17388
17935
  return result;
@@ -17443,7 +17990,7 @@ function normalizePassageHeader(line) {
17443
17990
  }
17444
17991
  return `:: ${name}${suffix}`;
17445
17992
  }
17446
- var PASSAGE_HEADER_REGEX2, MACRO_OPEN_REGEX, MACRO_CLOSE_REGEX, DEFAULT_DEDENTING, formatPlugin;
17993
+ var PASSAGE_HEADER_REGEX2, MACRO_OPEN_REGEX, MACRO_CLOSE_REGEX, DEFAULT_DEDENTING, LIST_ITEM_REGEX, CODE_FENCE_REGEX, formatPlugin;
17447
17994
  var init_format = __esm({
17448
17995
  "src/plugins/format.ts"() {
17449
17996
  "use strict";
@@ -17455,6 +18002,8 @@ var init_format = __esm({
17455
18002
  MACRO_OPEN_REGEX = /^\{(?:[#.][a-zA-Z][\w-]*\s*)*([A-Za-z][\w-]*)\b/;
17456
18003
  MACRO_CLOSE_REGEX = /^\{\/([A-Za-z][\w-]*)\b/;
17457
18004
  DEFAULT_DEDENTING = /* @__PURE__ */ new Set(["else", "elseif", "next", "case", "default"]);
18005
+ LIST_ITEM_REGEX = /^(?:[-+*]|\d{1,9}[.)])(?:[ \t]|$)/;
18006
+ CODE_FENCE_REGEX = /^(?:`{3,}|~{3,})/;
17458
18007
  formatPlugin = {
17459
18008
  id: "format",
17460
18009
  capabilities: {
@@ -39794,7 +40343,7 @@ var init_protocol = __esm({
39794
40343
  return;
39795
40344
  }
39796
40345
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
39797
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
40346
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
39798
40347
  options?.signal?.throwIfAborted();
39799
40348
  }
39800
40349
  } catch (error48) {
@@ -39811,7 +40360,7 @@ var init_protocol = __esm({
39811
40360
  */
39812
40361
  request(request, resultSchema, options) {
39813
40362
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
39814
- return new Promise((resolve3, reject) => {
40363
+ return new Promise((resolve4, reject) => {
39815
40364
  const earlyReject = (error48) => {
39816
40365
  reject(error48);
39817
40366
  };
@@ -39889,7 +40438,7 @@ var init_protocol = __esm({
39889
40438
  if (!parseResult.success) {
39890
40439
  reject(parseResult.error);
39891
40440
  } else {
39892
- resolve3(parseResult.data);
40441
+ resolve4(parseResult.data);
39893
40442
  }
39894
40443
  } catch (error48) {
39895
40444
  reject(error48);
@@ -40150,12 +40699,12 @@ var init_protocol = __esm({
40150
40699
  }
40151
40700
  } catch {
40152
40701
  }
40153
- return new Promise((resolve3, reject) => {
40702
+ return new Promise((resolve4, reject) => {
40154
40703
  if (signal.aborted) {
40155
40704
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
40156
40705
  return;
40157
40706
  }
40158
- const timeoutId = setTimeout(resolve3, interval);
40707
+ const timeoutId = setTimeout(resolve4, interval);
40159
40708
  signal.addEventListener("abort", () => {
40160
40709
  clearTimeout(timeoutId);
40161
40710
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -43182,7 +43731,7 @@ var require_compile = __commonJS({
43182
43731
  const schOrFunc = root.refs[ref];
43183
43732
  if (schOrFunc)
43184
43733
  return schOrFunc;
43185
- let _sch = resolve3.call(this, root, ref);
43734
+ let _sch = resolve4.call(this, root, ref);
43186
43735
  if (_sch === void 0) {
43187
43736
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
43188
43737
  const { schemaId } = this.opts;
@@ -43209,7 +43758,7 @@ var require_compile = __commonJS({
43209
43758
  function sameSchemaEnv(s1, s2) {
43210
43759
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
43211
43760
  }
43212
- function resolve3(root, ref) {
43761
+ function resolve4(root, ref) {
43213
43762
  let sch;
43214
43763
  while (typeof (sch = this.refs[ref]) == "string")
43215
43764
  ref = sch;
@@ -43784,7 +44333,7 @@ var require_fast_uri = __commonJS({
43784
44333
  }
43785
44334
  return uri;
43786
44335
  }
43787
- function resolve3(baseURI, relativeURI, options) {
44336
+ function resolve4(baseURI, relativeURI, options) {
43788
44337
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
43789
44338
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
43790
44339
  schemelessOptions.skipEscape = true;
@@ -44011,7 +44560,7 @@ var require_fast_uri = __commonJS({
44011
44560
  var fastUri = {
44012
44561
  SCHEMES,
44013
44562
  normalize: normalize2,
44014
- resolve: resolve3,
44563
+ resolve: resolve4,
44015
44564
  resolveComponent,
44016
44565
  equal,
44017
44566
  serialize,
@@ -48135,7 +48684,7 @@ var init_mcp = __esm({
48135
48684
  let task = createTaskResult.task;
48136
48685
  const pollInterval = task.pollInterval ?? 5e3;
48137
48686
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
48138
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
48687
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
48139
48688
  const updatedTask = await extra.taskStore.getTask(taskId);
48140
48689
  if (!updatedTask) {
48141
48690
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -48726,12 +49275,12 @@ var init_stdio2 = __esm({
48726
49275
  this.onclose?.();
48727
49276
  }
48728
49277
  send(message) {
48729
- return new Promise((resolve3) => {
49278
+ return new Promise((resolve4) => {
48730
49279
  const json2 = serializeMessage(message);
48731
49280
  if (this._stdout.write(json2)) {
48732
- resolve3();
49281
+ resolve4();
48733
49282
  } else {
48734
- this._stdout.once("drain", resolve3);
49283
+ this._stdout.once("drain", resolve4);
48735
49284
  }
48736
49285
  });
48737
49286
  }
@@ -48745,7 +49294,7 @@ __export(server_exports, {
48745
49294
  startMcpServer: () => startMcpServer
48746
49295
  });
48747
49296
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
48748
- import { resolve as resolve2 } from "node:path";
49297
+ import { resolve as resolve3 } from "node:path";
48749
49298
  import { pathToFileURL as pathToFileURL3, fileURLToPath as fileURLToPath5 } from "node:url";
48750
49299
  function filterBySeverity2(diagnostics, minSeverity) {
48751
49300
  const minIdx = SEVERITY_ORDER2.indexOf(minSeverity);
@@ -48763,9 +49312,9 @@ async function resolveFiles(pattern, cwd) {
48763
49312
  return [...new Set(matches)];
48764
49313
  }
48765
49314
  function findConfigRoot(files) {
48766
- const dirs = files.map((f) => resolve2(f, ".."));
49315
+ const dirs = files.map((f) => resolve3(f, ".."));
48767
49316
  let configRoot = dirs.reduce((a, b) => {
48768
- while (!b.startsWith(a)) a = resolve2(a, "..");
49317
+ while (!b.startsWith(a)) a = resolve3(a, "..");
48769
49318
  return a;
48770
49319
  });
48771
49320
  let search = configRoot;
@@ -48774,7 +49323,7 @@ function findConfigRoot(files) {
48774
49323
  configRoot = search;
48775
49324
  break;
48776
49325
  }
48777
- const parent = resolve2(search, "..");
49326
+ const parent = resolve3(search, "..");
48778
49327
  if (parent === search) break;
48779
49328
  search = parent;
48780
49329
  }
@@ -48783,7 +49332,7 @@ function findConfigRoot(files) {
48783
49332
  function createWorkspace(files) {
48784
49333
  const configRoot = findConfigRoot(files);
48785
49334
  const projectConfig = loadConfigFromDisk(configRoot);
48786
- const workspace = new WorkspaceModel();
49335
+ const workspace = new WorkspaceModel({ workspaceRoot: configRoot });
48787
49336
  const fileContents = /* @__PURE__ */ new Map();
48788
49337
  for (const filePath of files) {
48789
49338
  try {
@@ -48795,7 +49344,7 @@ function createWorkspace(files) {
48795
49344
  }
48796
49345
  workspace.initialize(fileContents);
48797
49346
  if (Object.keys(projectConfig.macros).length > 0) {
48798
- workspace.macros.loadSupplements(projectConfig.macros);
49347
+ workspace.macros.loadConfig(projectConfig.macros);
48799
49348
  }
48800
49349
  return workspace;
48801
49350
  }
@@ -49976,8 +50525,8 @@ var require_semaphore = __commonJS({
49976
50525
  this._waiting = [];
49977
50526
  }
49978
50527
  lock(thunk) {
49979
- return new Promise((resolve3, reject) => {
49980
- this._waiting.push({ thunk, resolve: resolve3, reject });
50528
+ return new Promise((resolve4, reject) => {
50529
+ this._waiting.push({ thunk, resolve: resolve4, reject });
49981
50530
  this.runNext();
49982
50531
  });
49983
50532
  }
@@ -51467,9 +52016,9 @@ ${JSON.stringify(message, null, 4)}`);
51467
52016
  if (typeof cancellationStrategy.sender.enableCancellation === "function") {
51468
52017
  cancellationStrategy.sender.enableCancellation(requestMessage);
51469
52018
  }
51470
- return new Promise(async (resolve3, reject) => {
52019
+ return new Promise(async (resolve4, reject) => {
51471
52020
  const resolveWithCleanup = (r) => {
51472
- resolve3(r);
52021
+ resolve4(r);
51473
52022
  cancellationStrategy.sender.cleanup(id);
51474
52023
  disposable?.dispose();
51475
52024
  };
@@ -51880,10 +52429,10 @@ var require_ril = __commonJS({
51880
52429
  return api_1.Disposable.create(() => this.stream.off("end", listener));
51881
52430
  }
51882
52431
  write(data, encoding) {
51883
- return new Promise((resolve3, reject) => {
52432
+ return new Promise((resolve4, reject) => {
51884
52433
  const callback = (error48) => {
51885
52434
  if (error48 === void 0 || error48 === null) {
51886
- resolve3();
52435
+ resolve4();
51887
52436
  } else {
51888
52437
  reject(error48);
51889
52438
  }
@@ -52135,10 +52684,10 @@ var require_main = __commonJS({
52135
52684
  exports.generateRandomPipeName = generateRandomPipeName;
52136
52685
  function createClientPipeTransport(pipeName, encoding = "utf-8") {
52137
52686
  let connectResolve;
52138
- const connected = new Promise((resolve3, _reject) => {
52139
- connectResolve = resolve3;
52687
+ const connected = new Promise((resolve4, _reject) => {
52688
+ connectResolve = resolve4;
52140
52689
  });
52141
- return new Promise((resolve3, reject) => {
52690
+ return new Promise((resolve4, reject) => {
52142
52691
  let server = (0, net_1.createServer)((socket) => {
52143
52692
  server.close();
52144
52693
  connectResolve([
@@ -52149,7 +52698,7 @@ var require_main = __commonJS({
52149
52698
  server.on("error", reject);
52150
52699
  server.listen(pipeName, () => {
52151
52700
  server.removeListener("error", reject);
52152
- resolve3({
52701
+ resolve4({
52153
52702
  onConnected: () => {
52154
52703
  return connected;
52155
52704
  }
@@ -52168,10 +52717,10 @@ var require_main = __commonJS({
52168
52717
  exports.createServerPipeTransport = createServerPipeTransport;
52169
52718
  function createClientSocketTransport(port, encoding = "utf-8") {
52170
52719
  let connectResolve;
52171
- const connected = new Promise((resolve3, _reject) => {
52172
- connectResolve = resolve3;
52720
+ const connected = new Promise((resolve4, _reject) => {
52721
+ connectResolve = resolve4;
52173
52722
  });
52174
- return new Promise((resolve3, reject) => {
52723
+ return new Promise((resolve4, reject) => {
52175
52724
  const server = (0, net_1.createServer)((socket) => {
52176
52725
  server.close();
52177
52726
  connectResolve([
@@ -52182,7 +52731,7 @@ var require_main = __commonJS({
52182
52731
  server.on("error", reject);
52183
52732
  server.listen(port, "127.0.0.1", () => {
52184
52733
  server.removeListener("error", reject);
52185
- resolve3({
52734
+ resolve4({
52186
52735
  onConnected: () => {
52187
52736
  return connected;
52188
52737
  }
@@ -56968,8 +57517,8 @@ var require_server = __commonJS({
56968
57517
  if (value instanceof Promise) {
56969
57518
  return value;
56970
57519
  } else if (Is.thenable(value)) {
56971
- return new Promise((resolve3, reject) => {
56972
- value.then((resolved) => resolve3(resolved), (error48) => reject(error48));
57520
+ return new Promise((resolve4, reject) => {
57521
+ value.then((resolved) => resolve4(resolved), (error48) => reject(error48));
56973
57522
  });
56974
57523
  } else {
56975
57524
  return Promise.resolve(value);
@@ -57239,7 +57788,7 @@ var require_files = __commonJS({
57239
57788
  function isWindows() {
57240
57789
  return process.platform === "win32";
57241
57790
  }
57242
- function resolve3(moduleName, nodePath, cwd, tracer) {
57791
+ function resolve4(moduleName, nodePath, cwd, tracer) {
57243
57792
  const nodePathKey = "NODE_PATH";
57244
57793
  const app = [
57245
57794
  "var p = process;",
@@ -57258,7 +57807,7 @@ var require_files = __commonJS({
57258
57807
  "}",
57259
57808
  "});"
57260
57809
  ].join("");
57261
- return new Promise((resolve4, reject) => {
57810
+ return new Promise((resolve5, reject) => {
57262
57811
  let env = process.env;
57263
57812
  let newEnv = /* @__PURE__ */ Object.create(null);
57264
57813
  Object.keys(env).forEach((key) => newEnv[key] = env[key]);
@@ -57290,7 +57839,7 @@ var require_files = __commonJS({
57290
57839
  if (message2.c === "r") {
57291
57840
  cp.send({ c: "e" });
57292
57841
  if (message2.s) {
57293
- resolve4(message2.r);
57842
+ resolve5(message2.r);
57294
57843
  } else {
57295
57844
  reject(new Error(`Failed to resolve module: ${moduleName}`));
57296
57845
  }
@@ -57306,7 +57855,7 @@ var require_files = __commonJS({
57306
57855
  }
57307
57856
  });
57308
57857
  }
57309
- exports.resolve = resolve3;
57858
+ exports.resolve = resolve4;
57310
57859
  function resolveGlobalNodePath(tracer) {
57311
57860
  let npmCommand = "npm";
57312
57861
  const env = /* @__PURE__ */ Object.create(null);
@@ -57421,17 +57970,17 @@ var require_files = __commonJS({
57421
57970
  if (!path2.isAbsolute(nodePath)) {
57422
57971
  nodePath = path2.join(workspaceRoot, nodePath);
57423
57972
  }
57424
- return resolve3(moduleName, nodePath, nodePath, tracer).then((value) => {
57973
+ return resolve4(moduleName, nodePath, nodePath, tracer).then((value) => {
57425
57974
  if (FileSystem.isParent(nodePath, value)) {
57426
57975
  return value;
57427
57976
  } else {
57428
57977
  return Promise.reject(new Error(`Failed to load ${moduleName} from node path location.`));
57429
57978
  }
57430
57979
  }).then(void 0, (_error) => {
57431
- return resolve3(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
57980
+ return resolve4(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
57432
57981
  });
57433
57982
  } else {
57434
- return resolve3(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
57983
+ return resolve4(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
57435
57984
  }
57436
57985
  }
57437
57986
  exports.resolveModulePath = resolveModulePath;
@@ -58061,7 +58610,7 @@ function getCompletions(uri, position, triggerChar, workspace) {
58061
58610
  if (position.line >= lines.length) return [];
58062
58611
  const lineText = lines[position.line].substring(0, position.character);
58063
58612
  if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
58064
- return getClosingMacroCompletions(text, position, workspace);
58613
+ return getClosingMacroCompletions(uri, text, position, workspace);
58065
58614
  }
58066
58615
  const transientDotPathMatch = /%([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
58067
58616
  if (transientDotPathMatch) {
@@ -58091,11 +58640,18 @@ function getCompletions(uri, position, triggerChar, workspace) {
58091
58640
  }
58092
58641
  return [];
58093
58642
  }
58094
- function getClosingMacroCompletions(text, position, workspace) {
58643
+ function getClosingMacroCompletions(uri, text, position, workspace) {
58644
+ const passages = workspace.passages.getPassagesInDocument(uri);
58095
58645
  const macros = parseMacros(text);
58096
- pairMacros(macros, (name) => workspace.macros.isBlock(name));
58646
+ pairMacros(
58647
+ macros,
58648
+ (name) => workspace.macros.isBlock(name),
58649
+ passages.map((p) => p.range.start.line)
58650
+ );
58651
+ const passageStart = workspace.passages.getPassageAt(uri, position.line)?.range.start.line ?? 0;
58097
58652
  const openStack = [];
58098
58653
  for (const macro of macros) {
58654
+ if (macro.range.start.line < passageStart) continue;
58099
58655
  if (macro.range.start.line > position.line || macro.range.start.line === position.line && macro.range.start.character >= position.character) {
58100
58656
  break;
58101
58657
  }
@@ -58146,7 +58702,7 @@ function getMacroCompletions(workspace) {
58146
58702
  kind: 3,
58147
58703
  // CompletionItemKind.Function
58148
58704
  detail: `(widget) ${widget.name}`,
58149
- documentation: widget.params.length > 0 ? `Parameters: ${widget.params.map((p) => `@${p}`).join(", ")}` : void 0
58705
+ documentation: widget.params.length > 0 ? `Parameters: ${widget.params.join(", ")}` : void 0
58150
58706
  });
58151
58707
  }
58152
58708
  return items;
@@ -58421,7 +58977,7 @@ function getWidgetHover(line, position, workspace) {
58421
58977
  return null;
58422
58978
  }
58423
58979
  function buildWidgetHover(widget, line, nameStart, nameEnd) {
58424
- const sig = widget.params.length > 0 ? widget.params.map((p) => `@${p}`).join(", ") : "no parameters";
58980
+ const sig = widget.params.length > 0 ? widget.params.join(", ") : "no parameters";
58425
58981
  return {
58426
58982
  contents: `**Widget** \`${widget.name}\`
58427
58983
 
@@ -58495,7 +59051,7 @@ function getSignatureHelp(uri, position, workspace) {
58495
59051
  }
58496
59052
  const widget = workspace.widgets.getWidget(macroName);
58497
59053
  if (widget && widget.params.length > 0) {
58498
- const paramLabels = widget.params.map((p) => `@${p}`);
59054
+ const paramLabels = widget.params;
58499
59055
  return {
58500
59056
  signatures: [{
58501
59057
  label: `{${macroName} ${paramLabels.join(", ")}}`,
@@ -58551,53 +59107,21 @@ function getDefinition(uri, position, workspace) {
58551
59107
  const lines = text.split("\n");
58552
59108
  if (position.line >= lines.length) return null;
58553
59109
  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;
59110
+ const passageResult = getPassageRefDefinition(text, position, workspace);
59111
+ if (passageResult) return passageResult;
58558
59112
  const widgetResult = getWidgetDefinition(line, position, workspace);
58559
59113
  if (widgetResult) return widgetResult;
58560
59114
  return null;
58561
59115
  }
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;
59116
+ function getPassageRefDefinition(text, position, workspace) {
59117
+ const ref = findPassageRefAt(text, position);
59118
+ if (!ref) return null;
59119
+ const passage = workspace.passages.getPassage(ref.name);
59120
+ if (!passage) return null;
59121
+ return {
59122
+ uri: passage.uri,
59123
+ range: passage.headerEnd
59124
+ };
58601
59125
  }
58602
59126
  function getWidgetDefinition(line, position, workspace) {
58603
59127
  const re = /\{([A-Za-z_$][\w$]*)/g;
@@ -58697,19 +59221,25 @@ function findReferences(uri, position, workspace, includeDeclaration) {
58697
59221
  }
58698
59222
  }
58699
59223
  {
58700
- const widgetRegex = /\{([A-Za-z_$][\w$]*)/g;
59224
+ const widgetRegex = /\{\/?([A-Za-z_$][\w$]*)/g;
58701
59225
  let match2;
58702
59226
  while ((match2 = widgetRegex.exec(line)) !== null) {
58703
59227
  const name = match2[1];
58704
- const nameStart = match2.index + 1;
59228
+ const nameStart = match2.index + match2[0].length - name.length;
58705
59229
  const nameEnd = nameStart + name.length;
58706
59230
  if (position.character >= nameStart && position.character <= nameEnd) {
58707
- if (!workspace.macros.getMacro(name) && workspace.widgets.getWidget(name)) {
59231
+ const widget = workspace.widgets.getWidget(name);
59232
+ const isClosing = match2[0][1] === "/";
59233
+ if (!workspace.macros.getMacro(name) && widget && (!isClosing || widget.block)) {
58708
59234
  return findWidgetReferences(name, workspace, includeDeclaration);
58709
59235
  }
58710
59236
  }
58711
59237
  }
58712
59238
  }
59239
+ const passageRef = findPassageRefAt(text, position);
59240
+ if (passageRef) {
59241
+ return findPassageReferences(passageRef.name, workspace, includeDeclaration);
59242
+ }
58713
59243
  {
58714
59244
  const allPassages = workspace.passages.getAllPassages();
58715
59245
  const passageNames = new Set(allPassages.map((p) => p.name));
@@ -58735,39 +59265,16 @@ function findPassageReferences(passageName, workspace, includeDeclaration) {
58735
59265
  if (passage) {
58736
59266
  locations.push({
58737
59267
  uri: passage.uri,
58738
- range: passage.headerEnd
59268
+ range: passage.nameRange
58739
59269
  });
58740
59270
  }
58741
59271
  }
58742
- const gotoRegex = /\{(?:goto|include)\s+"([^"]+)"\s*\}/gi;
58743
- const linkMacroRegex = /\{(?:link|button)\s+"[^"]*"\s+"([^"]+)"\s*\}/gi;
58744
59272
  for (const docUri of workspace.documents.getUris()) {
58745
59273
  const docText = workspace.documents.getText(docUri);
58746
59274
  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
- }
59275
+ for (const ref of [...parseLinks(docText), ...parseMacroPassageRefs(docText)]) {
59276
+ if (ref.name === passageName) {
59277
+ locations.push({ uri: docUri, range: ref.range });
58771
59278
  }
58772
59279
  }
58773
59280
  }
@@ -58806,30 +59313,30 @@ function findTransientReferences(varName, workspace, includeDeclaration) {
58806
59313
  }
58807
59314
  function findWidgetReferences(widgetName, workspace, includeDeclaration) {
58808
59315
  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
- }
59316
+ const widget = workspace.widgets.getWidget(widgetName);
59317
+ if (includeDeclaration && widget) {
59318
+ locations.push({ uri: widget.uri, range: widget.range });
58814
59319
  }
58815
- const widgetInvocationRegex = /\{([A-Za-z_$][\w$]*)\b/g;
59320
+ const widgetInvocationRegex = /\{(\/)?([A-Za-z_$][\w$]*)\b/g;
59321
+ const lowerName = widgetName.toLowerCase();
59322
+ const isBlock = widget?.block ?? false;
58816
59323
  for (const docUri of workspace.documents.getUris()) {
58817
59324
  const docText = workspace.documents.getText(docUri);
58818
59325
  if (!docText) continue;
58819
59326
  const lines = docText.split("\n");
58820
59327
  for (let lineNum = 0; lineNum < lines.length; lineNum++) {
58821
59328
  const line = lines[lineNum];
58822
- if (/\{widget\s+"/i.test(line)) continue;
58823
59329
  widgetInvocationRegex.lastIndex = 0;
58824
59330
  let match2;
58825
59331
  while ((match2 = widgetInvocationRegex.exec(line)) !== null) {
58826
- if (match2[1] === widgetName) {
58827
- const nameStart = match2.index + 1;
59332
+ if (match2[1] && !isBlock) continue;
59333
+ if (match2[2].toLowerCase() === lowerName) {
59334
+ const nameStart = match2.index + 1 + (match2[1] ? 1 : 0);
58828
59335
  locations.push({
58829
59336
  uri: docUri,
58830
59337
  range: {
58831
59338
  start: { line: lineNum, character: nameStart },
58832
- end: { line: lineNum, character: nameStart + widgetName.length }
59339
+ end: { line: lineNum, character: nameStart + match2[2].length }
58833
59340
  }
58834
59341
  });
58835
59342
  }
@@ -58897,10 +59404,13 @@ function computeRename(uri, position, newName, workspace) {
58897
59404
  }
58898
59405
  case "variable": {
58899
59406
  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);
59407
+ const refs = symbol2.sigil === "%" ? findTransientReferences(symbol2.name, workspace, true) : findVariableReferences(symbol2.name, workspace, true);
58902
59408
  for (const ref of refs) {
58903
- addEdit(ref.uri, ref.range, bareName);
59409
+ const start = ref.range.start.character + 1;
59410
+ addEdit(ref.uri, {
59411
+ start: { line: ref.range.start.line, character: start },
59412
+ end: { line: ref.range.start.line, character: start + symbol2.name.length }
59413
+ }, bareName);
58904
59414
  }
58905
59415
  break;
58906
59416
  }
@@ -58911,23 +59421,7 @@ function computeRename(uri, position, newName, workspace) {
58911
59421
  }
58912
59422
  const widget = workspace.widgets.getWidget(symbol2.name);
58913
59423
  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
- }
59424
+ addEdit(widget.uri, widget.nameRange, newName);
58931
59425
  }
58932
59426
  break;
58933
59427
  }
@@ -58969,6 +59463,7 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58969
59463
  return {
58970
59464
  kind: "variable",
58971
59465
  name: match2[1],
59466
+ sigil: "$",
58972
59467
  range: {
58973
59468
  start: { line: position.line, character: start },
58974
59469
  end: { line: position.line, character: end }
@@ -58987,6 +59482,7 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58987
59482
  return {
58988
59483
  kind: "variable",
58989
59484
  name: match2[1],
59485
+ sigil: "%",
58990
59486
  range: {
58991
59487
  start: { line: position.line, character: start },
58992
59488
  end: { line: position.line, character: end }
@@ -58995,34 +59491,23 @@ function resolveSymbolAtCursor(uri, position, workspace) {
58995
59491
  }
58996
59492
  }
58997
59493
  }
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
- }
59494
+ for (const widget of workspace.widgets.getAllWidgets()) {
59495
+ const { start, end } = widget.nameRange;
59496
+ if (widget.uri === uri && position.line === start.line && position.character >= start.character && position.character <= end.character) {
59497
+ return { kind: "widget", name: widget.name, range: widget.nameRange };
59015
59498
  }
59016
59499
  }
59017
59500
  {
59018
- const re = /\{([A-Za-z_$][\w$]*)/g;
59501
+ const re = /\{\/?([A-Za-z_$][\w$]*)/g;
59019
59502
  let match2;
59020
59503
  while ((match2 = re.exec(line)) !== null) {
59021
59504
  const name = match2[1];
59022
- const nameStart = match2.index + 1;
59505
+ const nameStart = match2.index + match2[0].length - name.length;
59023
59506
  const nameEnd = nameStart + name.length;
59024
59507
  if (position.character >= nameStart && position.character <= nameEnd) {
59025
- if (!workspace.macros.getMacro(name) && workspace.widgets.getWidget(name)) {
59508
+ const widget = workspace.widgets.getWidget(name);
59509
+ const isClosing = match2[0][1] === "/";
59510
+ if (!workspace.macros.getMacro(name) && widget && (!isClosing || widget.block)) {
59026
59511
  return {
59027
59512
  kind: "widget",
59028
59513
  name,
@@ -59116,7 +59601,7 @@ function computeCodeLenses(uri, workspace) {
59116
59601
  });
59117
59602
  continue;
59118
59603
  }
59119
- const widgetMatch = line.match(/\{widget\s+"([^"]+)"/);
59604
+ const widgetMatch = line.match(/\{widget\s+["']?([^\s"'}]+)/);
59120
59605
  if (widgetMatch) {
59121
59606
  const widgetName = widgetMatch[1];
59122
59607
  const refs = findWidgetReferences(widgetName, workspace, true);
@@ -59196,34 +59681,24 @@ function computeInlayHints(uri, range2, workspace) {
59196
59681
  return hints;
59197
59682
  }
59198
59683
  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]));
59684
+ if (workspace.widgets.getAllWidgets().length === 0) return;
59202
59685
  const macros = parseMacros(text);
59686
+ const lineStarts = buildLineStarts(text);
59203
59687
  for (const macro of macros) {
59204
59688
  if (!macro.open) continue;
59205
59689
  if (macro.range.start.line < range2.start.line || macro.range.start.line > range2.end.line) continue;
59206
- const widget = widgetMap.get(macro.name);
59690
+ const widget = workspace.widgets.getWidget(macro.name);
59207
59691
  if (!widget || widget.params.length === 0) continue;
59208
59692
  if (!macro.rawArgs || macro.rawArgs.trim() === "") continue;
59209
59693
  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;
59694
+ const macroEnd = lineStarts[macro.range.end.line] + macro.range.end.character;
59695
+ const argsStart = macroEnd - 1 - macro.rawArgs.length;
59217
59696
  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
- }
59697
+ hints.push({
59698
+ position: offsetToPosition(argsStart + args2[i].start, lineStarts),
59699
+ label: `${widget.params[i]}:`,
59700
+ kind: "parameter"
59701
+ });
59227
59702
  }
59228
59703
  }
59229
59704
  }
@@ -59506,12 +59981,15 @@ var init_semantic_tokens = __esm({
59506
59981
  });
59507
59982
 
59508
59983
  // src/plugins/code-actions.ts
59509
- function computeCodeActions(uri, diagnostics, workspace) {
59984
+ import { readFileSync as readFileSync7 } from "node:fs";
59985
+ import { basename, dirname as dirname2, join as join3 } from "node:path";
59986
+ import { fileURLToPath as fileURLToPath6, pathToFileURL as pathToFileURL4 } from "node:url";
59987
+ function computeCodeActions(uri, diagnostics, workspace, options = {}) {
59510
59988
  const actions = [];
59511
59989
  for (const diag of diagnostics) {
59512
59990
  switch (diag.code) {
59513
59991
  case DiagnosticCode.UndefinedMacro: {
59514
- const action = fixUndefinedMacro(diag);
59992
+ const action = fixUndefinedMacro(uri, diag, workspace, options);
59515
59993
  if (action) actions.push(action);
59516
59994
  break;
59517
59995
  }
@@ -59539,27 +60017,141 @@ function computeCodeActions(uri, diagnostics, workspace) {
59539
60017
  }
59540
60018
  return actions;
59541
60019
  }
59542
- function fixUndefinedMacro(diag) {
60020
+ function fixUndefinedMacro(uri, diag, workspace, options) {
59543
60021
  const match2 = diag.message.match(/\{(\w[\w-]*)\}/);
59544
60022
  if (!match2) return null;
59545
60023
  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`,
60024
+ const root = resolveConfigRoot(uri, options.workspaceRoot);
60025
+ if (!root) return null;
60026
+ const existingPath = findConfigFile(root);
60027
+ const configPath = existingPath ?? join3(root, DEFAULT_CONFIG_FILENAME);
60028
+ const configUri = pathToFileURL4(configPath).toString();
60029
+ const action = {
60030
+ title: `Add '${macroName}' to ${basename(configPath)}`,
59553
60031
  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
- }]
60032
+ diagnosticCodes: [DiagnosticCode.UndefinedMacro]
60033
+ };
60034
+ if (!existingPath) {
60035
+ const start = { line: 0, character: 0 };
60036
+ return {
60037
+ ...action,
60038
+ createFile: configUri,
60039
+ edits: [{
60040
+ uri: configUri,
60041
+ range: { start, end: start },
60042
+ newText: (0, import_yaml2.stringify)({ macros: { [macroName]: NEW_MACRO_ENTRY } })
60043
+ }]
60044
+ };
60045
+ }
60046
+ let text = workspace.documents.getText(configUri);
60047
+ if (text === void 0) {
60048
+ try {
60049
+ text = readFileSync7(existingPath, "utf-8");
60050
+ } catch {
60051
+ return null;
60052
+ }
60053
+ }
60054
+ const edit = configPath.toLowerCase().endsWith(".json") ? addMacroToJsonConfig(text, macroName) : addMacroToYamlConfig(text, macroName);
60055
+ if (!edit) return null;
60056
+ return { ...action, edits: [{ uri: configUri, ...edit }] };
60057
+ }
60058
+ function resolveConfigRoot(uri, workspaceRoot) {
60059
+ if (workspaceRoot) {
60060
+ return workspaceRoot.startsWith("file:") ? fileURLToPath6(workspaceRoot) : workspaceRoot;
60061
+ }
60062
+ if (!uri.startsWith("file:")) return null;
60063
+ const docDir = dirname2(fileURLToPath6(uri));
60064
+ let search = docDir;
60065
+ for (let i = 0; i < 10; i++) {
60066
+ if (findConfigFile(search)) return search;
60067
+ const parent = dirname2(search);
60068
+ if (parent === search) break;
60069
+ search = parent;
60070
+ }
60071
+ return docDir;
60072
+ }
60073
+ function macrosPath(root) {
60074
+ const legacy = root?.["spindle-0"];
60075
+ return legacy !== null && typeof legacy === "object" ? ["spindle-0", "macros"] : ["macros"];
60076
+ }
60077
+ function definesMacro(macros, macroName) {
60078
+ if (macros === null || typeof macros !== "object") return false;
60079
+ const lower = macroName.toLowerCase();
60080
+ return Object.keys(macros).some((k) => k.toLowerCase() === lower);
60081
+ }
60082
+ function addMacroToYamlConfig(text, macroName) {
60083
+ const doc = (0, import_yaml2.parseDocument)(text);
60084
+ if (doc.errors.length > 0) return null;
60085
+ if (doc.contents !== null && !(0, import_yaml2.isMap)(doc.contents)) return null;
60086
+ const path2 = macrosPath(doc.toJS());
60087
+ const parent = path2.length > 1 ? doc.getIn(path2.slice(0, -1), true) : doc.contents;
60088
+ const macros = doc.getIn(path2, true);
60089
+ if ((0, import_yaml2.isMap)(macros) && definesMacro(macros.toJS(doc), macroName)) return null;
60090
+ if ((0, import_yaml2.isMap)(parent) && (0, import_yaml2.isMap)(macros) && !macros.flow && macros.items.length > 0 && macros.range) {
60091
+ const macrosKey = parent.items.find((p) => (0, import_yaml2.isScalar)(p.key) && p.key.value === "macros")?.key;
60092
+ const firstKey = macros.items[0].key;
60093
+ if ((0, import_yaml2.isScalar)(macrosKey) && macrosKey.range && (0, import_yaml2.isScalar)(firstKey) && firstKey.range) {
60094
+ const indent = columnOf(text, firstKey.range[0]);
60095
+ const step = Math.max(indent - columnOf(text, macrosKey.range[0]), 1);
60096
+ const entry = (0, import_yaml2.stringify)({ [macroName]: NEW_MACRO_ENTRY }, { indent: step }).replace(/^(?=.)/gm, " ".repeat(indent));
60097
+ let offset = macros.range[1];
60098
+ if (offset > 0 && text[offset - 1] !== "\n") {
60099
+ const nl = text.indexOf("\n", offset);
60100
+ offset = nl === -1 ? text.length : nl + 1;
60101
+ }
60102
+ const lead = offset === text.length && text.length > 0 && !text.endsWith("\n") ? "\n" : "";
60103
+ const pos = offsetToPosition2(text, offset);
60104
+ return { range: { start: pos, end: pos }, newText: lead + entry };
60105
+ }
60106
+ }
60107
+ if ((0, import_yaml2.isMap)(macros)) {
60108
+ macros.set(macroName, doc.createNode(NEW_MACRO_ENTRY));
60109
+ } else {
60110
+ doc.setIn(path2, doc.createNode({ [macroName]: NEW_MACRO_ENTRY }));
60111
+ }
60112
+ return minimalEdit(text, doc.toString({ lineWidth: 0 }));
60113
+ }
60114
+ function addMacroToJsonConfig(text, macroName) {
60115
+ let raw;
60116
+ try {
60117
+ raw = text.trim() ? JSON.parse(text) : {};
60118
+ } catch {
60119
+ return null;
60120
+ }
60121
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
60122
+ const path2 = macrosPath(raw);
60123
+ const container = path2.length > 1 ? raw[path2[0]] : raw;
60124
+ const macros = container.macros;
60125
+ if (definesMacro(macros, macroName)) return null;
60126
+ if (macros !== null && typeof macros === "object" && !Array.isArray(macros)) {
60127
+ macros[macroName] = NEW_MACRO_ENTRY;
60128
+ } else {
60129
+ container.macros = { [macroName]: NEW_MACRO_ENTRY };
60130
+ }
60131
+ const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
60132
+ const newline = text.endsWith("\n") || !text.trim() ? "\n" : "";
60133
+ return minimalEdit(text, JSON.stringify(raw, null, indent) + newline);
60134
+ }
60135
+ function columnOf(text, offset) {
60136
+ return offset - (text.lastIndexOf("\n", offset - 1) + 1);
60137
+ }
60138
+ function offsetToPosition2(text, offset) {
60139
+ const before = text.slice(0, offset);
60140
+ const lineStart = before.lastIndexOf("\n") + 1;
60141
+ return { line: before.split("\n").length - 1, character: offset - lineStart };
60142
+ }
60143
+ function minimalEdit(oldText, newText) {
60144
+ const max = Math.min(oldText.length, newText.length);
60145
+ let prefix = 0;
60146
+ while (prefix < max && oldText[prefix] === newText[prefix]) prefix++;
60147
+ let suffix = 0;
60148
+ while (suffix < max - prefix && oldText[oldText.length - 1 - suffix] === newText[newText.length - 1 - suffix]) suffix++;
60149
+ return {
60150
+ range: {
60151
+ start: offsetToPosition2(oldText, prefix),
60152
+ end: offsetToPosition2(oldText, oldText.length - suffix)
60153
+ },
60154
+ newText: newText.slice(prefix, newText.length - suffix)
59563
60155
  };
59564
60156
  }
59565
60157
  function fixUndeclaredVariable(diag, workspace) {
@@ -59669,11 +60261,15 @@ function toLspRange6(r) {
59669
60261
  end: { line: r.end.line, character: r.end.character }
59670
60262
  };
59671
60263
  }
59672
- var codeActionsPlugin;
60264
+ var import_yaml2, DEFAULT_CONFIG_FILENAME, NEW_MACRO_ENTRY, codeActionsPlugin;
59673
60265
  var init_code_actions = __esm({
59674
60266
  "src/plugins/code-actions.ts"() {
59675
60267
  "use strict";
60268
+ import_yaml2 = __toESM(require_dist(), 1);
59676
60269
  init_diagnostic_codes();
60270
+ init_config_loader();
60271
+ DEFAULT_CONFIG_FILENAME = "spindle.config.yaml";
60272
+ NEW_MACRO_ENTRY = { description: "" };
59677
60273
  codeActionsPlugin = {
59678
60274
  id: "code-actions",
59679
60275
  capabilities: {
@@ -59696,7 +60292,8 @@ var init_code_actions = __esm({
59696
60292
  const actions = computeCodeActions(
59697
60293
  params.textDocument.uri,
59698
60294
  diagnostics,
59699
- ctx.workspace
60295
+ ctx.workspace,
60296
+ { workspaceRoot: ctx.config.workspaceRoot }
59700
60297
  );
59701
60298
  return actions.map((a) => ({
59702
60299
  title: a.title,
@@ -59704,7 +60301,16 @@ var init_code_actions = __esm({
59704
60301
  diagnostics: params.context.diagnostics.filter(
59705
60302
  (d) => a.diagnosticCodes.includes(String(d.code))
59706
60303
  ),
59707
- edit: {
60304
+ edit: a.createFile ? {
60305
+ // Resource operations are only expressible via documentChanges
60306
+ documentChanges: [
60307
+ { kind: "create", uri: a.createFile, options: { ignoreIfExists: true } },
60308
+ ...a.edits.map((e) => ({
60309
+ textDocument: { uri: e.uri, version: null },
60310
+ edits: [{ range: toLspRange6(e.range), newText: e.newText }]
60311
+ }))
60312
+ ]
60313
+ } : {
59708
60314
  changes: Object.fromEntries(
59709
60315
  a.edits.map((e) => [
59710
60316
  e.uri,
@@ -59744,7 +60350,7 @@ function computeDocumentSymbols(uri, workspace) {
59744
60350
  if (passage.tags?.includes("widget")) {
59745
60351
  const contentStart = passage.range.start.line + 1;
59746
60352
  const contentEnd = passage.range.end.line + 1;
59747
- const widgetDefRegex2 = /\{widget\s+"([^"]+)"[^}]*\}/gi;
60353
+ const widgetDefRegex2 = /\{widget\s+["']?([^\s"'}]+)["']?[^}]*\}/gi;
59748
60354
  for (let i = contentStart; i < contentEnd && i < lines.length; i++) {
59749
60355
  widgetDefRegex2.lastIndex = 0;
59750
60356
  let match2;
@@ -59849,14 +60455,19 @@ function searchWorkspaceSymbols(query, workspace) {
59849
60455
  range: widget.range
59850
60456
  });
59851
60457
  }
59852
- for (const [name, decl] of workspace.variables.getDeclared()) {
59853
- const displayName = "$" + name;
60458
+ const declarations = [
60459
+ ...workspace.variables.getDeclared().values(),
60460
+ ...workspace.variables.getDeclaredTransient().values()
60461
+ ];
60462
+ for (const decl of declarations) {
60463
+ const displayName = decl.sigil + decl.name;
59854
60464
  if (lowerQuery && !displayName.toLowerCase().includes(lowerQuery)) continue;
60465
+ if (!decl.declarationUri || !decl.declarationRange) continue;
59855
60466
  results.push({
59856
60467
  name: displayName,
59857
60468
  kind: import_vscode_languageserver3.SymbolKind.Variable,
59858
- uri: decl.declarationUri ?? "",
59859
- range: decl.declarationRange ?? { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
60469
+ uri: decl.declarationUri,
60470
+ range: decl.declarationRange
59860
60471
  });
59861
60472
  }
59862
60473
  return results;
@@ -59912,7 +60523,11 @@ function computeFoldingRanges(uri, workspace) {
59912
60523
  }
59913
60524
  }
59914
60525
  const macros = parseMacros(text);
59915
- pairMacros(macros, (name) => workspace.macros.isBlock(name));
60526
+ pairMacros(
60527
+ macros,
60528
+ (name) => workspace.macros.isBlock(name),
60529
+ passages.map((p) => p.range.start.line)
60530
+ );
59916
60531
  for (const macro of macros) {
59917
60532
  if (macro.open && macro.pair !== -1) {
59918
60533
  const closingMacro = macros[macro.pair];
@@ -60059,17 +60674,22 @@ var server_exports2 = {};
60059
60674
  __export(server_exports2, {
60060
60675
  startServer: () => startServer
60061
60676
  });
60062
- import { fileURLToPath as fileURLToPath6, pathToFileURL as pathToFileURL4 } from "node:url";
60063
- import { readFileSync as readFileSync7 } from "node:fs";
60677
+ import { fileURLToPath as fileURLToPath7, pathToFileURL as pathToFileURL5 } from "node:url";
60678
+ import { readFileSync as readFileSync8 } from "node:fs";
60679
+ import { basename as basename2 } from "node:path";
60064
60680
  function uriToFsPath(uri) {
60065
60681
  try {
60066
- return fileURLToPath6(uri);
60682
+ return fileURLToPath7(uri);
60067
60683
  } catch {
60068
60684
  return uri;
60069
60685
  }
60070
60686
  }
60071
60687
  function fsPathToUri(fsPath) {
60072
- return pathToFileURL4(fsPath).toString();
60688
+ return pathToFileURL5(fsPath).toString();
60689
+ }
60690
+ function isConfigFileUri(uri) {
60691
+ const name = basename2(uriToFsPath(uri));
60692
+ return name.startsWith("spindle.config.") || name.includes("twee-config.");
60073
60693
  }
60074
60694
  function startServer(_args) {
60075
60695
  const connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -60087,15 +60707,17 @@ function startServer(_args) {
60087
60707
  diagnostics: initOptions.diagnostics,
60088
60708
  workspaceRoot
60089
60709
  };
60090
- workspace = new WorkspaceModel();
60710
+ workspace = new WorkspaceModel(workspaceRoot ? { workspaceRoot } : void 0);
60091
60711
  console.error("[spindle-lsp] workspaceRoot:", workspaceRoot ?? "undefined");
60712
+ console.error("[spindle-lsp] builtin macros:", workspace.macros.builtinsPath ?? "not found");
60713
+ for (const warning of workspace.macros.warnings) console.error("[spindle-lsp]", warning);
60092
60714
  activePlugins = loadPlugins(allPlugins, config2);
60093
60715
  console.error("[spindle-lsp] plugins loaded:", activePlugins.map((p) => p.id).join(", "));
60094
60716
  for (const plugin of activePlugins) {
60095
60717
  plugin.initialize({ connection, workspace, config: config2 });
60096
60718
  }
60097
60719
  if (Object.keys(projectConfig.macros).length > 0) {
60098
- workspace.macros.loadSupplements(projectConfig.macros);
60720
+ workspace.macros.loadConfig(projectConfig.macros);
60099
60721
  console.error("[spindle-lsp] loaded config macros:", Object.keys(projectConfig.macros).length);
60100
60722
  }
60101
60723
  return {
@@ -60119,6 +60741,9 @@ function startServer(_args) {
60119
60741
  try {
60120
60742
  const fileContents = await scanWorkspaceFiles(workspaceRoot);
60121
60743
  console.error("[spindle-lsp] scanned", fileContents.size, "files from root:", workspaceRoot ?? "undefined");
60744
+ for (const uri of documents.keys()) {
60745
+ fileContents.delete(uri);
60746
+ }
60122
60747
  workspace.initialize(fileContents);
60123
60748
  console.error("[spindle-lsp] workspace initialized, macros:", workspace.macros.getAllMacros().length);
60124
60749
  } catch (err) {
@@ -60158,23 +60783,40 @@ function startServer(_args) {
60158
60783
  if (workspace) {
60159
60784
  try {
60160
60785
  const fsPath = uriToFsPath(textDocument.uri);
60161
- const text = readFileSync7(fsPath, "utf-8");
60786
+ const text = readFileSync8(fsPath, "utf-8");
60162
60787
  workspace.documents.update(textDocument.uri, text);
60163
60788
  } catch {
60164
60789
  workspace.documents.close(textDocument.uri);
60165
60790
  }
60166
60791
  }
60167
60792
  });
60793
+ function reloadProjectConfig() {
60794
+ if (!workspaceRoot) return;
60795
+ try {
60796
+ const projectConfig = loadConfigFromDisk(workspaceRoot);
60797
+ workspace.macros.loadConfig(projectConfig.macros);
60798
+ console.error("[spindle-lsp] reloaded config macros:", Object.keys(projectConfig.macros).length);
60799
+ } catch (err) {
60800
+ console.error("[spindle-lsp] config reload failed:", err);
60801
+ return;
60802
+ }
60803
+ workspace.refresh();
60804
+ }
60168
60805
  connection.onDidChangeWatchedFiles(({ changes }) => {
60169
60806
  if (!workspace) return;
60807
+ let configChanged = false;
60170
60808
  for (const change of changes) {
60809
+ if (isConfigFileUri(change.uri)) {
60810
+ configChanged = true;
60811
+ continue;
60812
+ }
60813
+ if (documents.has(change.uri)) continue;
60171
60814
  if (change.type === import_node.FileChangeType.Deleted) {
60172
60815
  workspace.documents.close(change.uri);
60173
- documents.delete(change.uri);
60174
60816
  } else {
60175
60817
  try {
60176
60818
  const fsPath = uriToFsPath(change.uri);
60177
- const text = readFileSync7(fsPath, "utf-8");
60819
+ const text = readFileSync8(fsPath, "utf-8");
60178
60820
  if (workspace.documents.has(change.uri)) {
60179
60821
  workspace.documents.update(change.uri, text);
60180
60822
  } else {
@@ -60184,6 +60826,9 @@ function startServer(_args) {
60184
60826
  }
60185
60827
  }
60186
60828
  }
60829
+ if (configChanged) {
60830
+ reloadProjectConfig();
60831
+ }
60187
60832
  });
60188
60833
  connection.onShutdown(() => {
60189
60834
  for (const plugin of activePlugins) {
@@ -60204,7 +60849,7 @@ async function scanWorkspaceFiles(root) {
60204
60849
  });
60205
60850
  for (const filePath of files) {
60206
60851
  try {
60207
- const text = readFileSync7(filePath, "utf-8");
60852
+ const text = readFileSync8(filePath, "utf-8");
60208
60853
  const uri = fsPathToUri(filePath);
60209
60854
  contents.set(uri, text);
60210
60855
  } catch {
@@ -60231,7 +60876,7 @@ var init_server4 = __esm({
60231
60876
 
60232
60877
  // src/bin.ts
60233
60878
  var args = process.argv.slice(2);
60234
- var VERSION = "0.9.0";
60879
+ var VERSION = "0.9.2";
60235
60880
  var HELP = `spindle-lsp v${VERSION}
60236
60881
  Language Server Protocol server for Spindle story format
60237
60882