@rohal12/spindle-lsp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -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) {
@@ -3216,7 +3343,10 @@ async function formatDocument(text, options) {
3216
3343
  }
3217
3344
  continue;
3218
3345
  }
3219
- const indented = indentMacros(region.lines, isBlock, isDedenting);
3346
+ let indented = indentMacros(region.lines, isBlock, isDedenting);
3347
+ if (options?.maxLineLength) {
3348
+ indented = wrapLines(indented, options.maxLineLength);
3349
+ }
3220
3350
  resultLines.push(...indented);
3221
3351
  }
3222
3352
  }
@@ -3263,6 +3393,65 @@ function indentMacros(lines, isBlock, isDedenting) {
3263
3393
  async function formatRange(text, _range, options) {
3264
3394
  return formatDocument(text, options);
3265
3395
  }
3396
+ function wrapLines(lines, maxLen) {
3397
+ const result = [];
3398
+ for (const line of lines) {
3399
+ if (line.length <= maxLen) {
3400
+ result.push(line);
3401
+ continue;
3402
+ }
3403
+ const trimmed = line.trim();
3404
+ if (trimmed === "" || /^::/.test(trimmed) || /^\{[\/#.]/.test(trimmed) || // closing/opening-only macro lines
3405
+ /^\[\[/.test(trimmed) || // links
3406
+ /^\{choice\b/.test(trimmed) || // choice macros
3407
+ /^\{choices\b/.test(trimmed)) {
3408
+ result.push(line);
3409
+ continue;
3410
+ }
3411
+ const indentMatch = line.match(/^(\s*)/);
3412
+ const indent = indentMatch ? indentMatch[1] : "";
3413
+ const wrapped = wordWrap(trimmed, maxLen, indent);
3414
+ result.push(...wrapped);
3415
+ }
3416
+ return result;
3417
+ }
3418
+ function wordWrap(text, maxLen, indent) {
3419
+ const lines = [];
3420
+ let remaining = text;
3421
+ while (remaining.length > 0) {
3422
+ const currentMax = lines.length === 0 ? maxLen - indent.length : maxLen - indent.length;
3423
+ if (remaining.length <= currentMax) {
3424
+ lines.push(indent + remaining);
3425
+ break;
3426
+ }
3427
+ let breakIdx = -1;
3428
+ let inMacro = 0;
3429
+ for (let i = 0; i < remaining.length && i <= currentMax; i++) {
3430
+ if (remaining[i] === "{") inMacro++;
3431
+ else if (remaining[i] === "}") inMacro = Math.max(0, inMacro - 1);
3432
+ else if (remaining[i] === " " && inMacro === 0 && i > 0) {
3433
+ breakIdx = i;
3434
+ }
3435
+ }
3436
+ if (breakIdx === -1) {
3437
+ for (let i = currentMax + 1; i < remaining.length; i++) {
3438
+ if (remaining[i] === "{") inMacro++;
3439
+ else if (remaining[i] === "}") inMacro = Math.max(0, inMacro - 1);
3440
+ else if (remaining[i] === " " && inMacro === 0) {
3441
+ breakIdx = i;
3442
+ break;
3443
+ }
3444
+ }
3445
+ }
3446
+ if (breakIdx === -1) {
3447
+ lines.push(indent + remaining);
3448
+ break;
3449
+ }
3450
+ lines.push(indent + remaining.substring(0, breakIdx));
3451
+ remaining = remaining.substring(breakIdx + 1);
3452
+ }
3453
+ return lines;
3454
+ }
3266
3455
  function normalizePassageHeader(line) {
3267
3456
  const headerMatch = line.match(/^::\s+(.*)/);
3268
3457
  if (!headerMatch) return line;
@@ -3352,13 +3541,28 @@ import { glob as glob2 } from "glob";
3352
3541
  function parseArgs2(args2) {
3353
3542
  const options = {
3354
3543
  check: false,
3544
+ maxLineLength: null,
3355
3545
  patterns: []
3356
3546
  };
3357
- for (const arg of args2) {
3547
+ let i = 0;
3548
+ while (i < args2.length) {
3549
+ const arg = args2[i];
3358
3550
  if (arg === "--check") {
3359
3551
  options.check = true;
3552
+ i++;
3553
+ } else if (arg === "--max-line-length" && i + 1 < args2.length) {
3554
+ const n = parseInt(args2[i + 1], 10);
3555
+ if (!isNaN(n) && n > 0) options.maxLineLength = n;
3556
+ i += 2;
3557
+ } else if (arg.startsWith("--max-line-length=")) {
3558
+ const n = parseInt(arg.slice("--max-line-length=".length), 10);
3559
+ if (!isNaN(n) && n > 0) options.maxLineLength = n;
3560
+ i++;
3360
3561
  } else if (!arg.startsWith("--")) {
3361
3562
  options.patterns.push(arg);
3563
+ i++;
3564
+ } else {
3565
+ i++;
3362
3566
  }
3363
3567
  }
3364
3568
  if (options.patterns.length === 0) {
@@ -3383,11 +3587,15 @@ async function runFormat(args2) {
3383
3587
  console.log("No files found");
3384
3588
  return 0;
3385
3589
  }
3590
+ const formatOpts = {};
3591
+ if (options.maxLineLength !== null) {
3592
+ formatOpts.maxLineLength = options.maxLineLength;
3593
+ }
3386
3594
  const unformatted = [];
3387
3595
  for (const filePath of uniqueFiles) {
3388
3596
  try {
3389
3597
  const text = readFileSync5(filePath, "utf-8");
3390
- const formatted = await formatDocument(text);
3598
+ const formatted = await formatDocument(text, formatOpts);
3391
3599
  if (formatted !== text) {
3392
3600
  if (options.check) {
3393
3601
  const relative = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
@@ -3663,6 +3871,10 @@ function getCompletions(uri, position, triggerChar, workspace) {
3663
3871
  if (/\{\/[A-Za-z\w-]*$/.test(lineText)) {
3664
3872
  return getClosingMacroCompletions(text, position, workspace);
3665
3873
  }
3874
+ const transientDotPathMatch = /%([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
3875
+ if (transientDotPathMatch) {
3876
+ return getTransientDotPathCompletions(transientDotPathMatch[1], workspace);
3877
+ }
3666
3878
  const dotPathMatch = /\$([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)?$/.exec(lineText);
3667
3879
  if (dotPathMatch) {
3668
3880
  return getDotPathCompletions(dotPathMatch[1], workspace);
@@ -3676,6 +3888,9 @@ function getCompletions(uri, position, triggerChar, workspace) {
3676
3888
  if (/@[A-Za-z_$]?[\w$]*$/.test(lineText)) {
3677
3889
  return getLocalVariableCompletions(text);
3678
3890
  }
3891
+ if (/%[A-Za-z_$]?[\w$]*$/.test(lineText) && !/%[A-Za-z_$][\w$]*\./.test(lineText)) {
3892
+ return getTransientVariableCompletions(workspace);
3893
+ }
3679
3894
  if (/\[\[[^\]]*$/.test(lineText)) {
3680
3895
  return getPassageNameCompletions(workspace);
3681
3896
  }
@@ -3803,6 +4018,33 @@ function getDotPathCompletions(varName, workspace) {
3803
4018
  detail: `field of $${varName}`
3804
4019
  }));
3805
4020
  }
4021
+ function getTransientVariableCompletions(workspace) {
4022
+ const declared = workspace.variables.getDeclaredTransient();
4023
+ if (declared.size === 0) return [];
4024
+ const items = [];
4025
+ for (const [name, decl] of declared) {
4026
+ items.push({
4027
+ label: `%${name}`,
4028
+ kind: 6,
4029
+ // CompletionItemKind.Variable
4030
+ detail: "transient variable",
4031
+ insertText: name,
4032
+ documentation: decl.fields && decl.fields.length > 0 ? `Fields: ${decl.fields.join(", ")}` : void 0
4033
+ });
4034
+ }
4035
+ return items;
4036
+ }
4037
+ function getTransientDotPathCompletions(varName, workspace) {
4038
+ const declared = workspace.variables.getDeclaredTransient();
4039
+ const decl = declared.get(varName);
4040
+ if (!decl || !decl.fields || decl.fields.length === 0) return [];
4041
+ return decl.fields.map((field) => ({
4042
+ label: field,
4043
+ kind: 5,
4044
+ // CompletionItemKind.Field
4045
+ detail: `field of %${varName}`
4046
+ }));
4047
+ }
3806
4048
  function getPassageNameCompletions(workspace) {
3807
4049
  const passages = workspace.passages.getAllPassages();
3808
4050
  if (passages.length === 0) return [];
@@ -3822,7 +4064,7 @@ var init_completions = __esm({
3822
4064
  id: "completions",
3823
4065
  capabilities: {
3824
4066
  completionProvider: {
3825
- triggerCharacters: ["{", "$", "_", "@", "[", "."]
4067
+ triggerCharacters: ["{", "$", "_", "@", "%", "[", "."]
3826
4068
  }
3827
4069
  },
3828
4070
  initialize(ctx) {
@@ -3946,6 +4188,28 @@ Fields: ${decl.fields.map((f) => `\`${f}\``).join(", ")}` : "";
3946
4188
  }
3947
4189
  }
3948
4190
  }
4191
+ {
4192
+ const re = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
4193
+ let match;
4194
+ while ((match = re.exec(line)) !== null) {
4195
+ const start = match.index;
4196
+ const end = start + match[0].length;
4197
+ if (position.character >= start && position.character <= end) {
4198
+ const baseName = match[1].split(".")[0];
4199
+ const decl = workspace.variables.getDeclaredTransient().get(baseName);
4200
+ const typeInfo = decl?.fields && decl.fields.length > 0 ? `
4201
+
4202
+ Fields: ${decl.fields.map((f) => `\`${f}\``).join(", ")}` : "";
4203
+ return {
4204
+ contents: `**Transient variable** \`%${match[1]}\`${typeInfo}`,
4205
+ range: {
4206
+ start: { line: position.line, character: start },
4207
+ end: { line: position.line, character: end }
4208
+ }
4209
+ };
4210
+ }
4211
+ }
4212
+ }
3949
4213
  return null;
3950
4214
  }
3951
4215
  function getWidgetHover(line, position, workspace) {
@@ -4228,6 +4492,18 @@ function findReferences(uri, position, workspace, includeDeclaration) {
4228
4492
  }
4229
4493
  }
4230
4494
  }
4495
+ {
4496
+ const transRegex = /(?<!\w)%([A-Za-z_$][\w$]*)/g;
4497
+ let match;
4498
+ while ((match = transRegex.exec(line)) !== null) {
4499
+ const start = match.index;
4500
+ const end = start + match[0].length;
4501
+ if (position.character >= start && position.character <= end) {
4502
+ const varName = match[1];
4503
+ return findTransientReferences(varName, workspace, includeDeclaration);
4504
+ }
4505
+ }
4506
+ }
4231
4507
  {
4232
4508
  const widgetRegex = /\{([A-Za-z_$][\w$]*)/g;
4233
4509
  let match;
@@ -4322,6 +4598,20 @@ function findVariableReferences(varName, workspace, includeDeclaration) {
4322
4598
  }
4323
4599
  return locations;
4324
4600
  }
4601
+ function findTransientReferences(varName, workspace, includeDeclaration) {
4602
+ const results = [];
4603
+ if (includeDeclaration) {
4604
+ const decl = workspace.variables.getDeclaredTransient().get(varName);
4605
+ if (decl?.declarationUri && decl.declarationRange) {
4606
+ results.push({ uri: decl.declarationUri, range: decl.declarationRange });
4607
+ }
4608
+ }
4609
+ const usages = workspace.variables.getTransientUsages(varName);
4610
+ for (const u of usages) {
4611
+ results.push({ uri: u.uri, range: u.range });
4612
+ }
4613
+ return results;
4614
+ }
4325
4615
  function findWidgetReferences(widgetName, workspace, includeDeclaration) {
4326
4616
  const locations = [];
4327
4617
  if (includeDeclaration) {
@@ -4414,8 +4704,9 @@ function computeRename(uri, position, newName, workspace) {
4414
4704
  break;
4415
4705
  }
4416
4706
  case "variable": {
4417
- const bareName = newName.startsWith("$") ? newName.slice(1) : newName;
4418
- const refs = findVariableReferences(symbol.name, workspace, true);
4707
+ const bareName = newName.startsWith("$") ? newName.slice(1) : newName.startsWith("%") ? newName.slice(1) : newName;
4708
+ const isTransient = workspace.variables.getDeclaredTransient().has(symbol.name);
4709
+ const refs = isTransient ? findTransientReferences(symbol.name, workspace, true) : findVariableReferences(symbol.name, workspace, true);
4419
4710
  for (const ref of refs) {
4420
4711
  addEdit(ref.uri, ref.range, bareName);
4421
4712
  }
@@ -4494,6 +4785,24 @@ function resolveSymbolAtCursor(uri, position, workspace) {
4494
4785
  }
4495
4786
  }
4496
4787
  }
4788
+ {
4789
+ const transRegex = /(?<!\w)%([A-Za-z_$][\w$]*)/g;
4790
+ let match;
4791
+ while ((match = transRegex.exec(line)) !== null) {
4792
+ const start = match.index;
4793
+ const end = start + match[0].length;
4794
+ if (position.character >= start && position.character <= end) {
4795
+ return {
4796
+ kind: "variable",
4797
+ name: match[1],
4798
+ range: {
4799
+ start: { line: position.line, character: start },
4800
+ end: { line: position.line, character: end }
4801
+ }
4802
+ };
4803
+ }
4804
+ }
4805
+ }
4497
4806
  {
4498
4807
  const widgetDefRegex2 = /\{widget\s+"([^"]+)"/gi;
4499
4808
  let match;
@@ -4691,6 +5000,7 @@ function computeInlayHints(uri, range, workspace) {
4691
5000
  const hints = [];
4692
5001
  addWidgetParamHints(text, range, workspace, hints);
4693
5002
  addVariableTypeHints(uri, text, range, workspace, hints);
5003
+ addTransientTypeHints(uri, text, range, workspace, hints);
4694
5004
  return hints;
4695
5005
  }
4696
5006
  function addWidgetParamHints(text, range, workspace, hints) {
@@ -4747,6 +5057,28 @@ function addVariableTypeHints(uri, text, range, workspace, hints) {
4747
5057
  });
4748
5058
  }
4749
5059
  }
5060
+ function addTransientTypeHints(uri, text, range, workspace, hints) {
5061
+ const storyTransientsPassage = workspace.passages.getStoryTransients();
5062
+ if (!storyTransientsPassage) return;
5063
+ if (storyTransientsPassage.uri !== uri) return;
5064
+ const lines = text.split("\n");
5065
+ const startLine = Math.max(range.start.line, 0);
5066
+ const endLine = Math.min(range.end.line, lines.length - 1);
5067
+ for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
5068
+ const line = lines[lineNum];
5069
+ const declMatch = line.match(/^%([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
5070
+ if (!declMatch) continue;
5071
+ const valueStr = declMatch[2].trim();
5072
+ const inferredType = inferType(valueStr);
5073
+ if (!inferredType) continue;
5074
+ const eqIdx = line.indexOf("=");
5075
+ hints.push({
5076
+ position: { line: lineNum, character: eqIdx },
5077
+ label: `: ${inferredType}`,
5078
+ kind: "type"
5079
+ });
5080
+ }
5081
+ }
4750
5082
  function inferType(value) {
4751
5083
  if (/^\d+(\.\d+)?$/.test(value)) return "number";
4752
5084
  if (/^"[^"]*"$/.test(value) || /^'[^']*'$/.test(value)) return "string";
@@ -4851,6 +5183,7 @@ function computeSemanticTokensAbsolute(uri, workspace) {
4851
5183
  const storyVarRegex = /(?<!\w)\$([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
4852
5184
  const tempVarRegex = /(?<!\w)_([A-Za-z_$][\w$]*)/g;
4853
5185
  const localVarRegex = /(?<!\w)@([A-Za-z_$][\w$]*)/g;
5186
+ const transientVarRegex = /(?<!\w)%([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
4854
5187
  const sugarKeywordRegex = /\b(to|is|isnot|eq|neq|gt|gte|lt|lte|and|or|not|def|ndef)\b/g;
4855
5188
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
4856
5189
  if (headerLines.has(lineIndex)) continue;
@@ -4886,6 +5219,16 @@ function computeSemanticTokensAbsolute(uri, workspace) {
4886
5219
  tokenModifiers: encodeModifiers(["readonly"])
4887
5220
  });
4888
5221
  }
5222
+ transientVarRegex.lastIndex = 0;
5223
+ while ((m = transientVarRegex.exec(line)) !== null) {
5224
+ tokens.push({
5225
+ line: lineIndex,
5226
+ startChar: m.index,
5227
+ length: m[0].length,
5228
+ tokenType: encodeType("variable"),
5229
+ tokenModifiers: encodeModifiers(["defaultLibrary"])
5230
+ });
5231
+ }
4889
5232
  sugarKeywordRegex.lastIndex = 0;
4890
5233
  while ((m = sugarKeywordRegex.exec(line)) !== null) {
4891
5234
  tokens.push({
@@ -4990,6 +5333,11 @@ function computeCodeActions(uri, diagnostics, workspace) {
4990
5333
  if (action) actions.push(action);
4991
5334
  break;
4992
5335
  }
5336
+ case DiagnosticCode.UndeclaredTransient: {
5337
+ const action = fixUndeclaredTransient(diag, workspace);
5338
+ if (action) actions.push(action);
5339
+ break;
5340
+ }
4993
5341
  }
4994
5342
  }
4995
5343
  return actions;
@@ -5072,6 +5420,40 @@ function fixNoStoryVariables(uri, workspace) {
5072
5420
  }]
5073
5421
  };
5074
5422
  }
5423
+ function fixUndeclaredTransient(diag, workspace) {
5424
+ const match = diag.message.match(/'%(\w+)'/);
5425
+ if (!match) return null;
5426
+ const varName = match[1];
5427
+ const storyTransients = workspace.passages.getStoryTransients();
5428
+ if (!storyTransients) return null;
5429
+ const storyTransientsUri = storyTransients.uri;
5430
+ const text = workspace.documents.getText(storyTransientsUri);
5431
+ if (text === void 0) return null;
5432
+ const lines = text.split("\n");
5433
+ const contentStart = storyTransients.headerEnd.end.line + 1;
5434
+ let contentEnd = lines.length;
5435
+ for (let i = contentStart; i < lines.length; i++) {
5436
+ if (/^::\s+/.test(lines[i])) {
5437
+ contentEnd = i;
5438
+ break;
5439
+ }
5440
+ }
5441
+ const insertLine = contentEnd;
5442
+ return {
5443
+ title: `Declare '%${varName}' in StoryTransients`,
5444
+ kind: "quickfix",
5445
+ diagnosticCodes: [DiagnosticCode.UndeclaredTransient],
5446
+ edits: [{
5447
+ uri: storyTransientsUri,
5448
+ range: {
5449
+ start: { line: insertLine, character: 0 },
5450
+ end: { line: insertLine, character: 0 }
5451
+ },
5452
+ newText: `%${varName} = null
5453
+ `
5454
+ }]
5455
+ };
5456
+ }
5075
5457
  function toLspRange6(r) {
5076
5458
  return {
5077
5459
  start: { line: r.start.line, character: r.start.character },