@mariozechner/pi-tui 0.62.0 → 0.63.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.
@@ -155,6 +155,7 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
155
155
  minPrimaryColumnWidth: 12,
156
156
  maxPrimaryColumnWidth: 32,
157
157
  };
158
+ const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
158
159
  export class Editor {
159
160
  state = {
160
161
  lines: [""],
@@ -178,6 +179,11 @@ export class Editor {
178
179
  autocompleteState = null;
179
180
  autocompletePrefix = "";
180
181
  autocompleteMaxVisible = 5;
182
+ autocompleteAbort;
183
+ autocompleteDebounceTimer;
184
+ autocompleteRequestTask = Promise.resolve();
185
+ autocompleteStartToken = 0;
186
+ autocompleteRequestId = 0;
181
187
  // Paste tracking for large pastes
182
188
  pastes = new Map();
183
189
  pasteCounter = 0;
@@ -237,6 +243,7 @@ export class Editor {
237
243
  }
238
244
  }
239
245
  setAutocompleteProvider(provider) {
246
+ this.cancelAutocomplete();
240
247
  this.autocompleteProvider = provider;
241
248
  }
242
249
  /**
@@ -478,7 +485,6 @@ export class Editor {
478
485
  if (kb.matches(data, "tui.input.tab")) {
479
486
  const selected = this.autocompleteList.getSelectedItem();
480
487
  if (selected && this.autocompleteProvider) {
481
- const shouldChainSlashArgumentAutocomplete = this.shouldChainSlashArgumentAutocompleteOnTabSelection();
482
488
  this.pushUndoSnapshot();
483
489
  this.lastAction = null;
484
490
  const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
@@ -488,9 +494,6 @@ export class Editor {
488
494
  this.cancelAutocomplete();
489
495
  if (this.onChange)
490
496
  this.onChange(this.getText());
491
- if (shouldChainSlashArgumentAutocomplete && this.isBareCompletedSlashCommandAtCursor()) {
492
- this.tryTriggerAutocomplete();
493
- }
494
497
  }
495
498
  return;
496
499
  }
@@ -781,6 +784,7 @@ export class Editor {
781
784
  return { line: this.state.cursorLine, col: this.state.cursorCol };
782
785
  }
783
786
  setText(text) {
787
+ this.cancelAutocomplete();
784
788
  this.lastAction = null;
785
789
  this.historyIndex = -1; // Exit history browsing mode
786
790
  const normalized = this.normalizeText(text);
@@ -798,6 +802,7 @@ export class Editor {
798
802
  insertTextAtCursor(text) {
799
803
  if (!text)
800
804
  return;
805
+ this.cancelAutocomplete();
801
806
  this.pushUndoSnapshot();
802
807
  this.lastAction = null;
803
808
  this.historyIndex = -1;
@@ -908,6 +913,7 @@ export class Editor {
908
913
  }
909
914
  }
910
915
  handlePaste(pastedText) {
916
+ this.cancelAutocomplete();
911
917
  this.historyIndex = -1; // Exit history browsing mode
912
918
  this.lastAction = null;
913
919
  this.pushUndoSnapshot();
@@ -952,6 +958,7 @@ export class Editor {
952
958
  this.insertTextAtCursorInternal(filteredText);
953
959
  }
954
960
  addNewLine() {
961
+ this.cancelAutocomplete();
955
962
  this.historyIndex = -1; // Exit history browsing mode
956
963
  this.lastAction = null;
957
964
  this.pushUndoSnapshot();
@@ -981,6 +988,7 @@ export class Editor {
981
988
  return this.state.cursorCol > 0 && currentLine[this.state.cursorCol - 1] === "\\";
982
989
  }
983
990
  submitValue() {
991
+ this.cancelAutocomplete();
984
992
  const result = this.expandPasteMarkers(this.state.lines.join("\n")).trim();
985
993
  this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
986
994
  this.pastes.clear();
@@ -1677,22 +1685,6 @@ export class Editor {
1677
1685
  isInSlashCommandContext(textBeforeCursor) {
1678
1686
  return this.isSlashMenuAllowed() && textBeforeCursor.trimStart().startsWith("/");
1679
1687
  }
1680
- shouldChainSlashArgumentAutocompleteOnTabSelection() {
1681
- if (this.autocompleteState !== "regular") {
1682
- return false;
1683
- }
1684
- const currentLine = this.state.lines[this.state.cursorLine] || "";
1685
- const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
1686
- return this.isInSlashCommandContext(textBeforeCursor) && !textBeforeCursor.trimStart().includes(" ");
1687
- }
1688
- isBareCompletedSlashCommandAtCursor() {
1689
- const currentLine = this.state.lines[this.state.cursorLine] || "";
1690
- if (this.state.cursorCol !== currentLine.length) {
1691
- return false;
1692
- }
1693
- const textBeforeCursor = currentLine.slice(0, this.state.cursorCol).trimStart();
1694
- return /^\/\S+ $/.test(textBeforeCursor);
1695
- }
1696
1688
  // Autocomplete methods
1697
1689
  /**
1698
1690
  * Find the best autocomplete item index for the given prefix.
@@ -1725,38 +1717,13 @@ export class Editor {
1725
1717
  return new SelectList(items, this.autocompleteMaxVisible, this.theme.selectList, layout);
1726
1718
  }
1727
1719
  tryTriggerAutocomplete(explicitTab = false) {
1728
- if (!this.autocompleteProvider)
1729
- return;
1730
- // Check if we should trigger file completion on Tab
1731
- if (explicitTab) {
1732
- const provider = this.autocompleteProvider;
1733
- const shouldTrigger = !provider.shouldTriggerFileCompletion ||
1734
- provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1735
- if (!shouldTrigger) {
1736
- return;
1737
- }
1738
- }
1739
- const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1740
- if (suggestions && suggestions.items.length > 0) {
1741
- this.autocompletePrefix = suggestions.prefix;
1742
- this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
1743
- // If typed prefix exactly matches one of the suggestions, select that item
1744
- const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
1745
- if (bestMatchIndex >= 0) {
1746
- this.autocompleteList.setSelectedIndex(bestMatchIndex);
1747
- }
1748
- this.autocompleteState = "regular";
1749
- }
1750
- else {
1751
- this.cancelAutocomplete();
1752
- }
1720
+ this.requestAutocomplete({ force: false, explicitTab });
1753
1721
  }
1754
1722
  handleTabCompletion() {
1755
1723
  if (!this.autocompleteProvider)
1756
1724
  return;
1757
1725
  const currentLine = this.state.lines[this.state.cursorLine] || "";
1758
1726
  const beforeCursor = currentLine.slice(0, this.state.cursorCol);
1759
- // Check if we're in a slash command context
1760
1727
  if (this.isInSlashCommandContext(beforeCursor) && !beforeCursor.trimStart().includes(" ")) {
1761
1728
  this.handleSlashCommandCompletion();
1762
1729
  }
@@ -1765,79 +1732,130 @@ export class Editor {
1765
1732
  }
1766
1733
  }
1767
1734
  handleSlashCommandCompletion() {
1768
- this.tryTriggerAutocomplete(true);
1735
+ this.requestAutocomplete({ force: false, explicitTab: true });
1769
1736
  }
1770
- /*
1771
- https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/559322883
1772
- 17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
1773
- 536643416/job/55932288317 havea look at .gi
1774
- */
1775
1737
  forceFileAutocomplete(explicitTab = false) {
1738
+ this.requestAutocomplete({ force: true, explicitTab });
1739
+ }
1740
+ requestAutocomplete(options) {
1776
1741
  if (!this.autocompleteProvider)
1777
1742
  return;
1778
- // Check if provider supports force file suggestions via runtime check
1779
- const provider = this.autocompleteProvider;
1780
- if (typeof provider.getForceFileSuggestions !== "function") {
1781
- this.tryTriggerAutocomplete(true);
1743
+ if (options.force) {
1744
+ const provider = this.autocompleteProvider;
1745
+ const shouldTrigger = !provider.shouldTriggerFileCompletion ||
1746
+ provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1747
+ if (!shouldTrigger) {
1748
+ return;
1749
+ }
1750
+ }
1751
+ this.cancelAutocompleteRequest();
1752
+ const startToken = ++this.autocompleteStartToken;
1753
+ const debounceMs = this.getAutocompleteDebounceMs(options);
1754
+ if (debounceMs > 0) {
1755
+ this.autocompleteDebounceTimer = setTimeout(() => {
1756
+ this.autocompleteDebounceTimer = undefined;
1757
+ void this.startAutocompleteRequest(startToken, options);
1758
+ }, debounceMs);
1782
1759
  return;
1783
1760
  }
1784
- const suggestions = provider.getForceFileSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1785
- if (suggestions && suggestions.items.length > 0) {
1786
- // If there's exactly one suggestion, apply it immediately
1787
- if (explicitTab && suggestions.items.length === 1) {
1788
- const item = suggestions.items[0];
1789
- this.pushUndoSnapshot();
1790
- this.lastAction = null;
1791
- const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, item, suggestions.prefix);
1792
- this.state.lines = result.lines;
1793
- this.state.cursorLine = result.cursorLine;
1794
- this.setCursorCol(result.cursorCol);
1795
- if (this.onChange)
1796
- this.onChange(this.getText());
1761
+ void this.startAutocompleteRequest(startToken, options);
1762
+ }
1763
+ async startAutocompleteRequest(startToken, options) {
1764
+ const previousTask = this.autocompleteRequestTask;
1765
+ this.autocompleteRequestTask = (async () => {
1766
+ await previousTask;
1767
+ if (startToken !== this.autocompleteStartToken || !this.autocompleteProvider) {
1797
1768
  return;
1798
1769
  }
1799
- this.autocompletePrefix = suggestions.prefix;
1800
- this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
1801
- // If typed prefix exactly matches one of the suggestions, select that item
1802
- const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
1803
- if (bestMatchIndex >= 0) {
1804
- this.autocompleteList.setSelectedIndex(bestMatchIndex);
1805
- }
1806
- this.autocompleteState = "force";
1770
+ const controller = new AbortController();
1771
+ this.autocompleteAbort = controller;
1772
+ const requestId = ++this.autocompleteRequestId;
1773
+ const snapshotText = this.getText();
1774
+ const snapshotLine = this.state.cursorLine;
1775
+ const snapshotCol = this.state.cursorCol;
1776
+ await this.runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options);
1777
+ })();
1778
+ await this.autocompleteRequestTask;
1779
+ }
1780
+ getAutocompleteDebounceMs(options) {
1781
+ if (options.explicitTab || options.force) {
1782
+ return 0;
1807
1783
  }
1808
- else {
1784
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1785
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
1786
+ const isAttachmentContext = /(?:^|[ \t])@(?:"[^"]*|[^\s]*)$/.test(textBeforeCursor);
1787
+ return isAttachmentContext ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
1788
+ }
1789
+ async runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options) {
1790
+ if (!this.autocompleteProvider)
1791
+ return;
1792
+ const suggestions = await this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol, { signal: controller.signal, force: options.force });
1793
+ if (!this.isAutocompleteRequestCurrent(requestId, controller, snapshotText, snapshotLine, snapshotCol)) {
1794
+ return;
1795
+ }
1796
+ this.autocompleteAbort = undefined;
1797
+ if (!suggestions || suggestions.items.length === 0) {
1809
1798
  this.cancelAutocomplete();
1799
+ this.tui.requestRender();
1800
+ return;
1801
+ }
1802
+ if (options.force && options.explicitTab && suggestions.items.length === 1) {
1803
+ const item = suggestions.items[0];
1804
+ this.pushUndoSnapshot();
1805
+ this.lastAction = null;
1806
+ const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, item, suggestions.prefix);
1807
+ this.state.lines = result.lines;
1808
+ this.state.cursorLine = result.cursorLine;
1809
+ this.setCursorCol(result.cursorCol);
1810
+ if (this.onChange)
1811
+ this.onChange(this.getText());
1812
+ this.tui.requestRender();
1813
+ return;
1810
1814
  }
1815
+ this.applyAutocompleteSuggestions(suggestions, options.force ? "force" : "regular");
1816
+ this.tui.requestRender();
1811
1817
  }
