@rohal12/spindle-lsp 0.15.34 → 0.15.35

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
@@ -55,7 +55,7 @@ var VERSION;
55
55
  var init_version = __esm({
56
56
  "src/version.ts"() {
57
57
  "use strict";
58
- VERSION = false ? pkg.version : "0.15.34";
58
+ VERSION = false ? pkg.version : "0.15.35";
59
59
  }
60
60
  });
61
61
 
@@ -14056,6 +14056,9 @@ function peg$parse(input2, options) {
14056
14056
  if (peg$success) return peg$result;
14057
14057
  else peg$throw();
14058
14058
  }
14059
+ function isOptionClause(name) {
14060
+ return name.toLowerCase() === OPTION_CLAUSE;
14061
+ }
14059
14062
  function consumesOptionClauses(macro) {
14060
14063
  const name = macro.name.toLowerCase();
14061
14064
  return macro.source === "builtin" && !macro.widget && (name === "listbox" || name === "cycle");
@@ -21872,6 +21875,71 @@ function blockLookup(macros, includeRuntime = true) {
21872
21875
  for (const macro of effectiveToolingMacros(macros)) definitions.set(macro.name, macro.block);
21873
21876
  return (name) => includeRuntime && isRegisteredBlockMacro(name) || (definitions.get(name) ?? (includeRuntime ? isBlockMacro(name) : isBuiltinBlockMacro(name)));
21874
21877
  }
21878
+ function macroCalls(source, macros = []) {
21879
+ const all = effectiveToolingMacros(macros);
21880
+ const definitions = new NameMap(all.map((macro) => [macro.name, macro]));
21881
+ const parametersOf2 = parameterLookup(all);
21882
+ const options = tokenizationOptions({
21883
+ macros: all,
21884
+ hooks: { isBlock: blockLookup(all, false) }
21885
+ });
21886
+ const hooks = options.hooks;
21887
+ const tokens2 = tokenizeMarkupTolerant$1(source, options).tokens;
21888
+ const calls = [];
21889
+ const classify2 = (tokens3) => {
21890
+ const found = /* @__PURE__ */ new Map();
21891
+ const pending = pairMarkup(tokens3, hooks).nodes.map((node2) => ({ node: node2 }));
21892
+ while (pending.length) {
21893
+ const { node: node2, parent } = pending.pop();
21894
+ const token = node2.token;
21895
+ if (token.type === "macro") {
21896
+ const definition2 = parent && definitions.get(parent.name);
21897
+ const value = definition2 && consumesOptionClauses(definition2) && isOptionClause(token.name);
21898
+ found.set(token, {
21899
+ token,
21900
+ kind: value ? "value" : "invocation",
21901
+ ...value && { owner: parent }
21902
+ });
21903
+ }
21904
+ if (!node2.body) continue;
21905
+ const owner = token.type === "macro" ? token : void 0;
21906
+ for (const child of node2.body.children) pending.push({
21907
+ node: child,
21908
+ parent: owner
21909
+ });
21910
+ for (const branch of node2.body.branches) {
21911
+ found.set(branch.tag, {
21912
+ token: branch.tag,
21913
+ kind: "branch",
21914
+ owner
21915
+ });
21916
+ for (const child of branch.children) pending.push({
21917
+ node: child,
21918
+ parent: owner
21919
+ });
21920
+ }
21921
+ }
21922
+ for (const token of tokens3) {
21923
+ if (token.type !== "macro" || token.isClose) continue;
21924
+ const call = found.get(token) ?? {
21925
+ token,
21926
+ kind: "branch"
21927
+ };
21928
+ calls.push(call);
21929
+ }
21930
+ };
21931
+ classify2(tokens2);
21932
+ for (const piece of passagePiecesOf(source, tokens2, parametersOf2, { isBlock: hooks.isBlock })) {
21933
+ if (piece.kind !== "text" || !piece.tokens.length) continue;
21934
+ classify2(piece.tokens);
21935
+ }
21936
+ let literalEnd = -1;
21937
+ return calls.sort((a, b) => a.token.start - b.token.start).filter((call) => {
21938
+ if (call.token.start < literalEnd) return false;
21939
+ if (call.kind === "value" || call.kind === "branch") literalEnd = call.token.argsEnd;
21940
+ return true;
21941
+ });
21942
+ }
21875
21943
  function passagePieces(source, macros = []) {
21876
21944
  const all = effectiveToolingMacros(macros);
21877
21945
  const parametersOf2 = parameterLookup(all);
@@ -33216,6 +33284,7 @@ var init_passage_markup = __esm({
33216
33284
  cachedTokens;
33217
33285
  cachedPairing;
33218
33286
  cachedPieces;
33287
+ cachedInvocations;
33219
33288
  cachedDeclarations;
33220
33289
  /** The document offset of `contentOffset`. */
33221
33290
  docOffset(contentOffset) {
@@ -33268,6 +33337,16 @@ var init_passage_markup = __esm({
33268
33337
  get pieces() {
33269
33338
  return this.cachedPieces ??= this.isMarkup ? passagePieces(this.content, this.doc.context.macros) : [];
33270
33339
  }
33340
+ /** Contextual calls, excluding parent-owned clauses and widget registration heads. */
33341
+ get invocations() {
33342
+ if (!this.cachedInvocations) {
33343
+ const macros = this.doc.context.macros;
33344
+ const definitions = widgetDefinitions([{ ...this.passage, content: this.content }], macros);
33345
+ const registration = new Set(definitions.map((definition2) => definition2.start));
33346
+ this.cachedInvocations = this.isMarkup ? macroCalls(this.content, macros).filter((call) => call.kind === "invocation" && !registration.has(call.token.start)).map((call) => call.token) : [];
33347
+ }
33348
+ return this.cachedInvocations;
33349
+ }
33271
33350
  /** Whether the cursor is executable, with decoded code mapped back to `content`. */
33272
33351
  completionAt(offset2) {
33273
33352
  return this.isMarkup ? completionContext(this.content, offset2, this.doc.context.macros) : { kind: "literal" };
@@ -33607,24 +33686,6 @@ var init_variable_tracker = __esm({
33607
33686
  }
33608
33687
  });
33609
33688
 
33610
- // src/core/markup/tokens.ts
33611
- function* markupTokens(passage) {
33612
- yield* passage.tokens;
33613
- for (const piece of passage.pieces) {
33614
- if (piece.kind === "text") yield* piece.tokens;
33615
- }
33616
- }
33617
- function* macroTokens(passage) {
33618
- for (const token of markupTokens(passage)) {
33619
- if (token.type === "macro") yield token;
33620
- }
33621
- }
33622
- var init_tokens = __esm({
33623
- "src/core/markup/tokens.ts"() {
33624
- "use strict";
33625
- }
33626
- });
33627
-
33628
33689
  // src/core/markup/widgets.ts
33629
33690
  function widgetSources(passages, macros) {
33630
33691
  const sources = /* @__PURE__ */ new Map();
@@ -33650,7 +33711,6 @@ var init_widget_registry = __esm({
33650
33711
  "src/core/workspace/widget-registry.ts"() {
33651
33712
  "use strict";
33652
33713
  init_tooling();
33653
- init_tokens();
33654
33714
  init_widgets();
33655
33715
  WidgetRegistry = class {
33656
33716
  /** Widgets keyed by lower-cased name; Spindle resolves widgets case-insensitively. */
@@ -33729,8 +33789,8 @@ var init_widget_registry = __esm({
33729
33789
  recordInvocations(uri, passages) {
33730
33790
  const names = /* @__PURE__ */ new Set();
33731
33791
  for (const passage of passages) {
33732
- for (const macro of macroTokens(passage)) {
33733
- if (!macro.isClose) names.add(macro.name.toLowerCase());
33792
+ for (const macro of passage.invocations) {
33793
+ names.add(macro.name.toLowerCase());
33734
33794
  }
33735
33795
  }
33736
33796
  this.invokedByUri.set(uri, names);
@@ -34759,7 +34819,9 @@ var init_diagnostic_codes = __esm({
34759
34819
  UnsupportedSpindleVersion: "SP001",
34760
34820
  /** A fatal Twee source error reported by the public compiler parser. */
34761
34821
  TweeSyntaxError: "SP002",
34762
- /** StoryData explicitly selects an entry passage absent from the story. */
34822
+ /** Compiler-owned errors decoding StoryData metadata. */
34823
+ InvalidStoryData: "SP003",
34824
+ /** The selected entry passage is absent or omitted from compiled story data. */
34763
34825
  MissingEntryPassage: "SP004",
34764
34826
  /** `unknown-macro`: a macro that is none of the built-in, configured or defined ones. */
34765
34827
  UndefinedMacro: "SP100",
@@ -34815,6 +34877,7 @@ var init_diagnostic_codes = __esm({
34815
34877
  severityMap = {
34816
34878
  [DiagnosticCode.UnsupportedSpindleVersion]: "warning",
34817
34879
  [DiagnosticCode.TweeSyntaxError]: "error",
34880
+ [DiagnosticCode.InvalidStoryData]: "error",
34818
34881
  [DiagnosticCode.MissingEntryPassage]: "error",
34819
34882
  [DiagnosticCode.UndefinedMacro]: "warning",
34820
34883
  [DiagnosticCode.MalformedContainer]: "error",
@@ -34853,6 +34916,24 @@ var init_diagnostic_codes = __esm({
34853
34916
  }
34854
34917
  });
34855
34918
 
34919
+ // src/core/markup/tokens.ts
34920
+ function* markupTokens(passage) {
34921
+ yield* passage.tokens;
34922
+ for (const piece of passage.pieces) {
34923
+ if (piece.kind === "text") yield* piece.tokens;
34924
+ }
34925
+ }
34926
+ function* macroTokens(passage) {
34927
+ for (const token of markupTokens(passage)) {
34928
+ if (token.type === "macro") yield token;
34929
+ }
34930
+ }
34931
+ var init_tokens = __esm({
34932
+ "src/core/markup/tokens.ts"() {
34933
+ "use strict";
34934
+ }
34935
+ });
34936
+
34856
34937
  // src/core/markup/source-view.ts
34857
34938
  function viewIndex(view, offset2) {
34858
34939
  if (!view.sourceOffsets) return offset2 - view.offset;
@@ -35215,8 +35296,8 @@ function buildReport(documents, workspace) {
35215
35296
  const invoked = /* @__PURE__ */ new Set();
35216
35297
  for (const passage of passages) {
35217
35298
  if (!passage.isMarkup) continue;
35218
- for (const token of macroTokens(passage)) {
35219
- if (!token.isClose) invoked.add(token.name.toLowerCase());
35299
+ for (const token of passage.invocations) {
35300
+ invoked.add(token.name.toLowerCase());
35220
35301
  }
35221
35302
  }
35222
35303
  return {
@@ -35235,17 +35316,33 @@ function buildReport(documents, workspace) {
35235
35316
  }
35236
35317
  function storyDataDiagnostics(documents, workspace) {
35237
35318
  const found = /* @__PURE__ */ new Map();
35238
- const effective = workspace.passages.getEffectivePassage("StoryData");
35239
- for (const doc of documents) {
35319
+ const data2 = workspace.passages.getEffectivePassage("StoryData");
35320
+ const implicit = workspace.passages.getEffectivePassage("Start");
35321
+ if (!data2 && (!implicit || isRuntimePassage(implicit))) return found;
35322
+ const { parseTwee, StoryBuilder, startPassageDiagnostics } = createCompilerRequire3(import.meta.url)("@rohal12/twee-ts");
35323
+ const builder = new StoryBuilder();
35324
+ const sources = new Map(documents.flatMap((doc) => doc.passages.map((passage) => [passage.passage, passage])));
35325
+ const parsed = new Map(documents.map((doc) => [doc, new Map(parseTwee(doc.text).passages.map((passage) => [passage.source?.line, passage]))]));
35326
+ const effective = workspace.passages.getEffectivePassages();
35327
+ for (const source of effective) {
35328
+ const passage = sources.get(source);
35329
+ if (!passage) continue;
35330
+ const input2 = parsed.get(passage.doc)?.get(source.headerEnd.start.line + 1);
35331
+ if (!input2) continue;
35240
35332
  const diagnostics = [];
35241
- for (const passage of doc.passages) {
35242
- if (passage.passage !== effective) continue;
35243
- const data2 = readStoryData(passage.content);
35244
- if (data2.start && !workspace.passages.getEffectivePassage(data2.start)) {
35245
- diagnostics.push(makeDiag(passage.passage.nameRange, DiagnosticCode.MissingEntryPassage, `Starting passage "${data2.start}" not found.`));
35246
- }
35333
+ builder.add(input2, diagnostics);
35334
+ if (source.name === "StoryData") {
35335
+ found.set(passage.doc, diagnostics.filter((d) => d.level === "error").map((d) => makeDiag(passage.range(0, passage.content.trimEnd().length), DiagnosticCode.InvalidStoryData, d.message)));
35247
35336
  }
35248
- found.set(doc, diagnostics);
35337
+ }
35338
+ const story = builder.build();
35339
+ const entry = workspace.passages.getEffectivePassage(story.twine2.start || "Start");
35340
+ const owner = story.twine2.start ? workspace.passages.getEffectivePassage("StoryData") : entry;
35341
+ const markup = owner && sources.get(owner);
35342
+ if (markup) {
35343
+ const diagnostics = found.get(markup.doc) ?? [];
35344
+ diagnostics.push(...startPassageDiagnostics(story).map((d) => makeDiag(markup.passage.nameRange, DiagnosticCode.MissingEntryPassage, d.message)));
35345
+ found.set(markup.doc, diagnostics);
35249
35346
  }
35250
35347
  return found;
35251
35348
  }
@@ -35597,8 +35694,7 @@ function widgetDiagnostics(passage, story, workspace) {
35597
35694
  if (!passage.isMarkup) return [];
35598
35695
  const diagnostics = [];
35599
35696
  const macros = [...macroTokens(passage)];
35600
- for (const token of macros) {
35601
- if (token.isClose) continue;
35697
+ for (const token of passage.invocations) {
35602
35698
  if (workspace.getEffectiveMacro(token.name)) continue;
35603
35699
  const widget = story.widgets.get(token.name.toLowerCase());
35604
35700
  if (!widget) continue;
@@ -35732,7 +35828,6 @@ var init_diagnostics = __esm({
35732
35828
  init_link_parser();
35733
35829
  init_passage_parser();
35734
35830
  init_story_variables_owner();
35735
- init_story_data();
35736
35831
  init_macro_sources();
35737
35832
  init_spindle_version();
35738
35833
  tweeReports = /* @__PURE__ */ new WeakMap();
@@ -87432,7 +87527,7 @@ function linkable(name) {
87432
87527
  return tokens2.length === 1 && link.type === "link" && link.target === name && !bracketLinkMismatch("label", name);
87433
87528
  }
87434
87529
  function getPassageNameCompletions(workspace) {
87435
- return workspace.passages.getEffectivePassages().filter((p) => isRuntimePassage(p) && linkable(p.name)).map((p) => ({
87530
+ return workspace.passages.getEffectivePassages().filter((p) => isRuntimePassage(p) && !isSpecialPassage(p.name) && linkable(p.name)).map((p) => ({
87436
87531
  label: p.name,
87437
87532
  kind: 18,
87438
87533
  // CompletionItemKind.Reference
@@ -87745,14 +87840,14 @@ var init_signature = __esm({
87745
87840
  function* pairedHeads(paired, tokens2) {
87746
87841
  const unclosed = [];
87747
87842
  for (const node2 of walkNodes(paired.nodes)) {
87748
- if (node2.token.type === "macro") yield { name: node2.token.name, closing: false, start: node2.token.nameStart, end: node2.token.nameEnd };
87843
+ if (node2.token.type === "macro") yield { name: node2.token.name, closing: false, opener: node2.token.start, start: node2.token.nameStart, end: node2.token.nameEnd };
87749
87844
  for (const branch of node2.body?.branches ?? []) {
87750
- yield { name: branch.tag.name, closing: false, start: branch.tag.nameStart, end: branch.tag.nameEnd };
87845
+ yield { name: branch.tag.name, closing: false, opener: branch.tag.start, start: branch.tag.nameStart, end: branch.tag.nameEnd };
87751
87846
  }
87752
87847
  const close = node2.body?.close;
87753
87848
  if (node2.token.type !== "macro") continue;
87754
87849
  if (close?.type === "macro" && close.name.toLowerCase() === node2.token.name.toLowerCase()) {
87755
- yield { name: close.name, closing: true, start: close.nameStart, end: close.nameEnd };
87850
+ yield { name: close.name, closing: true, opener: node2.token.start, start: close.nameStart, end: close.nameEnd };
87756
87851
  } else if (node2.body && !close) {
87757
87852
  unclosed.push({ name: node2.token.name.toLowerCase(), start: node2.token.start });
87758
87853
  }
@@ -87762,8 +87857,9 @@ function* pairedHeads(paired, tokens2) {
87762
87857
  const token = byStart.get(error62.start);
87763
87858
  if (error62.code !== "stray-closer" || token?.type !== "macro" || !token.isClose) continue;
87764
87859
  const name = token.name.toLowerCase();
87765
- if (unclosed.some((open2) => open2.name === name && open2.start < error62.start)) {
87766
- yield { name: token.name, closing: true, start: token.nameStart, end: token.nameEnd };
87860
+ const open2 = unclosed.find((open3) => open3.name === name && open3.start < error62.start);
87861
+ if (open2) {
87862
+ yield { name: token.name, closing: true, opener: open2.start, start: token.nameStart, end: token.nameEnd };
87767
87863
  }
87768
87864
  }
87769
87865
  }
@@ -87776,7 +87872,8 @@ function passageMacroHeads(passage) {
87776
87872
  if (piece.kind !== "text") continue;
87777
87873
  found.push(...pairedHeads(pairMarkup(piece.tokens, { isBlock }), piece.tokens));
87778
87874
  }
87779
- heads = found.sort((a, b) => a.start - b.start).map((head) => ({ ...head, passage, range: passage.range(head.start, head.end) }));
87875
+ const invocations = new Set(passage.invocations.map((token) => token.start));
87876
+ heads = found.sort((a, b) => a.start - b.start).map(({ opener, ...head }) => ({ ...head, invocation: invocations.has(opener), passage, range: passage.range(head.start, head.end) }));
87780
87877
  kept2.set(passage, heads);
87781
87878
  return heads;
87782
87879
  }
@@ -87824,7 +87921,7 @@ function getPassageRefDefinition(uri, position, workspace) {
87824
87921
  function getWidgetDefinition(uri, position, workspace) {
87825
87922
  const doc = workspace.markup.get(uri);
87826
87923
  const head = doc && macroHeadAt(doc, position);
87827
- if (!head) return null;
87924
+ if (!head?.invocation) return null;
87828
87925
  if (workspace.getEffectiveMacro(head.name)) return null;
87829
87926
  const widget = workspace.widgets.getWidget(head.name);
87830
87927
  return widget ? { uri: widget.uri, range: widget.range } : null;
@@ -87887,7 +87984,7 @@ function findReferences(uri, position, workspace, includeDeclaration) {
87887
87984
  if (declaration) return findWidgetReferences(declaration.name, workspace, includeDeclaration);
87888
87985
  const doc = workspace.markup.get(uri);
87889
87986
  const head = doc && macroHeadAt(doc, position);
87890
- if (head) {
87987
+ if (head?.invocation) {
87891
87988
  const widget = workspace.widgets.getWidget(head.name);
87892
87989
  if (!workspace.getEffectiveMacro(head.name) && widget && (!head.closing || workspace.isContainer(head.name))) {
87893
87990
  return findWidgetReferences(head.name, workspace, includeDeclaration);
@@ -87957,17 +88054,11 @@ function findWidgetReferences(widgetName2, workspace, includeDeclaration, includ
87957
88054
  if (workspace.getEffectiveMacro(widgetName2)) return locations;
87958
88055
  const lowerName2 = widgetName2.toLowerCase();
87959
88056
  const isBlock = workspace.isContainer(widgetName2);
87960
- for (const docUri of workspace.documents.getUris()) {
87961
- const doc = workspace.markup.get(docUri);
87962
- if (!doc) continue;
87963
- for (const head of documentMacroHeads(doc)) {
87964
- if (head.closing && (!isBlock || !includeClosingTags)) continue;
87965
- if (head.name.toLowerCase() === lowerName2) {
87966
- locations.push({ uri: docUri, range: head.range });
87967
- }
87968
- }
87969
- }
87970
- return locations;
88057
+ const calls = workspace.documents.getUris().flatMap((uri) => {
88058
+ const doc = workspace.markup.get(uri);
88059
+ return doc ? documentMacroHeads(doc).filter((head) => head.invocation && head.name.toLowerCase() === lowerName2 && (!head.closing || isBlock && includeClosingTags)).map((head) => ({ uri, range: head.range })) : [];
88060
+ });
88061
+ return locations.concat(calls);
87971
88062
  }
87972
88063
  function toLspRange3(r) {
87973
88064
  return {
@@ -88292,7 +88383,7 @@ function resolveSymbolAtCursor(uri, position, workspace) {
88292
88383
  if (declaration) return { kind: "widget", name: declaration.name, range: declaration.nameRange };
88293
88384
  const doc = workspace.markup.get(uri);
88294
88385
  const head = doc && macroHeadAt(doc, position);
88295
- if (head) {
88386
+ if (head?.invocation) {
88296
88387
  const widget = workspace.widgets.getWidget(head.name);
88297
88388
  if (!workspace.getEffectiveMacro(head.name) && widget && (!head.closing || workspace.isContainer(head.name))) {
88298
88389
  return { kind: "widget", name: head.name, range: head.range };
@@ -88685,9 +88776,13 @@ var init_semantic_tokens = __esm({
88685
88776
  import { readFileSync as readFileSync7 } from "node:fs";
88686
88777
  import { basename as basename2, dirname as dirname5, join as join4 } from "node:path";
88687
88778
  import { fileURLToPath as fileURLToPath4, pathToFileURL as pathToFileURL6 } from "node:url";
88779
+ import { isDeepStrictEqual } from "node:util";
88688
88780
  function computeCodeActions(uri, diagnostics, workspace, options = {}) {
88689
88781
  const actions = [];
88782
+ const current2 = computeDiagnostics(uri, workspace);
88690
88783
  for (const diag of diagnostics) {
88784
+ const fresh = current2.find((candidate) => candidate.code === diag.code && isDeepStrictEqual(candidate.range, diag.range) && isDeepStrictEqual(candidate.data, diag.data));
88785
+ if (!fresh && diag.code !== DiagnosticCode.NoStoryVariables) continue;
88691
88786
  switch (diag.code) {
88692
88787
  case DiagnosticCode.UndefinedMacro:
88693
88788
  actions.push(...fixUndefinedMacro(uri, diag, workspace, options));
@@ -89017,6 +89112,7 @@ var init_code_actions = __esm({
89017
89112
  "use strict";
89018
89113
  import_yaml3 = __toESM(require_dist(), 1);
89019
89114
  init_diagnostic_codes();
89115
+ init_diagnostics();
89020
89116
  init_config_loader();
89021
89117
  init_story_variables_owner();
89022
89118
  init_macro_sources();