@marimo-team/islands 0.25.2-dev99 → 0.25.3-dev141

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.
Files changed (158) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BZmqc_F0.js → ConnectedDataExplorerComponent-CNSxdKyJ.js} +15 -15
  2. package/dist/{ErrorBoundary-CXkr_IcB.js → ErrorBoundary-Cg59O3xL.js} +12 -12
  3. package/dist/{ImageComparisonComponent-Cpfz30OZ.js → ImageComparisonComponent-uU8FSKZE.js} +3 -3
  4. package/dist/{Plot-CTyjbQDT.js → Plot-DdYOe1UK.js} +2 -2
  5. package/dist/{abnfDiagram-VCTEODGH-C_Lmaa2j.js → abnfDiagram-VCTEODGH-DQDjNXt3.js} +3 -3
  6. package/dist/{any-language-editor-BFzqhOBR.js → any-language-editor-B9REGlD9.js} +7 -7
  7. package/dist/{architectureDiagram-5GKGNRK7-BKMataHB.js → architectureDiagram-5GKGNRK7-Nmls7Wxn.js} +5 -5
  8. package/dist/assets/__vite-browser-external-BXYkiFsk.js +1 -0
  9. package/dist/assets/worker-HPY9fbhz.js +114 -0
  10. package/dist/{blockDiagram-I7D4REHJ-BeHOjyDk.js → blockDiagram-I7D4REHJ-Cg25UrxT.js} +9 -9
  11. package/dist/{button-BtsM2UqM.js → button-CvjkwdSk.js} +5 -5
  12. package/dist/{c4Diagram-7LVT6UL2-Bao10t_O.js → c4Diagram-7LVT6UL2-BxFgrnNV.js} +7 -7
  13. package/dist/{capabilities-A5jrzjCJ.js → capabilities-CQ5IjhzL.js} +1 -1
  14. package/dist/{cells-DZi0ioak.js → cells-C8iCYvLU.js} +6644 -5604
  15. package/dist/{channel-BxCkvUgE.js → channel-CPObtJXj.js} +1 -1
  16. package/dist/{chat-ui-CwOWzoSO.js → chat-ui-BDpymeoe.js} +1589 -1396
  17. package/dist/{check-CNP79M3A.js → check-EDwSwP89.js} +1 -1
  18. package/dist/{chunk-4HAMMTFA-CH7Gpamd.js → chunk-4HAMMTFA-D4eFPyFh.js} +5 -5
  19. package/dist/{chunk-75Z2AOVW-AI7CvgKn.js → chunk-75Z2AOVW-D7gxIs2Q.js} +1 -1
  20. package/dist/{chunk-DU6HZSFF-BSvuTkX4.js → chunk-DU6HZSFF-BwiyaNpg.js} +1 -1
  21. package/dist/{chunk-F27PBJKO-CLshwT9T.js → chunk-F27PBJKO-ufseIn5y.js} +1 -1
  22. package/dist/{chunk-GMAD6QVW-wnHcn-lH.js → chunk-GMAD6QVW-BhaJx0X3.js} +3 -3
  23. package/dist/{chunk-GVQU2GXP-7O2p8hut.js → chunk-GVQU2GXP-Cwbt-lF0.js} +2 -2
  24. package/dist/{chunk-IMKFNOWR-Bu0Vh_Yi.js → chunk-IMKFNOWR-CoIFEo-1.js} +5 -5
  25. package/dist/{chunk-L3NEJ4N5-B9_Alrxb.js → chunk-L3NEJ4N5-n-zgIVSy.js} +5 -5
  26. package/dist/{chunk-OSK3NFVY-PeQPH2fD.js → chunk-OSK3NFVY-DXa-Cxbn.js} +6 -6
  27. package/dist/{chunk-P2QGCYS3-D9twiFuU.js → chunk-P2QGCYS3-B_Zd3-X0.js} +1 -1
  28. package/dist/{chunk-POPQ4Y6H-BUiNs18s.js → chunk-POPQ4Y6H-hYOXbYH_.js} +1 -1
  29. package/dist/{chunk-PWAF6VOD-D8S9NPCy.js → chunk-PWAF6VOD-CQw5qATZ.js} +1 -1
  30. package/dist/{chunk-SHT3W25Y-B2dyNZ31.js → chunk-SHT3W25Y-lHiGx3ZC.js} +7 -7
  31. package/dist/{chunk-SVP7TREG-D7UPvB10.js → chunk-SVP7TREG-BvsHIrY_.js} +2 -2
  32. package/dist/{chunk-TICWLB2K-BNaGoadd.js → chunk-TICWLB2K-BC8YIm_n.js} +5 -5
  33. package/dist/{chunk-YOKDWASO-DrWbKnxA.js → chunk-YOKDWASO-DPZk8vhS.js} +6 -6
  34. package/dist/classDiagram-ZZMXUADV-Bt98lkkv.js +30 -0
  35. package/dist/classDiagram-v2-VYDZK3BY-D9bfbcC3.js +30 -0
  36. package/dist/{code-visibility-pPjYsRTH.js → code-visibility-Da3iitn_.js} +8 -8
  37. package/dist/{common-CkvBp0B4.js → common-C6FHcu5p.js} +6574 -7877
  38. package/dist/{compiler-runtime-Cj8ZWiNS.js → compiler-runtime-CldRdSsv.js} +2 -2
  39. package/dist/{copy-icon-BY7jgcaV.js → copy-icon-Ft7t9bqz.js} +6 -6
  40. package/dist/{cynefinDiagram-5FMLGOSQ-CCDn4Z9e.js → cynefinDiagram-5FMLGOSQ-CgWra7wt.js} +3 -3
  41. package/dist/{dagre-GXQ25YYZ-BJiUO_SV.js → dagre-GXQ25YYZ-CFZ8-35c.js} +10 -10
  42. package/dist/{data-grid-overlay-editor-L6ELxFAh.js → data-grid-overlay-editor-mJt3Pg4S.js} +7 -7
  43. package/dist/{data-grid-types-BucVmDDp.js → data-grid-types-BH_r7g7l.js} +1 -1
  44. package/dist/{diagram-S7CK7UJ4-BZfxFNRR.js → diagram-S7CK7UJ4-lCNQrW7d.js} +4 -4
  45. package/dist/{diagram-UQ7AKVKN-1xcsiIMg.js → diagram-UQ7AKVKN-CQPQ6pP_.js} +3 -3
  46. package/dist/{diagram-VSXAHHWV-DPAWJ9Nw.js → diagram-VSXAHHWV-BJWcVVJY.js} +2 -2
  47. package/dist/{diagram-VX7I27RA-DmRsnHYY.js → diagram-VX7I27RA-Cf3b0llz.js} +5 -5
  48. package/dist/{diagram-Z3DM3KII--b49ryKx.js → diagram-Z3DM3KII-Bt4QLrpn.js} +3 -3
  49. package/dist/{dialog-CmExhUDI.js → dialog-CEBFZNyc.js} +6 -6
  50. package/dist/{dist-CcyKTBTF.js → dist-DpBpA_n1.js} +2 -2
  51. package/dist/{dropdown-menu-Cz4TdOJh.js → dropdown-menu-CP_qCYHC.js} +139 -139
  52. package/dist/{ebnfDiagram-PWID7BFC-rSwVyh4y.js → ebnfDiagram-PWID7BFC-D3NAyPFY.js} +3 -3
  53. package/dist/{erDiagram-RLTQ6QDP-Bfd3iAoC.js → erDiagram-RLTQ6QDP-Tlu09_Nm.js} +12 -12
  54. package/dist/{error-banner-D30nA9nE.js → error-banner-Da600cwz.js} +6 -6
  55. package/dist/{error-entries-BovkFYNy.js → error-entries-CzoYkIQZ.js} +2 -2
  56. package/dist/{esm-DAVrGVKP.js → esm-AMFssoNp.js} +4 -4
  57. package/dist/{feedback-button-D-pV0Qji.js → feedback-button-DaPwHIdM.js} +13 -13
  58. package/dist/flowDiagram-HODETNUW-CAYbpJmV.js +16 -0
  59. package/dist/{formats-DhhbdyAH.js → formats-BGh2thRc.js} +6 -6
  60. package/dist/{ganttDiagram-EL5Y4UJY-B4m_YsKa.js → ganttDiagram-EL5Y4UJY-BQT16JSc.js} +2 -2
  61. package/dist/{gitGraphDiagram-WWUBYQGX-QRBbvWXh.js → gitGraphDiagram-WWUBYQGX-D5nSIzdE.js} +2 -2
  62. package/dist/{glide-data-editor-D6CfYwdr.js → glide-data-editor-Ctz2mH6k.js} +60 -60
  63. package/dist/{glide-utils-BVJ4fkxX.js → glide-utils-DJyqh0EM.js} +4 -4
  64. package/dist/{globals-BwK9YGrX.js → globals-EpFoRj_E.js} +3 -3
  65. package/dist/{highlighted-body-KPVGNVTW-Ad3MKZ18.js → highlighted-body-KPVGNVTW-zwjq21tf.js} +4 -4
  66. package/dist/{html-to-image-Cn1_16-x.js → html-to-image-CZY1pIZH.js} +17 -17
  67. package/dist/{infoDiagram-27XIBGKW-DVQFYvhg.js → infoDiagram-27XIBGKW-XrDxhr0x.js} +2 -2
  68. package/dist/{ishikawaDiagram-5VMMS53U-viPND-nm.js → ishikawaDiagram-5VMMS53U-CD1muzov.js} +3 -3
  69. package/dist/{journeyDiagram-3NMN7TZE-BeeJdJo3.js → journeyDiagram-3NMN7TZE-BaLqn-dZ.js} +2 -2
  70. package/dist/{jsx-runtime-DgDPJV9S.js → jsx-runtime-3BD1kT0s.js} +1 -1
  71. package/dist/{kanban-definition-UXKFOSKX-Dcl_ECO9.js → kanban-definition-UXKFOSKX-Dg9W142_.js} +9 -9
  72. package/dist/{katex-BNCmKUol.js → katex-BIdZGsyO.js} +141 -116
  73. package/dist/{label-DQO1jFjf.js → label-BDKF3Afc.js} +10 -10
  74. package/dist/{loader-Cm-ATtou.js → loader-BQOfjTIJ.js} +1 -1
  75. package/dist/main.js +499 -467
  76. package/dist/{mermaid-BfhcnSj-.js → mermaid-CiAXjLpA.js} +56 -56
  77. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +2 -0
  78. package/dist/{mhchem-BEc7A1or.js → mhchem-CdWPJK6g.js} +1 -1
  79. package/dist/{mindmap-definition-YA3MSWOX-BdxxhF-5.js → mindmap-definition-YA3MSWOX-DgCidgYJ.js} +11 -11
  80. package/dist/{number-overlay-editor-tMylyldi.js → number-overlay-editor-CtxrummL.js} +5 -5
  81. package/dist/{pegDiagram-XKGWAZYB-Du5p2s0w.js → pegDiagram-XKGWAZYB-coOYYKB6.js} +3 -3
  82. package/dist/{pieDiagram-E7YTZNPT-DJg-SOIq.js → pieDiagram-E7YTZNPT-BnXTc3bj.js} +3 -3
  83. package/dist/{process-output-CUG57KRp.js → process-output-e4jj9Ijt.js} +4 -4
  84. package/dist/{quadrantDiagram-AXDQQJYC-keLfFx9f.js → quadrantDiagram-AXDQQJYC-Ccw0xMjN.js} +1 -1
  85. package/dist/{railroadDiagram-O6MQD6OU-C0ojm-6_.js → railroadDiagram-O6MQD6OU-B28-u83n.js} +3 -3
  86. package/dist/{react-dom-BUgJ-ab_.js → react-dom-DUmH0GXv.js} +29 -21
  87. package/dist/{react-ofJbxM4x.js → react-sHtZdjeP.js} +103 -94
  88. package/dist/{react-vega-ewj7q_U5.js → react-vega-BED0DjiB.js} +3 -3
  89. package/dist/{requirementDiagram-BXWQKSXE-CNwNBmI3.js → requirementDiagram-BXWQKSXE-CdYCTMu0.js} +11 -11
  90. package/dist/{reveal-component-LXUMcqWX.js → reveal-component-DrIXvR_7.js} +906 -878
  91. package/dist/{sankeyDiagram-P5KCCOFB-Bh4EsJox.js → sankeyDiagram-P5KCCOFB-ELvOcnZH.js} +1 -1
  92. package/dist/{select-Bs-nmvm6.js → select-BButFi-d.js} +11 -11
  93. package/dist/{sequenceDiagram-WJ2MYXX4-DQaW5qBG.js → sequenceDiagram-WJ2MYXX4-CM3aRAe_.js} +4 -4
  94. package/dist/{spec-DVsoMvYT.js → spec-BwarBpUJ.js} +4 -4
  95. package/dist/{stateDiagram-D77RDMKH-BDa515QC.js → stateDiagram-D77RDMKH-DOZUMVj8.js} +13 -13
  96. package/dist/stateDiagram-v2-MP3YSRHH-CISYxma6.js +30 -0
  97. package/dist/style.css +1 -1
  98. package/dist/{swimlanes-42K2YHIH-M4IdGi8J.js → swimlanes-42K2YHIH-BkSppxp8.js} +10 -10
  99. package/dist/swimlanesDiagram-VR7AAH4N-c8G5pGxC.js +30 -0
  100. package/dist/{swiper-component-CpN-hewZ.js → swiper-component-DKntyn8p.js} +5 -5
  101. package/dist/{timeline-definition-24CTP7MA-DH-kvakg.js → timeline-definition-24CTP7MA-CaMQayqK.js} +3 -3
  102. package/dist/{toDate-vEi5kTfY.js → toDate-BNJf01u1.js} +7 -7
  103. package/dist/{toast-DfEpJedt.js → toast-B9UMEi8u.js} +9 -9
  104. package/dist/{tooltip-CuNWg2ay.js → tooltip-BO1REbqs.js} +7 -7
  105. package/dist/{use-restore-focus-kmgwrL8q.js → use-restore-focus-BEM7sFWl.js} +2042 -1827
  106. package/dist/{use-vega-container-remeasure-4DwJbPWu.js → use-vega-container-remeasure-Bz4PZMtt.js} +2 -2
  107. package/dist/{useAsyncData-rkAlR_vh.js → useAsyncData-Deh5kszT.js} +3 -3
  108. package/dist/{useDateFormatter-C-tz65rD.js → useDateFormatter-D8XruNGG.js} +4 -4
  109. package/dist/{useDeepCompareMemoize-BlHFBj5_.js → useDeepCompareMemoize-DXwzRINI.js} +2 -2
  110. package/dist/{useIframeCapabilities-C1Bt04Iz.js → useIframeCapabilities-DBETvvPl.js} +2 -2
  111. package/dist/{useLifecycle-B68iY077.js → useLifecycle-j7D9uD1o.js} +6 -6
  112. package/dist/{useTheme-BwSgeAFb.js → useTheme-CRnjkku0.js} +7 -7
  113. package/dist/{vega-component-C-bPttRN.js → vega-component-DG29TAB7.js} +14 -14
  114. package/dist/{vega-embed-container-sSR5hAsy.js → vega-embed-container-BNHE18G9.js} +5 -5
  115. package/dist/{vennDiagram-4TSXK5OY-d5g7XeiI.js → vennDiagram-4TSXK5OY-CF8pfaK3.js} +3 -3
  116. package/dist/{wardleyDiagram-VM6X3IG4-BfX3bSzR.js → wardleyDiagram-VM6X3IG4-dGPu39d-.js} +3 -3
  117. package/dist/{xychartDiagram-S5SC5T6Z-C1QsLZg7.js → xychartDiagram-S5SC5T6Z-CP6-9q56.js} +5 -5
  118. package/package.json +21 -21
  119. package/src/components/data-table/__tests__/column_formatting.test.ts +30 -0
  120. package/src/components/data-table/__tests__/columns.test.tsx +70 -0
  121. package/src/components/data-table/column-formatting/feature.ts +20 -1
  122. package/src/components/data-table/columns.tsx +25 -18
  123. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +4 -3
  124. package/src/components/editor/output/console/ConsoleOutput.tsx +5 -3
  125. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +18 -0
  126. package/src/components/storage/__tests__/storage-file-viewer.test.tsx +2 -1
  127. package/src/core/cells/__tests__/document-roundtrip.test.ts +16 -0
  128. package/src/core/codemirror/copilot/__tests__/completion-integration.test.ts +447 -0
  129. package/src/core/codemirror/copilot/__tests__/inline-completion.test.ts +395 -0
  130. package/src/core/codemirror/copilot/__tests__/language-server.test.ts +511 -2
  131. package/src/core/codemirror/copilot/__tests__/rpc-cancellation.test.ts +201 -0
  132. package/src/core/codemirror/copilot/extension.ts +22 -11
  133. package/src/core/codemirror/copilot/language-server.ts +109 -60
  134. package/src/core/codemirror/language/__tests__/sql.test.ts +4 -3
  135. package/src/core/codemirror/language/embedded/__tests__/embedded-python.test.ts +138 -26
  136. package/src/core/codemirror/language/embedded/embedded-python.ts +47 -13
  137. package/src/core/codemirror/language/embedded/latex.ts +20 -13
  138. package/src/core/datasets/__tests__/data-source-discovery.test.ts +18 -11
  139. package/src/core/islands/bridge.ts +2 -2
  140. package/src/core/kernel/messages.ts +4 -7
  141. package/src/core/network/__tests__/api.test.ts +114 -1
  142. package/src/core/network/api.ts +3 -2
  143. package/src/core/wasm/bridge.ts +2 -2
  144. package/src/core/wasm/worker/types.ts +2 -2
  145. package/src/css/katex-css.test.ts +1 -1
  146. package/src/css/katex.min.css +6 -1
  147. package/src/hooks/__tests__/useEventListener.test.tsx +45 -1
  148. package/src/hooks/useEventListener.ts +6 -1
  149. package/src/plugins/impl/NumberPlugin.tsx +20 -3
  150. package/src/plugins/impl/__tests__/NumberPlugin.test.tsx +159 -1
  151. package/dist/assets/__vite-browser-external-BqAtzd2P.js +0 -1
  152. package/dist/assets/worker-Bf8dt6S-.js +0 -109
  153. package/dist/classDiagram-ZZMXUADV-Bbo2n384.js +0 -30
  154. package/dist/classDiagram-v2-VYDZK3BY-xR8cGP0r.js +0 -30
  155. package/dist/flowDiagram-HODETNUW-D7IBKS12.js +0 -16
  156. package/dist/mermaid-HWGCJPDP-DYCzSILp.js +0 -2
  157. package/dist/stateDiagram-v2-MP3YSRHH-ChNddepZ.js +0 -30
  158. package/dist/swimlanesDiagram-VR7AAH4N-FUSwmzEp.js +0 -30
