@rohal12/spindle-lsp 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -210,6 +210,10 @@ var init_passage_index = __esm({
210
210
  getStoryVariables() {
211
211
  return this.getPassage("StoryVariables");
212
212
  }
213
+ /** Get the StoryTransients passage, if any. */
214
+ getStoryTransients() {
215
+ return this.getPassage("StoryTransients");
216
+ }
213
217
  /** Get the StoryInit passage, if any. */
214
218
  getStoryInit() {
215
219
  return this.getPassage("StoryInit");
@@ -412,14 +416,16 @@ var init_macro_registry = __esm({
412
416
  });
413
417
 
414
418
  // src/core/workspace/variable-tracker.ts
415
- var varRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, VariableTracker;
419
+ var varRefRegex, transientRefRegex, EXCLUDED_PASSAGES, CLEAN_PATTERNS, VariableTracker;
416
420
  var init_variable_tracker = __esm({
417
421
  "src/core/workspace/variable-tracker.ts"() {
418
422
  "use strict";
419
423
  init_passage_parser();
420
424
  varRefRegex = /\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
425
+ transientRefRegex = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
421
426
  EXCLUDED_PASSAGES = /* @__PURE__ */ new Set([
422
427
  "StoryVariables",
428
+ "StoryTransients",
423
429
  "StoryInit",
424
430
  "StoryData",
425
431
  "StoryScript",
@@ -442,8 +448,12 @@ var init_variable_tracker = __esm({
442
448
  VariableTracker = class {
443
449
  declared = /* @__PURE__ */ new Map();
444
450
  _hasStoryVariables = false;
451
+ declaredTransient = /* @__PURE__ */ new Map();
452
+ _hasStoryTransients = false;
445
453
  /** Per-URI list of variable usages. */
446
454
  usagesByUri = /* @__PURE__ */ new Map();
455
+ /** Per-URI list of transient variable usages. */
456
+ transientUsagesByUri = /* @__PURE__ */ new Map();
447
457
  /**
448
458
  * Parse the StoryVariables passage content for declarations.
449
459
  * Each line like `$name = value` becomes a declaration.
@@ -474,14 +484,46 @@ var init_variable_tracker = __esm({
474
484
  this.declared.set(name, decl);
475
485
  }
476
486
  }
487
+ /**
488
+ * Parse the StoryTransients passage content for declarations.
489
+ * Each line like `%name = value` becomes a declaration.
490
+ */
491
+ parseStoryTransients(content) {
492
+ this.declaredTransient.clear();
493
+ this._hasStoryTransients = true;
494
+ const lines = content.split("\n");
495
+ for (const line of lines) {
496
+ const trimmed = line.trim();
497
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("<!--")) continue;
498
+ const match = /^%([A-Za-z_$][\w$]*)\s*=\s*(.*)$/.exec(trimmed);
499
+ if (!match) continue;
500
+ const name = match[1];
501
+ const expr = match[2].trim();
502
+ const decl = { name, sigil: "%" };
503
+ if (expr.startsWith("{")) {
504
+ const fieldRegex = /(\w+)\s*:/g;
505
+ let fieldMatch;
506
+ const fields = [];
507
+ while ((fieldMatch = fieldRegex.exec(expr)) !== null) {
508
+ fields.push(fieldMatch[1]);
509
+ }
510
+ if (fields.length > 0) {
511
+ decl.fields = fields;
512
+ }
513
+ }
514
+ this.declaredTransient.set(name, decl);
515
+ }
516
+ }
477
517
  /**
478
518
  * Scan a document for variable usages.
479
519
  * Identifies passages in the document and scans non-special ones.
480
520
  */
481
521
  scanDocument(uri, text, _macros) {
482
522
  this.usagesByUri.delete(uri);
523
+ this.transientUsagesByUri.delete(uri);
483
524
  const lines = text.split("\n");
484
525
  const usages = [];
526
+ const transientUsages = [];
485
527
  const passageBoundaries = [];
486
528
  for (let i = 0; i < lines.length; i++) {
487
529
  const header = parsePassageHeader(lines[i], i);
@@ -523,10 +565,32 @@ var init_variable_tracker = __esm({
523
565
  };
524
566
  usages.push({ uri, baseName, fullName, range });
525
567
  }
568
+ const tre = new RegExp(transientRefRegex.source, "g");
569
+ let tmatch;
570
+ while ((tmatch = tre.exec(cleaned)) !== null) {
571
+ const fullName = tmatch[1];
572
+ const baseName = fullName.split(".")[0];
573
+ const charOffset = tmatch.index;
574
+ let localLine = 0;
575
+ for (let i = 0; i < lineOffsets.length; i++) {
576
+ if (lineOffsets[i] > charOffset) break;
577
+ localLine = i;
578
+ }
579
+ const character = charOffset - lineOffsets[localLine];
580
+ const absoluteLine = contentStartLine + localLine;
581
+ const range = {
582
+ start: { line: absoluteLine, character },
583
+ end: { line: absoluteLine, character: character + tmatch[0].length }
584
+ };
585
+ transientUsages.push({ uri, baseName, fullName, range });
586
+ }
526
587
  }
527
588
  if (usages.length > 0) {
528
589
  this.usagesByUri.set(uri, usages);
529
590
  }
591
+ if (transientUsages.length > 0) {
592
+ this.transientUsagesByUri.set(uri, transientUsages);
593
+ }
530
594
  }
531
595
  /** Get all declared variables. */
532
596
  getDeclared() {
@@ -562,6 +626,40 @@ var init_variable_tracker = __esm({
562
626
  hasStoryVariables() {
563
627
  return this._hasStoryVariables;
564
628
  }
629
+ /** Get all declared transient variables. */
630
+ getDeclaredTransient() {
631
+ return this.declaredTransient;
632
+ }
633
+ /** Get all usages of a transient variable by base name. */
634
+ getTransientUsages(name) {
635
+ const results = [];
636
+ for (const usages of this.transientUsagesByUri.values()) {
637
+ for (const u of usages) {
638
+ if (u.baseName === name) {
639
+ results.push({ uri: u.uri, range: u.range });
640
+ }
641
+ }
642
+ }
643
+ return results;
644
+ }
645
+ /** Get undeclared transient variable usages in a specific document. */
646
+ getUndeclaredTransient(uri) {
647
+ const usages = this.transientUsagesByUri.get(uri);
648
+ if (!usages) return [];
649
+ const results = [];
650
+ const seen = /* @__PURE__ */ new Set();
651
+ for (const u of usages) {
652
+ if (!this.declaredTransient.has(u.baseName) && !seen.has(u.baseName)) {
653
+ seen.add(u.baseName);
654
+ results.push({ name: u.baseName, range: u.range });
655
+ }
656
+ }
657
+ return results;
658
+ }
659
+ /** Whether a StoryTransients passage has been parsed. */
660
+ hasStoryTransients() {
661
+ return this._hasStoryTransients;
662
+ }
565
663
  };
566
664
  }
567
665
  });
@@ -721,7 +819,7 @@ var variableInterpolationRegex, macroRegex;
721
819
  var init_macro_parser = __esm({
722
820
  "src/core/parsing/macro-parser.ts"() {
723
821
  "use strict";
724
- variableInterpolationRegex = /(?<!\\)\{([$_@][A-Za-z_$][\w$.]*)\}/g;
822
+ variableInterpolationRegex = /(?<!\\)\{([$_@%][A-Za-z_$][\w$.]*)\}/g;
725
823
  macroRegex = /(?<!\\)\{(\/)?(?:((?:[#.][a-zA-Z][\w-]*\s*)*)([A-Za-z][\w-]*))(?:\s+((?:(?:`(?:\\.|[^`\\])*?`)|(?:"(?:\\.|[^"\\])*?")|(?:'(?:\\.|[^'\\])*?')|(?:\[(?:[<>]?[Ii][Mm][Gg])?\[[^\r\n]*?\]\]+)|[^}]|(?:}))*?))?\}/gmi;
726
824
  }
727
825
  });
@@ -799,12 +897,12 @@ var init_macro_supplements = __esm({
799
897
  },
800
898
  set: {
801
899
  name: "set",
802
- description: "Sets variables based on the given expression.\n\nUsage: `{set $var to value}`\n\nSupports `$var` (story), `_var` (temporary), and `@var` (local) variables.",
900
+ description: "Sets variables based on the given expression.\n\nUsage: `{set $var to value}`\n\nSupports `$var` (story), `_var` (temporary), `@var` (local), and `%var` (transient) variables.",
803
901
  parameters: ["...text"]
804
902
  },
805
903
  unset: {
806
904
  name: "unset",
807
- description: "Unsets one or more variables.\n\nUsage: `{unset $var}`",
905
+ description: "Unsets one or more variables.\n\nUsage: `{unset $var}`\n\nSupports `$var` (story), `_var` (temporary), `@var` (local), and `%var` (transient) variables.",
808
906
  parameters: ["...text"]
809
907
  },
810
908
  computed: {
@@ -1143,6 +1241,23 @@ var init_workspace_model = __esm({
1143
1241
  this.variables.parseStoryVariables(content);
1144
1242
  }
1145
1243
  }
1244
+ const storyTransients = this.passages.getStoryTransients();
1245
+ if (storyTransients) {
1246
+ const text = this.documents.getText(storyTransients.uri);
1247
+ if (text) {
1248
+ const lines = text.split("\n");
1249
+ const contentStart = storyTransients.headerEnd.end.line + 1;
1250
+ let contentEnd = lines.length;
1251
+ for (let i = contentStart; i < lines.length; i++) {
1252
+ if (/^::\s+/.test(lines[i])) {
1253
+ contentEnd = i;
1254
+ break;
1255
+ }
1256
+ }
1257
+ const content = lines.slice(contentStart, contentEnd).join("\n");
1258
+ this.variables.parseStoryTransients(content);
1259
+ }
1260
+ }
1146
1261
  for (const uri of this.documents.getUris()) {
1147
1262
  const text = this.documents.getText(uri);
1148
1263
  if (text) {
@@ -1190,6 +1305,7 @@ var init_diagnostic_codes = __esm({
1190
1305
  UndeclaredVariable: "SP200",
1191
1306
  UndeclaredField: "SP201",
1192
1307
  NoStoryVariables: "SP202",
1308
+ UndeclaredTransient: "SP203",
1193
1309
  BrokenPassageLink: "SP300",
1194
1310
  WidgetArgCountMismatch: "SP301",
1195
1311
  DeadEndPassage: "SP400",
@@ -1211,6 +1327,7 @@ var init_diagnostic_codes = __esm({
1211
1327
  SP200: "warning",
1212
1328
  SP201: "warning",
1213
1329
  SP202: "info",
1330
+ SP203: "warning",
1214
1331
  SP300: "warning",
1215
1332
  SP301: "warning",
1216
1333
  SP400: "hint",
@@ -1411,7 +1528,7 @@ var varTestRegexp, EOF, Lexer, notSpaceRegex, spaceRegex;
1411
1528
  var init_argument_lexer = __esm({
1412
1529
  "src/core/parsing/argument-lexer.ts"() {
1413
1530
  "use strict";
1414
- varTestRegexp = /^[$_@][$A-Z_a-z][$0-9A-Z_.a-z]*/;
1531
+ varTestRegexp = /^[$_@%][$A-Z_a-z][$0-9A-Z_.a-z]*/;
1415
1532
  EOF = -1;
1416
1533
  Lexer = class {
1417
1534
  source;
@@ -2292,7 +2409,7 @@ function validateArguments(macros, workspace, passageNames, diagnostics) {
2292
2409
  }
2293
2410
  function validateVariables(uri, workspace, diagnostics) {
2294
2411
  if (!workspace.variables.hasStoryVariables()) {
2295
- const undeclared2 = workspace.variables.getUndeclared(uri);
2412
+ const undeclared = workspace.variables.getUndeclared(uri);
2296
2413
  const text = workspace.documents.getText(uri);
2297
2414
  if (text && /\$[A-Za-z_$]/.test(text)) {
2298
2415
  const passages = workspace.passages.getPassagesInDocument(uri);
@@ -2308,15 +2425,25 @@ function validateVariables(uri, workspace, diagnostics) {
2308
2425
  ));
2309
2426
  }
2310
2427
  }
2311
- return;
2428
+ } else {
2429
+ const undeclared = workspace.variables.getUndeclared(uri);
2430
+ for (const u of undeclared) {
2431
+ diagnostics.push(makeDiag(
2432
+ u.range,
2433
+ DiagnosticCode.UndeclaredVariable,
2434
+ `Variable '$${u.name}' is not declared in StoryVariables`
2435
+ ));
2436
+ }
2312
2437
  }
2313
- const undeclared = workspace.variables.getUndeclared(uri);
2314
- for (const u of undeclared) {
2315
- diagnostics.push(makeDiag(
2316
- u.range,
2317
- DiagnosticCode.UndeclaredVariable,
2318
- `Variable '$${u.name}' is not declared in StoryVariables`
2319
- ));
2438
+ if (workspace.variables.hasStoryTransients()) {
2439
+ const undeclaredTransient = workspace.variables.getUndeclaredTransient(uri);
2440
+ for (const u of undeclaredTransient) {
2441
+ diagnostics.push(makeDiag(
2442
+ u.range,
2443
+ DiagnosticCode.UndeclaredTransient,
2444
+ `Transient variable '%${u.name}' is not declared in StoryTransients`
2445
+ ));
2446
+ }
2320
2447
  }
2321
2448
  }
2322
2449
  function validateLinks(text, passages, passageNames, diagnostics) {
@@ -3045,6 +3172,50 @@ var init_prettier_bridge = __esm({
3045
3172
  });
3046
3173
 
3047
3174
  // src/plugins/format/placeholders.ts
3175
+ function scanSpindleTokens(text) {
3176
+ const matches = [];
3177
+ let i = 0;
3178
+ while (i < text.length) {
3179
+ if (text[i] === "[" && text[i + 1] === "[") {
3180
+ const closeIdx = text.indexOf("]]", i + 2);
3181
+ if (closeIdx !== -1) {
3182
+ const end = closeIdx + 2;
3183
+ matches.push({ start: i, end, token: text.slice(i, end) });
3184
+ i = end;
3185
+ continue;
3186
+ }
3187
+ }
3188
+ if (text[i] === "{" && !(i > 0 && text[i - 1] === "\\")) {
3189
+ const next = i + 1 < text.length ? text[i + 1] : "";
3190
+ let isToken = false;
3191
+ if (next === "/") {
3192
+ isToken = i + 2 < text.length && /[A-Za-z]/.test(text[i + 2]);
3193
+ } else if (next === "#" || next === ".") {
3194
+ isToken = i + 2 < text.length && /[a-zA-Z]/.test(text[i + 2]);
3195
+ } else if (/[A-Za-z]/.test(next)) {
3196
+ isToken = true;
3197
+ } else if (/[$_@%]/.test(next)) {
3198
+ isToken = i + 2 < text.length && /[a-zA-Z]/.test(text[i + 2]);
3199
+ }
3200
+ if (isToken) {
3201
+ let depth = 1;
3202
+ let j = i + 1;
3203
+ while (j < text.length && depth > 0) {
3204
+ if (text[j] === "{") depth++;
3205
+ else if (text[j] === "}") depth--;
3206
+ j++;
3207
+ }
3208
+ if (depth === 0) {
3209
+ matches.push({ start: i, end: j, token: text.slice(i, j) });
3210
+ i = j;
3211
+ continue;
3212
+ }
3213
+ }
3214
+ }
3215
+ i++;
3216
+ }
3217
+ return matches;
3218
+ }
3048
3219
  function isInsideAttribute(text, pos) {
3049
3220
  let inQuote = false;
3050
3221
  let quoteChar = "";
@@ -3081,13 +3252,15 @@ function replaceSpindleTokens(html) {
3081
3252
  const resultLines = [];
3082
3253
  for (const line of lines) {
3083
3254
  const trimmed = line.trim();
3084
- SPINDLE_TOKEN_REGEX.lastIndex = 0;
3085
- if (!SPINDLE_TOKEN_REGEX.test(trimmed)) {
3255
+ const found = scanSpindleTokens(trimmed);
3256
+ if (found.length === 0) {
3086
3257
  resultLines.push(line);
3087
3258
  continue;
3088
3259
  }
3089
- SPINDLE_TOKEN_REGEX.lastIndex = 0;
3090
- const withoutSpindle = trimmed.replace(SPINDLE_TOKEN_REGEX, "");
3260
+ let withoutSpindle = trimmed;
3261
+ for (let fi = found.length - 1; fi >= 0; fi--) {
3262
+ withoutSpindle = withoutSpindle.slice(0, found[fi].start) + withoutSpindle.slice(found[fi].end);
3263
+ }
3091
3264
  const hasHtml = HTML_TAG_REGEX.test(withoutSpindle);
3092
3265
  if (!hasHtml) {
3093
3266
  const idx = tokens.length;
@@ -3095,16 +3268,20 @@ function replaceSpindleTokens(html) {
3095
3268
  const indent = line.match(/^(\s*)/)?.[1] ?? "";
3096
3269
  resultLines.push(`${indent}<!--SP:${idx}-->`);
3097
3270
  } else {
3098
- SPINDLE_TOKEN_REGEX.lastIndex = 0;
3099
- const replaced = line.replace(SPINDLE_TOKEN_REGEX, (match, ...args2) => {
3100
- const matchOffset = args2[args2.length - 2];
3271
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
3272
+ const indentLen = indent.length;
3273
+ const assignments = found.map((t) => {
3274
+ const lineStart = t.start + indentLen;
3101
3275
  const idx = tokens.length;
3102
- tokens.push(match);
3103
- if (isInsideAttribute(line, matchOffset)) {
3104
- return `__SP${idx}__`;
3105
- }
3106
- return `<!--SP:${idx}-->`;
3276
+ tokens.push(t.token);
3277
+ const placeholder = isInsideAttribute(line, lineStart) ? `__SP${idx}__` : `<!--SP:${idx}-->`;
3278
+ return { start: lineStart, end: t.end + indentLen, placeholder };
3107
3279
  });
3280
+ let replaced = line;
3281
+ for (let ai = assignments.length - 1; ai >= 0; ai--) {
3282
+ const a = assignments[ai];
3283
+ replaced = replaced.slice(0, a.start) + a.placeholder + replaced.slice(a.end);
3284
+ }
3108
3285
  resultLines.push(replaced);
3109
3286
  }
3110
3287
  }
@@ -3118,11 +3295,10 @@ function restoreSpindleTokens(text, tokens) {
3118
3295
  }
3119
3296
  return result;
3120
3297
  }
3121
- var SPINDLE_TOKEN_REGEX, HTML_TAG_REGEX;
3298
+ var HTML_TAG_REGEX;
3122
3299
  var init_placeholders = __esm({
3123
3300
  "src/plugins/format/placeholders.ts"() {
3124
3301
  "use strict";
3125
- SPINDLE_TOKEN_REGEX = /(?:\{\/[A-Za-z][\w-]*\s*\}|\{(?:[#.][a-zA-Z][\w-]*\s*)*[A-Za-z][\w-]*(?:\s+(?:[^{}]|\{[^{}]*\})*)?\}|\{[$_@][a-zA-Z][\w.]*\}|\[\[(?:[^\]]*)\]\])/g;
3126
3302
  HTML_TAG_REGEX = /<\/?[a-zA-Z][\w-]*[\s>/]/;
3127
3303
  }
3128
3304
  });
@@ -3216,7 +3392,10 @@ async function formatDocument(text, options) {
3216
3392
  }
3217
3393
  continue;
3218
3394
  }
3219
- const indented = indentMacros(region.lines, isBlock, isDedenting);
3395
+ let indented = indentMacros(region.lines, isBlock, isDedenting);
3396
+ if (options?.maxLineLength) {
3397
+ indented = wrapLines(indented, options.maxLineLength);
3398
+ }
3220
3399
  resultLines.push(...indented);
3221
3400
  }
3222
3401
  }
@@ -3263,6 +3442,65 @@ function indentMacros(lines, isBlock, isDedenting) {
3263
3442
  async function formatRange(text, _range, options) {
3264
3443
  return formatDocument(text, options);
3265
3444
  }
3445
+ function wrapLines(lines, maxLen) {
3446
+ const result = [];
3447
+ for (const line of lines) {
3448
+ if (line.length <= maxLen) {
3449
+ result.push(line);
3450
+ continue;
3451
+ }
3452
+ const trimmed = line.trim();
3453
+ if (trimmed === "" || /^::/.test(trimmed) || /^\{[\/#.]/.test(trimmed) || // closing/opening-only macro lines
3454
+ /^\[\[/.test(trimmed) || // links
3455
+ /^\{choice\b/.test(trimmed) || // choice macros
3456
+ /^\{choices\b/.test(trimmed)) {
3457
+ result.push(line);
3458
+ continue;
3459
+ }
3460
+ const indentMatch = line.match(/^(\s*)/);
3461
+ const indent = indentMatch ? indentMatch[1] : "";
3462
+ const wrapped = wordWrap(trimmed, maxLen, indent);
3463
+ result.push(...wrapped);
3464
+ }
3465
+ return result;
3466
+ }
3467
+ function wordWrap(text, maxLen, indent) {
3468
+ const lines = [];
3469
+ let remaining = text;
3470
+ while (remaining.length > 0) {
3471
+ const currentMax = lines.length === 0 ? maxLen - indent.length : maxLen - indent.length;
3472
+ if (remaining.length <= currentMax) {
3473
+ lines.push(indent + remaining);
3474
+ break;
3475
+ }
3476
+ let breakIdx = -1;
3477
+ let inMacro = 0;
3478
+ for (let i = 0; i < remaining.length && i <= currentMax; i++) {
3479
+ if (remaining[i] === "{") inMacro++;
3480
+ else if (remaining[i] === "}") inMacro = Math.max(0, inMacro - 1);
3481
+ else if (remaining[i] === " " && inMacro === 0 && i > 0) {
3482
+ breakIdx = i;
3483
+ }
3484
+ }
3485
+ if (breakIdx === -1) {
3486
+ for (let i = currentMax + 1; i < remaining.length; i++) {
3487
+ if (remaining[i] === "{") inMacro++;
3488
+ else if (remaining[i] === "}") inMacro = Math.max(0, inMacro - 1);
3489
+ else if (remaining[i] === " " && inMacro === 0) {
3490
+ breakIdx = i;
3491
+ break;
3492
+ }
3493
+ }
3494
+ }
3495
+ if (breakIdx === -1) {
3496
+ lines.push(indent + remaining);
3497
+ break;
3498
+ }
3499
+ lines.push(indent + remaining.substring(0, breakIdx));
3500
+ remaining = remaining.substring(breakIdx + 1);
3501
+ }
3502
+ return lines;
3503
+ }
3266
3504
  function normalizePassageHeader(line) {
3267
3505
  const headerMatch = line.match(/^::\s+(.*)/);
3268
3506
  if (!headerMatch) return line;
@@ -3352,13 +3590,28 @@ import { glob as glob2 } from "glob";
3352
3590
  function parseArgs2(args2) {
3353
3591
  const options = {
3354
3592
  check: false,
3593
+ maxLineLength: null,
3355
3594
  patterns: []
3356
3595
  };
3357
- for (const arg of args2) {
3596
+ let i = 0;
3597
+ while (i < args2.length) {
3598
+ const arg = args2[i];
3358
3599
  if (arg === "--check") {
3359
3600
  options.check = true;
3601
+ i++;
3602
+ } else if (arg === "--max-line-length" && i + 1 < args2.length) {
3603
+ const n = parseInt(args2[i + 1], 10);
3604
+ if (!isNaN(n) && n > 0) options.maxLineLength = n;
3605
+ i += 2;
3606
+ } else if (arg.startsWith("--max-line-length=")) {
3607
+ const n = parseInt(arg.slice("--max-line-length=".length), 10);
3608
+ if (!isNaN(n) && n > 0) options.maxLineLength = n;
3609
+ i++;
3360
3610
  } else if (!arg.startsWith("--")) {
3361
3611
  options.patterns.push(arg);
3612
+ i++;
3613
+ } else {
3614
+ i++;
3362
3615
  }
3363
3616
  }
3364
3617
  if (options.patterns.length === 0) {
@@ -3383,11 +3636,15 @@ async function runFormat(args2) {
3383
3636
  console.log("No files found");
3384
3637
  return 0;
3385
3638
  }
3639
+ const formatOpts = {};
3640
+ if (options.maxLineLength !== null) {
3641
+ formatOpts.maxLineLength = options.maxLineLength;
3642
+ }
3386
3643
  const unformatted = [];
3387
3644
  for (const filePath of uniqueFiles) {
3388
3645
  try {
3389
3646
  const text = readFileSync5(filePath, "utf-8");
3390
- const formatted = await formatDocument(text);
3647
+ const formatted = await formatDocument(text, formatOpts);
3391
3648
  if (formatted !== text) {
3392
3649
  if (options.check) {
3393
3650
  const relative = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
@@ -3663,6 +3920,10 @@ function getCompletions(uri, position, triggerChar, workspace) {
3663
3920
  if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
3664
3921
  return getClosingMacroCompletions(text, position, workspace);
3665
3922
  }
3923
+ const transientDotPathMatch = /%([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
3924
+ if (transientDotPathMatch) {
3925
+ return getTransientDotPathCompletions(transientDotPathMatch[1], workspace);
3926
+ }
3666
3927
  const dotPathMatch = /\$([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
3667
3928
  if (dotPathMatch) {
3668
3929
  return getDotPathCompletions(dotPathMatch[1], workspace);
@@ -3676,6 +3937,9 @@ function getCompletions(uri, position, triggerChar, workspace) {
3676
3937
  if (/@[A-Za-z_$]?[\w$]*$/.test(lineText)) {
3677
3938
  return getLocalVariableCompletions(text);
3678
3939
  }
3940
+ if (/%[A-Za-z_$]?[\w$]*$/.test(lineText) && !/%[A-Za-z_$][\w$]*\./.test(lineText)) {
3941
+ return getTransientVariableCompletions(workspace);
3942
+ }
3679
3943
  if (/\[\[[^\]]*$/.test(lineText)) {
3680
3944
  return getPassageNameCompletions(workspace);
3681
3945
  }
@@ -3803,6 +4067,33 @@ function getDotPathCompletions(varName, workspace) {
3803
4067
  detail: `field of $${varName}`
3804
4068
  }));
3805
4069
  }
4070
+ function getTransientVariableCompletions(workspace) {
4071
+ const declared = workspace.variables.getDeclaredTransient();
4072
+ if (declared.size === 0) return [];
4073
+ const items = [];
4074
+ for (const [name, decl] of declared) {
4075
+ items.push({
4076
+ label: `%${name}`,
4077
+ kind: 6,
4078
+ // CompletionItemKind.Variable
4079
+ detail: "transient variable",
4080
+ insertText: name,
4081
+ documentation: decl.fields && decl.fields.length > 0 ? `Fields: ${decl.fields.join(", ")}` : void 0
4082
+ });
4083
+ }
4084
+ return items;
4085
+ }
4086
+ function getTransientDotPathCompletions(varName, workspace) {
4087
+ const declared = workspace.variables.getDeclaredTransient();
4088
+ const decl = declared.get(varName);
4089
+ if (!decl || !decl.fields || decl.fields.length === 0) return [];
4090
+ return decl.fields.map((field) => ({
4091
+ label: field,
4092
+ kind: 5,
4093
+ // CompletionItemKind.Field
4094
+ detail: `field of %${varName}`
4095
+ }));
4096
+ }
3806
4097
  function getPassageNameCompletions(workspace) {
3807
4098
  const passages = workspace.passages.getAllPassages();
3808
4099
  if (passages.length === 0) return [];
@@ -3822,7 +4113,7 @@ var init_completions = __esm({
3822
4113
  id: "completions",
3823
4114
  capabilities: {
3824
4115
  completionProvider: {
3825
- triggerCharacters: ["{", "$", "_", "@", "[", "."]
4116
+ triggerCharacters: ["{", "$", "_", "@", "%", "[", "."]
3826
4117
  }
3827
4118
  },
3828
4119
  initialize(ctx) {
@@ -3946,6 +4237,28 @@ Fields: ${decl.fields.map((f) => `\`${f}\``).join(", ")}` : "";
3946
4237
  }
3947
4238
  }
3948
4239
  }
4240
+ {
4241
+ const re = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
4242
+ let match;
4243
+ while ((match = re.exec(line)) !== null) {
4244
+ const start = match.index;
4245
+ const end = start + match[0].length;
4246
+ if (position.character >= start && position.character <= end) {
4247
+ const baseName = match[1].split(".")[0];
4248
+ const decl = workspace.variables.getDeclaredTransient().get(baseName);
4249
+ const typeInfo = decl?.fields && decl.fields.length > 0 ? `
4250
+
4251
+ Fields: ${decl.fields.map((f) => `\`${f}\``).join(", ")}` : "";
4252
+ return {
4253
+ contents: `**Transient variable** \`%${match[1]}\`${typeInfo}`,
4254
+ range: {
4255
+ start: { line: position.line, character: start },
4256
+ end: { line: position.line, character: end }
4257
+ }
4258
+ };
4259
+ }
4260
+ }
4261
+ }
3949
4262
  return null;
3950
4263
  }
3951
4264
  function getWidgetHover(line, position, workspace) {
@@ -4228,6 +4541,18 @@ function findReferences(uri, position, workspace, includeDeclaration) {
4228
4541
  }
4229
4542
  }
4230
4543
  }
4544
+ {
4545
+ const transRegex = /(?<!\w)%([A-Za-z_$][\w$]*)/g;
4546
+ let match;
4547
+ while ((match = transRegex.exec(line)) !== null) {
4548
+ const start = match.index;
4549
+ const end = start + match[0].length;
4550
+ if (position.character >= start && position.character <= end) {
4551
+ const varName = match[1];
4552
+ return findTransientReferences(varName, workspace, includeDeclaration);
4553
+ }
4554
+ }
4555
+ }
4231
4556
  {
4232
4557
  const widgetRegex = /\{([A-Za-z_$][\w$]*)/g;
4233
4558
  let match;
@@ -4322,6 +4647,20 @@ function findVariableReferences(varName, workspace, includeDeclaration) {
4322
4647
  }
4323
4648
  return locations;
4324
4649
  }
4650
+ function findTransientReferences(varName, workspace, includeDeclaration) {
4651
+ const results = [];
4652
+ if (includeDeclaration) {
4653
+ const decl = workspace.variables.getDeclaredTransient().get(varName);
4654
+ if (decl?.declarationUri && decl.declarationRange) {
4655
+ results.push({ uri: decl.declarationUri, range: decl.declarationRange });
4656
+ }
4657
+ }
4658
+ const usages = workspace.variables.getTransientUsages(varName);
4659
+ for (const u of usages) {
4660
+ results.push({ uri: u.uri, range: u.range });
4661
+ }
4662
+ return results;
4663
+ }
4325
4664
  function findWidgetReferences(widgetName, workspace, includeDeclaration) {
4326
4665
  const locations = [];
4327
4666
  if (includeDeclaration) {
@@ -4414,8 +4753,9 @@ function computeRename(uri, position, newName, workspace) {
4414
4753
  break;
4415
4754
  }
4416
4755
  case "variable": {
4417
- const bareName = newName.startsWith("$") ? newName.slice(1) : newName;
4418
- const refs = findVariableReferences(symbol.name, workspace, true);
4756
+ const bareName = newName.startsWith("$") ? newName.slice(1) : newName.startsWith("%") ? newName.slice(1) : newName;
4757
+ const isTransient = workspace.variables.getDeclaredTransient().has(symbol.name);
4758
+ const refs = isTransient ? findTransientReferences(symbol.name, workspace, true) : findVariableReferences(symbol.name, workspace, true);
4419
4759
  for (const ref of refs) {
4420
4760
  addEdit(ref.uri, ref.range, bareName);
4421
4761
  }
@@ -4494,6 +4834,24 @@ function resolveSymbolAtCursor(uri, position, workspace) {
4494
4834
  }
4495
4835
  }
4496
4836
  }
4837
+ {
4838
+ const transRegex = /(?<!\w)%([A-Za-z_$][\w$]*)/g;
4839
+ let match;
4840
+ while ((match = transRegex.exec(line)) !== null) {
4841
+ const start = match.index;
4842
+ const end = start + match[0].length;
4843
+ if (position.character >= start && position.character <= end) {
4844
+ return {
4845
+ kind: "variable",
4846
+ name: match[1],
4847
+ range: {
4848
+ start: { line: position.line, character: start },
4849
+ end: { line: position.line, character: end }
4850
+ }
4851
+ };
4852
+ }
4853
+ }
4854
+ }
4497
4855
  {
4498
4856
  const widgetDefRegex2 = /\{widget\s+"([^"]+)"/gi;
4499
4857
  let match;
@@ -4691,6 +5049,7 @@ function computeInlayHints(uri, range, workspace) {
4691
5049
  const hints = [];
4692
5050
  addWidgetParamHints(text, range, workspace, hints);
4693
5051
  addVariableTypeHints(uri, text, range, workspace, hints);
5052
+ addTransientTypeHints(uri, text, range, workspace, hints);
4694
5053
  return hints;
4695
5054
  }
4696
5055
  function addWidgetParamHints(text, range, workspace, hints) {
@@ -4747,6 +5106,28 @@ function addVariableTypeHints(uri, text, range, workspace, hints) {
4747
5106
  });
4748
5107
  }
4749
5108
  }
5109
+ function addTransientTypeHints(uri, text, range, workspace, hints) {
5110
+ const storyTransientsPassage = workspace.passages.getStoryTransients();
5111
+ if (!storyTransientsPassage) return;
5112
+ if (storyTransientsPassage.uri !== uri) return;
5113
+ const lines = text.split("\n");
5114
+ const startLine = Math.max(range.start.line, 0);
5115
+ const endLine = Math.min(range.end.line, lines.length - 1);
5116
+ for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
5117
+ const line = lines[lineNum];
5118
+ const declMatch = line.match(/^%([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
5119
+ if (!declMatch) continue;
5120
+ const valueStr = declMatch[2].trim();
5121
+ const inferredType = inferType(valueStr);
5122
+ if (!inferredType) continue;
5123
+ const eqIdx = line.indexOf("=");
5124
+ hints.push({
5125
+ position: { line: lineNum, character: eqIdx },
5126
+ label: `: ${inferredType}`,
5127
+ kind: "type"
5128
+ });
5129
+ }
5130
+ }
4750
5131
  function inferType(value) {
4751
5132
  if (/^\d+(\.\d+)?$/.test(value)) return "number";
4752
5133
  if (/^"[^"]*"$/.test(value) || /^'[^']*'$/.test(value)) return "string";
@@ -4851,6 +5232,7 @@ function computeSemanticTokensAbsolute(uri, workspace) {
4851
5232
  const storyVarRegex = /(?<!\w)\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
4852
5233
  const tempVarRegex = /(?<!\w)_([A-Za-z_$][\w$]*)/g;
4853
5234
  const localVarRegex = /(?<!\w)@([A-Za-z_$][\w$]*)/g;
5235
+ const transientVarRegex = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
4854
5236
  const sugarKeywordRegex = /\b(to|is|isnot|eq|neq|gt|gte|lt|lte|and|or|not|def|ndef)\b/g;
4855
5237
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
4856
5238
  if (headerLines.has(lineIndex)) continue;
@@ -4886,6 +5268,16 @@ function computeSemanticTokensAbsolute(uri, workspace) {
4886
5268
  tokenModifiers: encodeModifiers(["readonly"])
4887
5269
  });
4888
5270
  }
5271
+ transientVarRegex.lastIndex = 0;
5272
+ while ((m = transientVarRegex.exec(line)) !== null) {
5273
+ tokens.push({
5274
+ line: lineIndex,
5275
+ startChar: m.index,
5276
+ length: m[0].length,
5277
+ tokenType: encodeType("variable"),
5278
+ tokenModifiers: encodeModifiers(["defaultLibrary"])
5279
+ });
5280
+ }
4889
5281
  sugarKeywordRegex.lastIndex = 0;
4890
5282
  while ((m = sugarKeywordRegex.exec(line)) !== null) {
4891
5283
  tokens.push({
@@ -4990,6 +5382,11 @@ function computeCodeActions(uri, diagnostics, workspace) {
4990
5382
  if (action) actions.push(action);
4991
5383
  break;
4992
5384
  }
5385
+ case DiagnosticCode.UndeclaredTransient: {
5386
+ const action = fixUndeclaredTransient(diag, workspace);
5387
+ if (action) actions.push(action);
5388
+ break;
5389
+ }
4993
5390
  }
4994
5391
  }
4995
5392
  return actions;
@@ -5072,6 +5469,40 @@ function fixNoStoryVariables(uri, workspace) {
5072
5469
  }]
5073
5470
  };
5074
5471
  }
5472
+ function fixUndeclaredTransient(diag, workspace) {
5473
+ const match = diag.message.match(/'%(\w+)'/);
5474
+ if (!match) return null;
5475
+ const varName = match[1];
5476
+ const storyTransients = workspace.passages.getStoryTransients();
5477
+ if (!storyTransients) return null;
5478
+ const storyTransientsUri = storyTransients.uri;
5479
+ const text = workspace.documents.getText(storyTransientsUri);
5480
+ if (text === void 0) return null;
5481
+ const lines = text.split("\n");
5482
+ const contentStart = storyTransients.headerEnd.end.line + 1;
5483
+ let contentEnd = lines.length;
5484
+ for (let i = contentStart; i < lines.length; i++) {
5485
+ if (/^::\s+/.test(lines[i])) {
5486
+ contentEnd = i;
5487
+ break;
5488
+ }
5489
+ }
5490
+ const insertLine = contentEnd;
5491
+ return {
5492
+ title: `Declare '%${varName}' in StoryTransients`,
5493
+ kind: "quickfix",
5494
+ diagnosticCodes: [DiagnosticCode.UndeclaredTransient],
5495
+ edits: [{
5496
+ uri: storyTransientsUri,
5497
+ range: {
5498
+ start: { line: insertLine, character: 0 },
5499
+ end: { line: insertLine, character: 0 }
5500
+ },
5501
+ newText: `%${varName} = null
5502
+ `
5503
+ }]
5504
+ };
5505
+ }
5075
5506
  function toLspRange6(r) {
5076
5507
  return {
5077
5508
  start: { line: r.start.line, character: r.start.character },