@rohal12/spindle-lsp 0.7.0 → 0.8.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
@@ -448,8 +448,10 @@ var init_variable_tracker = __esm({
448
448
  VariableTracker = class {
449
449
  declared = /* @__PURE__ */ new Map();
450
450
  _hasStoryVariables = false;
451
+ _nullDeclarations = [];
451
452
  declaredTransient = /* @__PURE__ */ new Map();
452
453
  _hasStoryTransients = false;
454
+ _nullTransientDeclarations = [];
453
455
  /** Per-URI list of variable usages. */
454
456
  usagesByUri = /* @__PURE__ */ new Map();
455
457
  /** Per-URI list of transient variable usages. */
@@ -458,17 +460,32 @@ var init_variable_tracker = __esm({
458
460
  * Parse the StoryVariables passage content for declarations.
459
461
  * Each line like `$name = value` becomes a declaration.
460
462
  */
461
- parseStoryVariables(content) {
463
+ parseStoryVariables(content, contentStartLine = 0) {
462
464
  this.declared.clear();
463
465
  this._hasStoryVariables = true;
466
+ this._nullDeclarations = [];
464
467
  const lines = content.split("\n");
465
- for (const line of lines) {
466
- const trimmed = line.trim();
468
+ for (let i = 0; i < lines.length; i++) {
469
+ const trimmed = lines[i].trim();
467
470
  if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("<!--")) continue;
468
471
  const match = /^\$([A-Za-z_$][\w$]*)\s*=\s*(.*)$/.exec(trimmed);
469
472
  if (!match) continue;
470
473
  const name = match[1];
471
474
  const expr = match[2].trim();
475
+ if (expr === "null") {
476
+ const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
477
+ const absLine = contentStartLine + i;
478
+ this._nullDeclarations.push({
479
+ name,
480
+ sigil: "$",
481
+ range: {
482
+ start: { line: absLine, character: charIdx },
483
+ end: { line: absLine, character: charIdx + 4 }
484
+ }
485
+ });
486
+ this.declared.set(name, { name, sigil: "$" });
487
+ continue;
488
+ }
472
489
  const decl = { name, sigil: "$" };
473
490
  if (expr.startsWith("{")) {
474
491
  const fieldRegex = /(\w+)\s*:/g;
@@ -488,17 +505,32 @@ var init_variable_tracker = __esm({
488
505
  * Parse the StoryTransients passage content for declarations.
489
506
  * Each line like `%name = value` becomes a declaration.
490
507
  */
491
- parseStoryTransients(content) {
508
+ parseStoryTransients(content, contentStartLine = 0) {
492
509
  this.declaredTransient.clear();
493
510
  this._hasStoryTransients = true;
511
+ this._nullTransientDeclarations = [];
494
512
  const lines = content.split("\n");
495
- for (const line of lines) {
496
- const trimmed = line.trim();
513
+ for (let i = 0; i < lines.length; i++) {
514
+ const trimmed = lines[i].trim();
497
515
  if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("<!--")) continue;
498
516
  const match = /^%([A-Za-z_$][\w$]*)\s*=\s*(.*)$/.exec(trimmed);
499
517
  if (!match) continue;
500
518
  const name = match[1];
501
519
  const expr = match[2].trim();
520
+ if (expr === "null") {
521
+ const charIdx = lines[i].indexOf("null", lines[i].indexOf("="));
522
+ const absLine = contentStartLine + i;
523
+ this._nullTransientDeclarations.push({
524
+ name,
525
+ sigil: "%",
526
+ range: {
527
+ start: { line: absLine, character: charIdx },
528
+ end: { line: absLine, character: charIdx + 4 }
529
+ }
530
+ });
531
+ this.declaredTransient.set(name, { name, sigil: "%" });
532
+ continue;
533
+ }
502
534
  const decl = { name, sigil: "%" };
503
535
  if (expr.startsWith("{")) {
504
536
  const fieldRegex = /(\w+)\s*:/g;
@@ -660,6 +692,14 @@ var init_variable_tracker = __esm({
660
692
  hasStoryTransients() {
661
693
  return this._hasStoryTransients;
662
694
  }
695
+ /** Get variables declared with null values in StoryVariables. */
696
+ getNullDeclarations() {
697
+ return this._nullDeclarations;
698
+ }
699
+ /** Get transient variables declared with null values in StoryTransients. */
700
+ getNullTransientDeclarations() {
701
+ return this._nullTransientDeclarations;
702
+ }
663
703
  };
664
704
  }
665
705
  });
@@ -1238,7 +1278,7 @@ var init_workspace_model = __esm({
1238
1278
  }
1239
1279
  }
1240
1280
  const content = lines.slice(contentStart, contentEnd).join("\n");
1241
- this.variables.parseStoryVariables(content);
1281
+ this.variables.parseStoryVariables(content, contentStart);
1242
1282
  }
1243
1283
  }
1244
1284
  const storyTransients = this.passages.getStoryTransients();
@@ -1255,7 +1295,7 @@ var init_workspace_model = __esm({
1255
1295
  }
1256
1296
  }
1257
1297
  const content = lines.slice(contentStart, contentEnd).join("\n");
1258
- this.variables.parseStoryTransients(content);
1298
+ this.variables.parseStoryTransients(content, contentStart);
1259
1299
  }
1260
1300
  }
1261
1301
  for (const uri of this.documents.getUris()) {
@@ -1306,6 +1346,7 @@ var init_diagnostic_codes = __esm({
1306
1346
  UndeclaredField: "SP201",
1307
1347
  NoStoryVariables: "SP202",
1308
1348
  UndeclaredTransient: "SP203",
1349
+ NullVariableValue: "SP204",
1309
1350
  BrokenPassageLink: "SP300",
1310
1351
  WidgetArgCountMismatch: "SP301",
1311
1352
  DeadEndPassage: "SP400",
@@ -1324,10 +1365,11 @@ var init_diagnostic_codes = __esm({
1324
1365
  SP112: "warning",
1325
1366
  SP114: "error",
1326
1367
  SP115: "error",
1327
- SP200: "warning",
1368
+ SP200: "error",
1328
1369
  SP201: "warning",
1329
1370
  SP202: "info",
1330
1371
  SP203: "warning",
1372
+ SP204: "error",
1331
1373
  SP300: "warning",
1332
1374
  SP301: "warning",
1333
1375
  SP400: "hint",
@@ -2434,6 +2476,16 @@ function validateVariables(uri, workspace, diagnostics) {
2434
2476
  `Variable '$${u.name}' is not declared in StoryVariables`
2435
2477
  ));
2436
2478
  }
2479
+ const storyVars = workspace.passages.getStoryVariables();
2480
+ if (storyVars && storyVars.uri === uri) {
2481
+ for (const nd of workspace.variables.getNullDeclarations()) {
2482
+ diagnostics.push(makeDiag(
2483
+ nd.range,
2484
+ DiagnosticCode.NullVariableValue,
2485
+ `Variable '$${nd.name}' is set to null. Spindle does not support null \u2014 use a valid default (number, string, boolean, array, or object).`
2486
+ ));
2487
+ }
2488
+ }
2437
2489
  }
2438
2490
  if (workspace.variables.hasStoryTransients()) {
2439
2491
  const undeclaredTransient = workspace.variables.getUndeclaredTransient(uri);
@@ -2444,6 +2496,16 @@ function validateVariables(uri, workspace, diagnostics) {
2444
2496
  `Transient variable '%${u.name}' is not declared in StoryTransients`
2445
2497
  ));
2446
2498
  }
2499
+ const storyTransients = workspace.passages.getStoryTransients();
2500
+ if (storyTransients && storyTransients.uri === uri) {
2501
+ for (const nd of workspace.variables.getNullTransientDeclarations()) {
2502
+ diagnostics.push(makeDiag(
2503
+ nd.range,
2504
+ DiagnosticCode.NullVariableValue,
2505
+ `Transient variable '%${nd.name}' is set to null. Spindle does not support null \u2014 use a valid default (number, string, boolean, array, or object).`
2506
+ ));
2507
+ }
2508
+ }
2447
2509
  }
2448
2510
  }
2449
2511
  function validateLinks(text, passages, passageNames, diagnostics) {
@@ -3172,6 +3234,50 @@ var init_prettier_bridge = __esm({
3172
3234
  });
3173
3235
 
3174
3236
  // src/plugins/format/placeholders.ts
3237
+ function scanSpindleTokens(text) {
3238
+ const matches = [];
3239
+ let i = 0;
3240
+ while (i < text.length) {
3241
+ if (text[i] === "[" && text[i + 1] === "[") {
3242
+ const closeIdx = text.indexOf("]]", i + 2);
3243
+ if (closeIdx !== -1) {
3244
+ const end = closeIdx + 2;
3245
+ matches.push({ start: i, end, token: text.slice(i, end) });
3246
+ i = end;
3247
+ continue;
3248
+ }
3249
+ }
3250
+ if (text[i] === "{" && !(i > 0 && text[i - 1] === "\\")) {
3251
+ const next = i + 1 < text.length ? text[i + 1] : "";
3252
+ let isToken = false;
3253
+ if (next === "/") {
3254
+ isToken = i + 2 < text.length && /[A-Za-z]/.test(text[i + 2]);
3255
+ } else if (next === "#" || next === ".") {
3256
+ isToken = i + 2 < text.length && /[a-zA-Z]/.test(text[i + 2]);
3257
+ } else if (/[A-Za-z]/.test(next)) {
3258
+ isToken = true;
3259
+ } else if (/[$_@%]/.test(next)) {
3260
+ isToken = i + 2 < text.length && /[a-zA-Z]/.test(text[i + 2]);
3261
+ }
3262
+ if (isToken) {
3263
+ let depth = 1;
3264
+ let j = i + 1;
3265
+ while (j < text.length && depth > 0) {
3266
+ if (text[j] === "{") depth++;
3267
+ else if (text[j] === "}") depth--;
3268
+ j++;
3269
+ }
3270
+ if (depth === 0) {
3271
+ matches.push({ start: i, end: j, token: text.slice(i, j) });
3272
+ i = j;
3273
+ continue;
3274
+ }
3275
+ }
3276
+ }
3277
+ i++;
3278
+ }
3279
+ return matches;
3280
+ }
3175
3281
  function isInsideAttribute(text, pos) {
3176
3282
  let inQuote = false;
3177
3283
  let quoteChar = "";
@@ -3208,13 +3314,15 @@ function replaceSpindleTokens(html) {
3208
3314
  const resultLines = [];
3209
3315
  for (const line of lines) {
3210
3316
  const trimmed = line.trim();
3211
- SPINDLE_TOKEN_REGEX.lastIndex = 0;
3212
- if (!SPINDLE_TOKEN_REGEX.test(trimmed)) {
3317
+ const found = scanSpindleTokens(trimmed);
3318
+ if (found.length === 0) {
3213
3319
  resultLines.push(line);
3214
3320
  continue;
3215
3321
  }
3216
- SPINDLE_TOKEN_REGEX.lastIndex = 0;
3217
- const withoutSpindle = trimmed.replace(SPINDLE_TOKEN_REGEX, "");
3322
+ let withoutSpindle = trimmed;
3323
+ for (let fi = found.length - 1; fi >= 0; fi--) {
3324
+ withoutSpindle = withoutSpindle.slice(0, found[fi].start) + withoutSpindle.slice(found[fi].end);
3325
+ }
3218
3326
  const hasHtml = HTML_TAG_REGEX.test(withoutSpindle);
3219
3327
  if (!hasHtml) {
3220
3328
  const idx = tokens.length;
@@ -3222,16 +3330,20 @@ function replaceSpindleTokens(html) {
3222
3330
  const indent = line.match(/^(\s*)/)?.[1] ?? "";
3223
3331
  resultLines.push(`${indent}<!--SP:${idx}-->`);
3224
3332
  } else {
3225
- SPINDLE_TOKEN_REGEX.lastIndex = 0;
3226
- const replaced = line.replace(SPINDLE_TOKEN_REGEX, (match, ...args2) => {
3227
- const matchOffset = args2[args2.length - 2];
3333
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
3334
+ const indentLen = indent.length;
3335
+ const assignments = found.map((t) => {
3336
+ const lineStart = t.start + indentLen;
3228
3337
  const idx = tokens.length;
3229
- tokens.push(match);
3230
- if (isInsideAttribute(line, matchOffset)) {
3231
- return `__SP${idx}__`;
3232
- }
3233
- return `<!--SP:${idx}-->`;
3338
+ tokens.push(t.token);
3339
+ const placeholder = isInsideAttribute(line, lineStart) ? `__SP${idx}__` : `<!--SP:${idx}-->`;
3340
+ return { start: lineStart, end: t.end + indentLen, placeholder };
3234
3341
  });
3342
+ let replaced = line;
3343
+ for (let ai = assignments.length - 1; ai >= 0; ai--) {
3344
+ const a = assignments[ai];
3345
+ replaced = replaced.slice(0, a.start) + a.placeholder + replaced.slice(a.end);
3346
+ }
3235
3347
  resultLines.push(replaced);
3236
3348
  }
3237
3349
  }
@@ -3245,11 +3357,10 @@ function restoreSpindleTokens(text, tokens) {
3245
3357
  }
3246
3358
  return result;
3247
3359
  }
3248
- var SPINDLE_TOKEN_REGEX, HTML_TAG_REGEX;
3360
+ var HTML_TAG_REGEX;
3249
3361
  var init_placeholders = __esm({
3250
3362
  "src/plugins/format/placeholders.ts"() {
3251
3363
  "use strict";
3252
- SPINDLE_TOKEN_REGEX = /(?:\{\/[A-Za-z][\w-]*\s*\}|\{(?:[#.][a-zA-Z][\w-]*\s*)*[A-Za-z][\w-]*(?:\s+(?:[^{}]|\{[^{}]*\})*)?\}|\{[$_@][a-zA-Z][\w.]*\}|\[\[(?:[^\]]*)\]\])/g;
3253
3364
  HTML_TAG_REGEX = /<\/?[a-zA-Z][\w-]*[\s>/]/;
3254
3365
  }
3255
3366
  });
@@ -5338,6 +5449,11 @@ function computeCodeActions(uri, diagnostics, workspace) {
5338
5449
  if (action) actions.push(action);
5339
5450
  break;
5340
5451
  }
5452
+ case DiagnosticCode.NullVariableValue: {
5453
+ const action = fixNullVariableValue(uri, diag);
5454
+ if (action) actions.push(action);
5455
+ break;
5456
+ }
5341
5457
  }
5342
5458
  }
5343
5459
  return actions;
@@ -5394,7 +5510,7 @@ function fixUndeclaredVariable(diag, workspace) {
5394
5510
  start: { line: insertLine, character: 0 },
5395
5511
  end: { line: insertLine, character: 0 }
5396
5512
  },
5397
- newText: `$${varName} = null
5513
+ newText: `$${varName} = 0
5398
5514
  `
5399
5515
  }]
5400
5516
  };
@@ -5449,11 +5565,23 @@ function fixUndeclaredTransient(diag, workspace) {
5449
5565
  start: { line: insertLine, character: 0 },
5450
5566
  end: { line: insertLine, character: 0 }
5451
5567
  },
5452
- newText: `%${varName} = null
5568
+ newText: `%${varName} = 0
5453
5569
  `
5454
5570
  }]
5455
5571
  };
5456
5572
  }
5573
+ function fixNullVariableValue(uri, diag) {
5574
+ return {
5575
+ title: "Replace null with 0",
5576
+ kind: "quickfix",
5577
+ diagnosticCodes: [DiagnosticCode.NullVariableValue],
5578
+ edits: [{
5579
+ uri,
5580
+ range: diag.range,
5581
+ newText: "0"
5582
+ }]
5583
+ };
5584
+ }
5457
5585
  function toLspRange6(r) {
5458
5586
  return {
5459
5587
  start: { line: r.start.line, character: r.start.character },