@@ -0,0 +1,395 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ EditorSelection,
5
+ EditorState,
6
+ type TransactionSpec,
7
+ } from "@codemirror/state";
8
+ import { EditorView } from "@codemirror/view";
9
+ import {
10
+ acceptInlineCompletion,
11
+ inlineCompletion,
12
+ rejectInlineCompletion,
13
+ } from "@marimo-team/codemirror-ai";
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
+
16
+ // Exercise the installed dependency to catch regressions in upstream releases.
17
+ describe("inline completion lifecycle", () => {
18
+ let view: EditorView;
19
+ const fetchFn =
20
+ vi.fn<
21
+ (
22
+ state: EditorState,
23
+ signal: AbortSignal,
24
+ view: EditorView,
25
+ ) => Promise<string>
26
+ >();
27
+
28
+ function create(
29
+ options: Partial<Parameters<typeof inlineCompletion>[0]> = {},
30
+ ) {
31
+ view = new EditorView({
32
+ parent: document.body,
33
+ state: EditorState.create({
34
+ doc: "pri",
35
+ selection: { anchor: 3 },
36
+ extensions: [
37
+ EditorState.allowMultipleSelections.of(true),
38
+ inlineCompletion({ delay: 150, fetchFn, ...options }),
39
+ ],
40
+ }),
41
+ });
42
+ }
43
+
44
+ function type(text: string) {
45
+ view.dispatch({
46
+ ...view.state.replaceSelection(text),
47
+ userEvent: "input.type",
48
+ });
49
+ }
50
+
51
+ function suggestion() {
52
+ return view.dom.querySelector(".cm-inline-suggestion")?.textContent ?? null;
53
+ }
54
+
55
+ async function show(text = 't("hello")') {
56
+ fetchFn.mockResolvedValue(text);
57
+ type("n");
58
+ await vi.advanceTimersByTimeAsync(150);
59
+ expect(suggestion()).toBe(text);
60
+ }
61
+
62
+ beforeEach(() => {
63
+ vi.useFakeTimers();
64
+ fetchFn.mockReset().mockResolvedValue('t("hello")');
65
+ });
66
+
67
+ afterEach(() => {
68
+ view?.destroy();
69
+ vi.useRealTimers();
70
+ vi.restoreAllMocks();
71
+ });
72
+
73
+ it("debounces bursts and fetches only the latest document at 150ms", async () => {
74
+ create();
75
+ type("n");
76
+ await vi.advanceTimersByTimeAsync(149);
77
+ expect(fetchFn).not.toHaveBeenCalled();
78
+ type("t");
79
+ await vi.advanceTimersByTimeAsync(149);
80
+ expect(fetchFn).not.toHaveBeenCalled();
81
+ const state = view.state;
82
+ await vi.advanceTimersByTimeAsync(1);
83
+ expect(fetchFn).toHaveBeenCalledExactlyOnceWith(
84
+ state,
85
+ expect.objectContaining({ aborted: false }),
86
+ view,
87
+ );
88
+ });
89
+
90
+ it.each(["t", 't("', 't("hello'])(
91
+ "retains the suffix immediately after matching input %j",
92
+ async (text) => {
93
+ create();
94
+ await show();
95
+ type(text);
96
+ expect(suggestion()).toBe('t("hello")'.slice(text.length));
97
+ await vi.advanceTimersByTimeAsync(1000);
98
+ expect(fetchFn).toHaveBeenCalledTimes(1);
99
+ expect(acceptInlineCompletion(view)).toBe(true);
100
+ expect(view.state.doc.toString()).toBe('print("hello")');
101
+ },
102
+ );
103
+
104
+ it("preserves multiline and UTF-16 suggestions", async () => {
105
+ create();
106
+ await show("😀\nnext()");
107
+ type("😀\n");
108
+ expect(suggestion()).toBe("next()");
109
+ expect(fetchFn).toHaveBeenCalledTimes(1);
110
+ });
111
+
112
+ it("fetches a continuation when the suggestion is fully typed", async () => {
113
+ create();
114
+ await show("t");
115
+ fetchFn.mockResolvedValue("(value)");
116
+ type("t");
117
+ expect(suggestion()).toBeNull();
118
+ await vi.advanceTimersByTimeAsync(150);
119
+ expect(suggestion()).toBe("(value)");
120
+ expect(fetchFn).toHaveBeenCalledTimes(2);
121
+ });
122
+
123
+ it.each<[string, TransactionSpec]>([
124
+ [
125
+ "mismatch",
126
+ { changes: { from: 4, insert: "x" }, selection: { anchor: 5 } },
127
+ ],
128
+ ["deletion", { changes: { from: 3, to: 4 }, selection: { anchor: 3 } }],
129
+ [
130
+ "replacement",
131
+ { changes: { from: 3, to: 4, insert: "t" }, selection: { anchor: 4 } },
132
+ ],
133
+ [
134
+ "edit away from cursor",
135
+ { changes: { from: 0, insert: "t" }, selection: { anchor: 5 } },
136
+ ],
137
+ [
138
+ "multiple edits",
139
+ {
140
+ changes: [
141
+ { from: 0, insert: "x" },
142
+ { from: 4, insert: "t" },
143
+ ],
144
+ selection: { anchor: 6 },
145
+ },
146
+ ],
147
+ [
148
+ "cursor moved after insertion",
149
+ { changes: { from: 4, insert: "t" }, selection: { anchor: 0 } },
150
+ ],
151
+ ])("discards a suggestion after %s", async (_label, transaction) => {
152
+ create();
153
+ await show();
154
+ view.dispatch(transaction);
155
+ expect(suggestion()).toBeNull();
156
+ await vi.advanceTimersByTimeAsync(150);
157
+ expect(fetchFn).toHaveBeenCalledTimes(2);
158
+ });
159
+
160
+ it("does not reuse suggestions or fetch for multiple cursors", async () => {
161
+ create();
162
+ await show();
163
+ view.dispatch({
164
+ selection: EditorSelection.create(
165
+ [EditorSelection.cursor(0), EditorSelection.cursor(4)],
166
+ 1,
167
+ ),
168
+ });
169
+ type("t");
170
+ expect(view.state.doc.toString()).toBe("tprint");
171
+ expect(view.state.selection.toJSON()).toEqual(
172
+ EditorSelection.create(
173
+ [EditorSelection.cursor(1), EditorSelection.cursor(6)],
174
+ 1,
175
+ ).toJSON(),
176
+ );
177
+ await vi.advanceTimersByTimeAsync(150);
178
+ expect(suggestion()).toBeNull();
179
+ expect(fetchFn).toHaveBeenCalledTimes(1);
180
+ });
181
+
182
+ it("does not fetch when an edit leaves a nonempty selection", async () => {
183
+ create();
184
+ view.dispatch({
185
+ changes: { from: 3, insert: "n" },
186
+ selection: { anchor: 0, head: 4 },
187
+ });
188
+ await vi.advanceTimersByTimeAsync(150);
189
+ expect(fetchFn).not.toHaveBeenCalled();
190
+ expect(suggestion()).toBeNull();
191
+ });
192
+
193
+ it("cancels a deferred cache hit before it can display at a new cursor", async () => {
194
+ create();
195
+ await show();
196
+ view.dispatch({ changes: { from: 3, to: 4 }, selection: { anchor: 3 } });
197
+ type("n");
198
+ view.dispatch({ selection: { anchor: 0 } });
199
+ await vi.advanceTimersByTimeAsync(150);
200
+ expect(fetchFn).toHaveBeenCalledTimes(1);
201
+ expect(suggestion()).toBeNull();
202
+ });
203
+
204
+ it("ignores a late failure from a cancelled request without hiding its replacement", async () => {
205
+ create();
206
+ const error = vi
207
+ .spyOn(console, "error")
208
+ .mockImplementation(() => undefined);
209
+ const old = Promise.withResolvers<string>();
210
+ fetchFn.mockReturnValueOnce(old.promise).mockResolvedValueOnce("(new)");
211
+ type("n");
212
+ await vi.advanceTimersByTimeAsync(150);
213
+ type("t");
214
+ await vi.advanceTimersByTimeAsync(150);
215
+ expect(suggestion()).toBe("(new)");
216
+ old.reject(new Error("old connection failed"));
217
+ await vi.advanceTimersByTimeAsync(0);
218
+ expect(suggestion()).toBe("(new)");
219
+ expect(error).not.toHaveBeenCalled();
220
+ });
221
+
222
+ it("cancels scheduled work when the cursor moves", async () => {
223
+ create();
224
+ type("n");
225
+ view.dispatch({ selection: { anchor: 0 } });
226
+ await vi.advanceTimersByTimeAsync(150);
227
+ expect(fetchFn).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it("aborts superseded work and ignores an out-of-order response", async () => {
231
+ create();
232
+ const old = Promise.withResolvers<string>();
233
+ const latest = Promise.withResolvers<string>();
234
+ fetchFn
235
+ .mockReturnValueOnce(old.promise)
236
+ .mockReturnValueOnce(latest.promise);
237
+ type("n");
238
+ await vi.advanceTimersByTimeAsync(150);
239
+ const oldSignal = fetchFn.mock.calls[0][1];
240
+ type("t");
241
+ expect(oldSignal.aborted).toBe(true);
242
+ await vi.advanceTimersByTimeAsync(150);
243
+ latest.resolve("(new)");
244
+ await vi.advanceTimersByTimeAsync(0);
245
+ old.resolve("stale");
246
+ await vi.advanceTimersByTimeAsync(0);
247
+ expect(suggestion()).toBe("(new)");
248
+ });
249
+
250
+ it("ignores a response after moving away and back to the same cursor", async () => {
251
+ create();
252
+ const result = Promise.withResolvers<string>();
253
+ fetchFn.mockReturnValue(result.promise);
254
+ type("n");
255
+ await vi.advanceTimersByTimeAsync(150);
256
+ view.dispatch({ selection: { anchor: 0 } });
257
+ view.dispatch({ selection: { anchor: 4 } });
258
+ result.resolve("stale");
259
+ await vi.advanceTimersByTimeAsync(0);
260
+ expect(suggestion()).toBeNull();
261
+ expect(fetchFn.mock.calls[0][1].aborted).toBe(true);
262
+ });
263
+
264
+ it.each([false, true])(
265
+ "cancels on destruction (already fetching: %s)",
266
+ async (started) => {
267
+ create();
268
+ const result = Promise.withResolvers<string>();
269
+ fetchFn.mockReturnValue(result.promise);
270
+ type("n");
271
+ if (started) {
272
+ await vi.advanceTimersByTimeAsync(150);
273
+ }
274
+ view.destroy();
275
+ result.resolve("stale");
276
+ await vi.advanceTimersByTimeAsync(200);
277
+ expect(fetchFn).toHaveBeenCalledTimes(started ? 1 : 0);
278
+ expect(fetchFn.mock.calls[0]?.[1].aborted).toBe(
279
+ started ? true : undefined,
280
+ );
281
+ },
282
+ );
283
+
284
+ it("recovers after a failed request", async () => {
285
+ create();
286
+ const error = vi
287
+ .spyOn(console, "error")
288
+ .mockImplementation(() => undefined);
289
+ fetchFn.mockRejectedValueOnce(new Error("offline"));
290
+ type("n");
291
+ await vi.advanceTimersByTimeAsync(150);
292
+ expect(suggestion()).toBeNull();
293
+ expect(error).toHaveBeenCalledTimes(1);
294
+ type("t");
295
+ await vi.advanceTimersByTimeAsync(150);
296
+ expect(suggestion()).toBe('t("hello")');
297
+ });
298
+
299
+ it("does not log an expected abort rejection", async () => {
300
+ create();
301
+ const error = vi
302
+ .spyOn(console, "error")
303
+ .mockImplementation(() => undefined);
304
+ fetchFn.mockRejectedValueOnce(new DOMException("aborted", "AbortError"));
305
+ type("n");
306
+ await vi.advanceTimersByTimeAsync(150);
307
+ expect(error).not.toHaveBeenCalled();
308
+ expect(suggestion()).toBeNull();
309
+ });
310
+
311
+ it("respects fetch/display gates and Escape rejection", async () => {
312
+ const before = vi.fn(() => false);
313
+ const display = vi.fn(() => false);
314
+ create({
315
+ events: { beforeSuggestionFetch: before, shouldShowSuggestion: display },
316
+ });
317
+ type("n");
318
+ await vi.advanceTimersByTimeAsync(150);
319
+ expect(fetchFn).not.toHaveBeenCalled();
320
+ before.mockReturnValue(true);
321
+ type("t");
322
+ await vi.advanceTimersByTimeAsync(150);
323
+ expect(suggestion()).toBeNull();
324
+ display.mockReturnValue(true);
325
+ type("(");
326
+ await vi.advanceTimersByTimeAsync(150);
327
+ expect(rejectInlineCompletion(view)).toBe(true);
328
+ expect(suggestion()).toBeNull();
329
+ expect(acceptInlineCompletion(view)).toBe(false);
330
+ });
331
+
332
+ it("reuses a cached result without dispatching during a view update", async () => {
333
+ create();
334
+ await show();
335
+ view.dispatch({ changes: { from: 3, to: 4 }, selection: { anchor: 3 } });
336
+ type("n");
337
+ expect(() => view.dispatch({})).not.toThrow();
338
+ await vi.advanceTimersByTimeAsync(0);
339
+ expect(suggestion()).toBe('t("hello")');
340
+ expect(fetchFn).toHaveBeenCalledTimes(1);
341
+ });
342
+
343
+ it("does not reuse the same document's cached suggestion at a different cursor", async () => {
344
+ create();
345
+ await show();
346
+ view.dispatch({ changes: { from: 3, to: 4 }, selection: { anchor: 3 } });
347
+ view.dispatch({
348
+ changes: { from: 3, insert: "n" },
349
+ selection: { anchor: 0 },
350
+ });
351
+ await vi.advanceTimersByTimeAsync(150);
352
+ expect(fetchFn).toHaveBeenCalledTimes(2);
353
+ expect(fetchFn.mock.calls[1][0].selection.main.head).toBe(0);
354
+ });
355
+
356
+ it("expires cached results", async () => {
357
+ create({ cacheTimeout: 100 });
358
+ await show();
359
+ await vi.advanceTimersByTimeAsync(101);
360
+ view.dispatch({ changes: { from: 3, to: 4 }, selection: { anchor: 3 } });
361
+ type("n");
362
+ await vi.advanceTimersByTimeAsync(150);
363
+ expect(fetchFn).toHaveBeenCalledTimes(2);
364
+ });
365
+ it("bounds the cache instead of retaining every document ever typed", async () => {
366
+ create({ delay: 0, cacheTimeout: 1_000_000 });
367
+ await show();
368
+ for (let i = 0; i < 21; i++) {
369
+ const text = `entry${i}`;
370
+ view.dispatch({
371
+ changes: { from: 0, to: view.state.doc.length, insert: text },
372
+ selection: { anchor: text.length },
373
+ });
374
+ await vi.advanceTimersByTimeAsync(0);
375
+ }
376
+ view.dispatch({
377
+ changes: { from: 0, to: view.state.doc.length, insert: "prin" },
378
+ selection: { anchor: 4 },
379
+ });
380
+ await vi.advanceTimersByTimeAsync(0);
381
+ expect(fetchFn).toHaveBeenCalledTimes(23);
382
+ });
383
+
384
+ it("does not cache empty results", async () => {
385
+ create();
386
+ fetchFn.mockResolvedValueOnce("");
387
+ type("n");
388
+ await vi.advanceTimersByTimeAsync(150);
389
+ view.dispatch({ changes: { from: 3, to: 4 }, selection: { anchor: 3 } });
390
+ type("n");
391
+ await vi.advanceTimersByTimeAsync(150);
392
+ expect(fetchFn).toHaveBeenCalledTimes(2);
393
+ expect(suggestion()).toBe('t("hello")');
394
+ });
395
+ });