1812
- cancelAutocomplete() {
1818
+ isAutocompleteRequestCurrent(requestId, controller, snapshotText, snapshotLine, snapshotCol) {
1819
+ return (!controller.signal.aborted &&
1820
+ requestId === this.autocompleteRequestId &&
1821
+ this.getText() === snapshotText &&
1822
+ this.state.cursorLine === snapshotLine &&
1823
+ this.state.cursorCol === snapshotCol);
1824
+ }
1825
+ applyAutocompleteSuggestions(suggestions, state) {
1826
+ this.autocompletePrefix = suggestions.prefix;
1827
+ this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
1828
+ const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
1829
+ if (bestMatchIndex >= 0) {
1830
+ this.autocompleteList.setSelectedIndex(bestMatchIndex);
1831
+ }
1832
+ this.autocompleteState = state;
1833
+ }
1834
+ cancelAutocompleteRequest() {
1835
+ this.autocompleteStartToken += 1;
1836
+ if (this.autocompleteDebounceTimer) {
1837
+ clearTimeout(this.autocompleteDebounceTimer);
1838
+ this.autocompleteDebounceTimer = undefined;
1839
+ }
1840
+ this.autocompleteAbort?.abort();
1841
+ this.autocompleteAbort = undefined;
1842
+ }
1843
+ clearAutocompleteUi() {
1813
1844
  this.autocompleteState = null;
1814
1845
  this.autocompleteList = undefined;
1815
1846
  this.autocompletePrefix = "";
1816
1847
  }
1848
+ cancelAutocomplete() {
1849
+ this.cancelAutocompleteRequest();
1850
+ this.clearAutocompleteUi();
1851
+ }
1817
1852
  isShowingAutocomplete() {
1818
1853
  return this.autocompleteState !== null;
1819
1854
  }
1820
1855
  updateAutocomplete() {
1821
1856
  if (!this.autocompleteState || !this.autocompleteProvider)
1822
1857
  return;
1823
- if (this.autocompleteState === "force") {
1824
- this.forceFileAutocomplete();
1825
- return;
1826
- }
1827
- const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1828
- if (suggestions && suggestions.items.length > 0) {
1829
- this.autocompletePrefix = suggestions.prefix;
1830
- // Always create new SelectList to ensure update
1831
- this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
1832
- // If typed prefix exactly matches one of the suggestions, select that item
1833
- const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
1834
- if (bestMatchIndex >= 0) {
1835
- this.autocompleteList.setSelectedIndex(bestMatchIndex);
1836
- }
1837
- }
1838
- else {
1839
- this.cancelAutocomplete();
1840
- }
1858
+ this.requestAutocomplete({ force: this.autocompleteState === "force", explicitTab: false });
1841
1859
  }
1842
1860
  }
1843
1861
  //# sourceMappingURL=editor.js.map