@rohal12/spindle-lsp 0.15.4 → 0.15.5

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.4";
58
+ VERSION = false ? pkg.version : "0.15.5";
59
59
  }
60
60
  });
61
61
 
@@ -23448,6 +23448,7 @@ var init_passage_parser = __esm({
23448
23448
  "StoryTitle",
23449
23449
  "StoryBanner",
23450
23450
  "StoryCaption",
23451
+ "StoryContext",
23451
23452
  "StoryMenu",
23452
23453
  "StoryInterface",
23453
23454
  "StoryAuthor"
@@ -32580,11 +32581,27 @@ var init_workspace_model = __esm({
32580
32581
  WorkspaceModel = class extends EventEmitter2 {
32581
32582
  documents;
32582
32583
  passages;
32583
- macros;
32584
- variables;
32585
- widgets;
32584
+ _macros;
32585
+ get macros() {
32586
+ this.ensureCurrent();
32587
+ return this._macros;
32588
+ }
32589
+ _variables;
32590
+ get variables() {
32591
+ this.ensureCurrent();
32592
+ return this._variables;
32593
+ }
32594
+ _widgets;
32595
+ get widgets() {
32596
+ this.ensureCurrent();
32597
+ return this._widgets;
32598
+ }
32586
32599
  /** Every document's markup, read through Spindle's tooling API. */
32587
- markup;
32600
+ _markup;
32601
+ get markup() {
32602
+ this.ensureCurrent();
32603
+ return this._markup;
32604
+ }
32588
32605
  /** True after initialize() has completed (full workspace scan done). */
32589
32606
  initialized = false;
32590
32607
  /** The story format declared by StoryData, updated with the passages. */
@@ -32594,12 +32611,17 @@ var init_workspace_model = __esm({
32594
32611
  * workspace root, else StoryData's `format-version`. Only a version below
32595
32612
  * {@link MINIMUM_SPINDLE_VERSION} matters (it is reported, not emulated).
32596
32613
  */
32597
- target;
32614
+ _target;
32615
+ get target() {
32616
+ this.ensureCurrent();
32617
+ return this._target;
32618
+ }
32598
32619
  workspaceRoot;
32599
32620
  /** The version installed under the workspace root, read at startup and on refresh(). */
32600
32621
  installedVersion;
32601
32622
  /** The `format-version` StoryData declares. */
32602
32623
  declaredVersion;
32624
+ analysisPending = false;
32603
32625
  debounceTimer = null;
32604
32626
  DEBOUNCE_MS = 200;
32605
32627
  /** Bound listeners for cleanup in dispose(). */
@@ -32610,11 +32632,11 @@ var init_workspace_model = __esm({
32610
32632
  super();
32611
32633
  this.documents = new DocumentStore();
32612
32634
  this.passages = new PassageIndex();
32613
- this.macros = new MacroRegistry();
32635
+ this._macros = new MacroRegistry();
32614
32636
  this.macros.onChange = () => this.markup?.invalidate();
32615
- this.variables = new VariableTracker();
32616
- this.widgets = new WidgetRegistry();
32617
- this.markup = new MarkupIndex({
32637
+ this._variables = new VariableTracker();
32638
+ this._widgets = new WidgetRegistry();
32639
+ this._markup = new MarkupIndex({
32618
32640
  text: (uri) => this.documents.getText(uri),
32619
32641
  passages: (uri) => this.passages.getPassagesInDocument(uri),
32620
32642
  context: () => ({
@@ -32624,7 +32646,7 @@ var init_workspace_model = __esm({
32624
32646
  });
32625
32647
  this.workspaceRoot = config2?.workspaceRoot;
32626
32648
  this.installedVersion = this.workspaceRoot ? readInstalledSpindleVersion(this.workspaceRoot) : void 0;
32627
- this.target = resolveSpindleTarget(this.installedVersion);
32649
+ this._target = resolveSpindleTarget(this.installedVersion);
32628
32650
  this.macros.loadBuiltins();
32629
32651
  this.macros.loadSupplements(macro_supplements_default);
32630
32652
  this.onDocumentChanged = (uri) => this.handleDocumentChange(uri);
@@ -32656,7 +32678,8 @@ var init_workspace_model = __esm({
32656
32678
  */
32657
32679
  refresh() {
32658
32680
  if (this.workspaceRoot) this.installedVersion = readInstalledSpindleVersion(this.workspaceRoot);
32659
- this.cascade();
32681
+ this.analysisPending = true;
32682
+ this.ensureCurrent();
32660
32683
  this.scheduleModelReady();
32661
32684
  }
32662
32685
  /**
@@ -32680,6 +32703,7 @@ var init_workspace_model = __esm({
32680
32703
  * undefined if no StoryData passage names one.
32681
32704
  */
32682
32705
  get storyFormat() {
32706
+ this.ensureCurrent();
32683
32707
  return this.format.name;
32684
32708
  }
32685
32709
  /**
@@ -32689,6 +32713,7 @@ var init_workspace_model = __esm({
32689
32713
  * Spindle.
32690
32714
  */
32691
32715
  isSpindleProject() {
32716
+ this.ensureCurrent();
32692
32717
  return this.format.isSpindle;
32693
32718
  }
32694
32719
  /** Clean up listeners and timers. */
@@ -32702,25 +32727,23 @@ var init_workspace_model = __esm({
32702
32727
  }
32703
32728
  this.removeAllListeners();
32704
32729
  }
32705
- /** Handle a document change: rebuild passages for that document, then cascade. */
32730
+ /** Index local headers immediately; defer expensive cross-document analysis. */
32706
32731
  handleDocumentChange(uri) {
32707
32732
  const text3 = this.documents.getText(uri);
32708
32733
  if (text3 !== void 0 && !isMacroSource(uri)) {
32709
32734
  this.passages.rebuild(uri, text3);
32710
32735
  }
32711
- this.refreshDiscoveredMacros();
32712
- this.cascade();
32736
+ this.analysisPending = true;
32713
32737
  this.updateStoryFormat();
32714
32738
  this.emit("documentChanged", uri);
32715
32739
  this.emit("passagesUpdated", uri);
32716
32740
  this.scheduleModelReady();
32717
32741
  }
32718
- /** Handle a document close: remove its passages and variable usages, then cascade. */
32742
+ /** Remove local indices immediately and schedule cross-document analysis. */
32719
32743
  handleDocumentClose(uri) {
32720
32744
  this.passages.remove(uri);
32721
- this.variables.removeDocument(uri);
32722
- this.refreshDiscoveredMacros();
32723
- this.cascade();
32745
+ this._variables.removeDocument(uri);
32746
+ this.analysisPending = true;
32724
32747
  this.updateStoryFormat();
32725
32748
  this.emit("documentClosed", uri);
32726
32749
  this.emit("passagesUpdated", uri);
@@ -32734,14 +32757,21 @@ var init_workspace_model = __esm({
32734
32757
  this.passages.rebuild(uri, text3);
32735
32758
  }
32736
32759
  }
32760
+ this.analysisPending = true;
32761
+ this.ensureCurrent();
32762
+ }
32763
+ /** Finish pending cross-document work once, before a feature reads the indices. */
32764
+ ensureCurrent() {
32765
+ if (!this.analysisPending) return;
32766
+ this.analysisPending = false;
32737
32767
  this.refreshDiscoveredMacros();
32738
32768
  this.cascade();
32739
32769
  this.updateStoryFormat();
32740
32770
  }
32741
32771
  /**
32742
32772
  * Re-read the story format from the StoryData passages after the passage
32743
- * index changed, emitting 'storyFormatChanged' if it differs. Called after
32744
- * the cascade, so listeners see up-to-date indices.
32773
+ * index changed, emitting 'storyFormatChanged' if it differs. Listeners
32774
+ * that read the analysis indices finish pending analysis through getters.
32745
32775
  */
32746
32776
  updateStoryFormat() {
32747
32777
  const format = resolveStoryFormat(storyDataFormats(
@@ -32763,7 +32793,8 @@ var init_workspace_model = __esm({
32763
32793
  */
32764
32794
  refreshDiscoveredMacros() {
32765
32795
  const found = [];
32766
- for (const uri of this.documents.getUris()) {
32796
+ const init = [];
32797
+ for (const uri of this.documents.getUris().sort()) {
32767
32798
  const text3 = this.documents.getText(uri);
32768
32799
  if (!text3) continue;
32769
32800
  if (isMacroSource(uri)) {
@@ -32777,10 +32808,11 @@ var init_workspace_model = __esm({
32777
32808
  if (!isStoryInit && !isScript) continue;
32778
32809
  lines ??= text3.split("\n");
32779
32810
  const content2 = lines.slice(passage.headerEnd.end.line + 1, passage.range.end.line + 1).join("\n");
32780
- found.push(...isScript ? discoverMacros(content2) : storyInitMacros(content2));
32811
+ if (isScript) found.push(...discoverMacros(content2));
32812
+ else init.push(...storyInitMacros(content2));
32781
32813
  }
32782
32814
  }
32783
- this.macros.setDiscoveredMacros(found);
32815
+ this.macros.setDiscoveredMacros([...found, ...init]);
32784
32816
  }
32785
32817
  /**
32786
32818
  * Cascade: rescan variables and widgets based on current passages.
@@ -32794,7 +32826,7 @@ var init_workspace_model = __esm({
32794
32826
  cascade() {
32795
32827
  this.markup.invalidate();
32796
32828
  this.declaredVersion = storyDataFormatVersion(this.passages.getAllPassages(), (uri) => this.documents.getText(uri));
32797
- this.target = resolveSpindleTarget(this.installedVersion, this.declaredVersion);
32829
+ this._target = resolveSpindleTarget(this.installedVersion, this.declaredVersion);
32798
32830
  const declarations = (name) => {
32799
32831
  const passage = this.passages.getPassage(name);
32800
32832
  return passage ? this.markup.get(passage.uri)?.passages.find((p) => p.passage === passage) : void 0;
@@ -32827,13 +32859,14 @@ var init_workspace_model = __esm({
32827
32859
  const macros = this.macros.toolingMacros();
32828
32860
  return [...macros, ...this.widgets.getAllWidgets().filter((widget) => !this.macros.getMacro(widget.name)).map((widget) => ({ name: widget.name, block: widget.block, subMacros: [], widget: widget.tooling }))];
32829
32861
  }
32830
- /** Debounce modelReady emission. */
32862
+ /** Coalesce analysis and diagnostics across a burst of document changes. */
32831
32863
  scheduleModelReady() {
32832
32864
  if (this.debounceTimer) {
32833
32865
  clearTimeout(this.debounceTimer);
32834
32866
  }
32835
32867
  this.debounceTimer = setTimeout(() => {
32836
32868
  this.debounceTimer = null;
32869
+ this.ensureCurrent();
32837
32870
  this.emit("modelReady");
32838
32871
  }, this.DEBOUNCE_MS);
32839
32872
  }
@@ -34416,8 +34449,6 @@ var init_diagnostics = __esm({
34416
34449
  }
34417
34450
  };
34418
34451
  ctx.workspace.on("modelReady", publishAll);
34419
- ctx.workspace.on("storyFormatChanged", publishAll);
34420
- ctx.workspace.on("documentChanged", publishFor);
34421
34452
  ctx.workspace.on("documentClosed", (uri) => {
34422
34453
  ctx.connection.sendDiagnostics({ uri, diagnostics: [] });
34423
34454
  });
@@ -80061,6 +80092,7 @@ function computeRename(uri, position, newName, workspace) {
80061
80092
  addEdit(declaration.uri, declaration.nameRange, escapePassageName(newName));
80062
80093
  }
80063
80094
  for (const { uri: refUri, ref: ref2, text: text3 } of refEdits) addEdit(refUri, ref2.range, text3);
80095
+ preserveEntryPassage(symbol2.name, newName, workspace, addEdit);
80064
80096
  break;
80065
80097
  }
80066
80098
  case "variable": {
@@ -80133,6 +80165,54 @@ function computeRename(uri, position, newName, workspace) {
80133
80165
  }
80134
80166
  return edits;
80135
80167
  }
80168
+ function preserveEntryPassage(oldName, newName, workspace, addEdit) {
80169
+ const storyData = workspace.passages.getPassages("StoryData");
80170
+ for (const declaration of storyData) {
80171
+ const passage = workspace.markup.get(declaration.uri).passages.find((p) => p.passage === declaration);
80172
+ let data2;
80173
+ try {
80174
+ data2 = JSON.parse(passage.content);
80175
+ if (!data2 || typeof data2 !== "object" || Array.isArray(data2)) throw new Error();
80176
+ } catch {
80177
+ throw new RenameError("Cannot preserve the entry passage: StoryData must contain a valid JSON object.").at(declaration.uri, declaration.nameRange);
80178
+ }
80179
+ const start = typeof data2.start === "string" && data2.start ? data2.start : "Start";
80180
+ if (start !== oldName) continue;
80181
+ if (Object.hasOwn(data2, "start") && typeof data2.start !== "string") {
80182
+ throw new RenameError("Cannot preserve the entry passage: StoryData.start must be a string.").at(declaration.uri, declaration.nameRange);
80183
+ }
80184
+ let depth = 0;
80185
+ let startValue = false;
80186
+ let replaced = false;
80187
+ lexJs(passage.content, {
80188
+ code(ch) {
80189
+ if (ch === "{" || ch === "[") depth++;
80190
+ if (ch === "}" || ch === "]") depth--;
80191
+ if (ch === ",") startValue = false;
80192
+ },
80193
+ literal(text3, index) {
80194
+ if (depth !== 1 || !text3.startsWith('"')) return;
80195
+ const isKey = /^\s*:/.test(passage.content.slice(index + text3.length));
80196
+ if (isKey) startValue = JSON.parse(text3) === "start";
80197
+ else if (startValue) {
80198
+ addEdit(declaration.uri, passage.range(index, index + text3.length), JSON.stringify(newName));
80199
+ replaced = true;
80200
+ }
80201
+ }
80202
+ });
80203
+ if (!replaced) {
80204
+ const index = passage.content.indexOf("{") + 1;
80205
+ addEdit(declaration.uri, passage.range(index, index), `"start":${JSON.stringify(newName)}${Object.keys(data2).length ? "," : ""}`);
80206
+ }
80207
+ }
80208
+ if (storyData.length === 0 && oldName === "Start") {
80209
+ const declaration = workspace.passages.getPassage(oldName);
80210
+ const doc = workspace.markup.get(declaration.uri);
80211
+ const end = doc.passages.at(-1).position(doc.passages.at(-1).content.length);
80212
+ const eol = doc.text.includes("\r\n") ? "\r\n" : "\n";
80213
+ addEdit(declaration.uri, { start: end, end }, `${eol}${eol}:: StoryData${eol}${JSON.stringify({ start: newName })}${eol}`);
80214
+ }
80215
+ }
80136
80216
  function passageHeaderProblem(name) {
80137
80217
  if (name.trim() === "") return "a passage name cannot be empty";
80138
80218
  if (/[\r\n]/.test(name)) return "a passage header is a single line, so the name cannot contain a line break";