@marimo-team/islands 0.25.2-dev97 → 0.25.2

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-B-t5rRT6.js → ErrorBoundary-HusVNio-.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-B_vyV9G1.js → code-visibility-4aW5326K.js} +8 -8
  37. package/dist/{common-DrnmK6Aw.js → common-Cjo7bjRK.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-CauDs7BG.js → data-grid-overlay-editor-CvdQXfSk.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-D7yOYYPr.js → feedback-button-ZuLnINhC.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-BVIswvcw.js → glide-data-editor-qM9FSJA9.js} +60 -60
  63. package/dist/{glide-utils-BVJ4fkxX.js → glide-utils-DJyqh0EM.js} +4 -4
  64. package/dist/{globals-oj-cWKZj.js → globals-Ci7B9t6C.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-CS7fiUj7.js → number-overlay-editor-DunDxAzh.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-taGJ10XF.js → reveal-component-A7HedxmE.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 +23 -22
  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 +166 -0
  146. package/src/css/katex.min.css +33 -28
  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
@@ -10,6 +10,7 @@ import {
10
10
  import { Provider } from "jotai";
11
11
  import type React from "react";
12
12
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+ import { requestId } from "@/__tests__/branded";
13
14
  import { MockRequestClient } from "@/__mocks__/requests";
14
15
  import { ModalProvider } from "@/components/modal/ImperativeModal";
15
16
  import { TooltipProvider } from "@/components/ui/tooltip";
@@ -129,7 +130,7 @@ describe("NotebookMenuDropdown", () => {
129
130
  }));
130
131
  invalidateDataSourceDiscovery();
131
132
  vi.spyOn(DiscoverDataSources, "request").mockResolvedValue({
132
- request_id: "request-id",
133
+ request_id: requestId("request-id"),
133
134
  sources: [],
134
135
  });
135
136
  });
@@ -142,7 +143,7 @@ describe("NotebookMenuDropdown", () => {
142
143
 
143
144
  it("shows detected databases in the add connection submenu", async () => {
144
145
  vi.mocked(DiscoverDataSources.request).mockResolvedValue({
145
- request_id: "request-id",
146
+ request_id: requestId("request-id"),
146
147
  sources: [POSTGRES_SOURCE],
147
148
  });
148
149
 
@@ -172,7 +173,7 @@ describe("NotebookMenuDropdown", () => {
172
173
 
173
174
  it("shows detected storage in the add remote storage submenu", async () => {
174
175
  vi.mocked(DiscoverDataSources.request).mockResolvedValue({
175
- request_id: "request-id",
176
+ request_id: requestId("request-id"),
176
177
  sources: [S3_SOURCE],
177
178
  });
178
179
 
@@ -360,10 +360,12 @@ const StdInput = (props: {
360
360
  }
361
361
 
362
362
  if (e.key === "Enter" && !e.shiftKey) {
363
- if (value && !isPassword) {
364
- addToHistory(value);
363
+ // Submit the visible input even if its change has not committed yet.
364
+ const submittedValue = e.currentTarget.value;
365
+ if (submittedValue && !isPassword) {
366
+ addToHistory(submittedValue);
365
367
  }
366
- onSubmit(value);
368
+ onSubmit(submittedValue);
367
369
  setValue("");
368
370
  e.preventDefault();
369
371
  e.stopPropagation();
@@ -179,6 +179,24 @@ describe("ConsoleOutput pdb history", () => {
179
179
  expect(nextInput).toHaveValue("");
180
180
  });
181
181
 
182
+ it("submits the visible input when its change has not reached React state", () => {
183
+ const onSubmitDebugger = vi.fn();
184
+ renderWithProvider(
185
+ <ConsoleOutput
186
+ {...defaultProps}
187
+ consoleOutputs={[stdinPrompt("Name: ")]}
188
+ onSubmitDebugger={onSubmitDebugger}
189
+ />,
190
+ );
191
+
192
+ const input = screen.getByTestId<HTMLInputElement>("console-input");
193
+ // Browser autofill or input events can precede the controlled-state update.
194
+ input.value = "marimo";
195
+ fireEvent.keyDown(input, { key: "Enter" });
196
+
197
+ expect(onSubmitDebugger).toHaveBeenCalledWith("marimo", 0);
198
+ });
199
+
182
200
  it("should submit an empty string when Enter is pressed with no input", () => {
183
201
  // Many CLIs prompt "Press Enter to continue" and expect "" back.
184
202
  const onSubmitDebugger = vi.fn();
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { render, screen } from "@testing-library/react";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { requestId } from "@/__tests__/branded";
5
6
  import { TooltipProvider } from "@/components/ui/tooltip";
6
7
  import { DownloadStorage } from "@/core/storage/request-registry";
7
8
  import { StorageFileViewer } from "../storage-file-viewer";
@@ -41,7 +42,7 @@ describe("StorageFileViewer", () => {
41
42
 
42
43
  it("renders TOML from generic storage metadata as read-only TOML", async () => {
43
44
  const request = vi.spyOn(DownloadStorage, "request").mockResolvedValue({
44
- request_id: "request-id",
45
+ request_id: requestId("request-id"),
45
46
  url: "https://example.com/pyproject.toml?signature=secret",
46
47
  });
47
48
  vi.stubGlobal(
@@ -178,6 +178,22 @@ function documentSnapshot(state: NotebookState) {
178
178
  }
179
179
 
180
180
  describe("document round-trip", () => {
181
+ it("applies a server transaction that deletes every cell", () => {
182
+ setup("a", "b");
183
+ applyTransactionChanges(
184
+ [
185
+ ...replica.cellIds.inOrderIds.map((cellId) => ({
186
+ type: "delete-cell" as const,
187
+ cellId,
188
+ })),
189
+ { type: "reorder-cells", cellIds: [] },
190
+ ],
191
+ replicaActions,
192
+ () => replica.cellIds.inOrderIds,
193
+ );
194
+ expect(documentSnapshot(replica)).toEqual([]);
195
+ });
196
+
181
197
  it("initial setup converges", () => {
182
198
  setup("a", "b", "c");
183
199
  expect(documentSnapshot(primary)).toEqual(documentSnapshot(replica));
@@ -0,0 +1,447 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { EditorView } from "@codemirror/view";
4
+ import {
5
+ languageServerWithClient,
6
+ type JSONRPCMessage,
7
+ type JSONRPCRequest,
8
+ type Transport,
9
+ } from "@marimo-team/codemirror-languageserver";
10
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
+ import { API } from "@/core/network/api";
12
+ import { languageAdapterState } from "../../language/extension";
13
+ import { copilotBundle } from "../extension";
14
+ import { getCodes } from "../getCodes";
15
+ import { CopilotLanguageServerClient } from "../language-server";
16
+
17
+ const fixture: { client?: CopilotLanguageServerClient } = vi.hoisted(
18
+ () => ({}),
19
+ );
20
+
21
+ vi.mock("../client", () => ({
22
+ COPILOT_FILENAME: "/__marimo_copilot__.py",
23
+ getCopilotClient: () => fixture.client,
24
+ copilotServer: () =>
25
+ languageServerWithClient({
26
+ client: fixture.client!,
27
+ documentUri: "file:///__marimo_copilot__.py",
28
+ languageId: "python",
29
+ sendIncrementalChanges: false,
30
+ hoverEnabled: false,
31
+ completionEnabled: false,
32
+ diagnosticsEnabled: false,
33
+ signatureHelpEnabled: false,
34
+ }),
35
+ }));
36
+ vi.mock("../state", async (importOriginal) => ({
37
+ ...(await importOriginal<typeof import("../state")>()),
38
+ isCopilotEnabled: () => true,
39
+ }));
40
+ vi.mock("../getCodes", () => ({
41
+ getCodes: vi.fn((code: string) => `import math\n\n${code}`),
42
+ }));
43
+
44
+ describe("Copilot editor integration", () => {
45
+ let view: EditorView;
46
+ let client: CopilotLanguageServerClient;
47
+ let emit: (message: JSONRPCMessage) => void;
48
+ let sent: JSONRPCMessage[];
49
+
50
+ function requests() {
51
+ return sent.filter(
52
+ (message): message is JSONRPCRequest =>
53
+ "id" in message &&
54
+ "method" in message &&
55
+ message.method === "textDocument/inlineCompletion",
56
+ );
57
+ }
58
+ function type(text: string) {
59
+ const from = view.state.selection.main.head;
60
+ view.dispatch({
61
+ changes: { from, insert: text },
62
+ selection: { anchor: from + text.length },
63
+ });
64
+ }
65
+ function respond(text: string) {
66
+ emit({
67
+ jsonrpc: "2.0",
68
+ id: requests().at(-1)!.id,
69
+ result: [{ insertText: text }],
70
+ });
71
+ }
72
+
73
+ function useCustomEditor() {
74
+ view.destroy();
75
+ view = new EditorView({
76
+ parent: document.body,
77
+ doc: "pri",
78
+ selection: { anchor: 3 },
79
+ extensions: [
80
+ languageAdapterState,
81
+ copilotBundle({
82
+ copilot: "custom",
83
+ activate_on_typing: true,
84
+ signature_hint_on_typing: false,
85
+ }),
86
+ ],
87
+ });
88
+ vi.spyOn(view, "hasFocus", "get").mockReturnValue(true);
89
+ }
90
+
91
+ beforeEach(async () => {
92
+ vi.useFakeTimers();
93
+ // copilotBundle intentionally disables itself in ordinary unit tests.
94
+ vi.stubEnv("NODE_ENV", "development");
95
+ sent = [];
96
+ const transport: Transport = {
97
+ connect: vi.fn().mockResolvedValue(undefined),
98
+ close: vi.fn(),
99
+ onMessage: (handler) => {
100
+ emit = handler;
101
+ return vi.fn();
102
+ },
103
+ send: (message) => {
104
+ sent.push(message);
105
+ if (
106
+ "id" in message &&
107
+ "method" in message &&
108
+ ["initialize", "shutdown"].includes(message.method)
109
+ ) {
110
+ emit({
111
+ jsonrpc: "2.0",
112
+ id: message.id,
113
+ result:
114
+ message.method === "initialize"
115
+ ? { capabilities: { textDocumentSync: 1 } }
116
+ : null,
117
+ });
118
+ }
119
+ },
120
+ };
121
+ client = new CopilotLanguageServerClient({
122
+ rootUri: "file:///",
123
+ workspaceFolders: null,
124
+ transport,
125
+ });
126
+ fixture.client = client;
127
+ await client.initializePromise;
128
+ view = new EditorView({
129
+ parent: document.body,
130
+ doc: "pri",
131
+ selection: { anchor: 3 },
132
+ extensions: copilotBundle({
133
+ copilot: "github",
134
+ activate_on_typing: true,
135
+ signature_hint_on_typing: false,
136
+ }),
137
+ });
138
+ vi.spyOn(view, "hasFocus", "get").mockReturnValue(true);
139
+ await vi.advanceTimersByTimeAsync(0);
140
+ sent.length = 0;
141
+ vi.mocked(getCodes).mockClear();
142
+ });
143
+
144
+ afterEach(() => {
145
+ view.destroy();
146
+ client.close();
147
+ fixture.client = undefined;
148
+ vi.useRealTimers();
149
+ vi.unstubAllEnvs();
150
+ vi.restoreAllMocks();
151
+ });
152
+
153
+ it("uploads once and requests the matching version/position at 150ms without an extra sleep", async () => {
154
+ type("n");
155
+ type("t");
156
+ await vi.advanceTimersByTimeAsync(149);
157
+ expect(sent).toEqual([]);
158
+ expect(getCodes).not.toHaveBeenCalled();
159
+ await vi.advanceTimersByTimeAsync(1);
160
+ expect(getCodes).toHaveBeenCalledExactlyOnceWith("print");
161
+ expect(sent).toEqual([
162
+ {
163
+ jsonrpc: "2.0",
164
+ method: "textDocument/didChange",
165
+ params: {
166
+ textDocument: { uri: "file:///__marimo_copilot__.py", version: 2 },
167
+ contentChanges: [{ text: "import math\n\nprint" }],
168
+ },
169
+ },
170
+ {
171
+ jsonrpc: "2.0",
172
+ id: expect.any(Number),
173
+ method: "textDocument/inlineCompletion",
174
+ params: {
175
+ textDocument: { uri: "file:///__marimo_copilot__.py", version: 2 },
176
+ position: { line: 2, character: 5 },
177
+ context: { triggerKind: 2 },
178
+ formattingOptions: { tabSize: 4, insertSpaces: true },
179
+ },
180
+ },
181
+ ]);
182
+ respond("(value)");
183
+ await vi.advanceTimersByTimeAsync(0);
184
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
185
+ "(value)",
186
+ );
187
+ type("(");
188
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
189
+ "value)",
190
+ );
191
+ await vi.advanceTimersByTimeAsync(500);
192
+ expect(requests()).toHaveLength(1);
193
+ expect(getCodes).toHaveBeenCalledTimes(1);
194
+ type("x");
195
+ await vi.advanceTimersByTimeAsync(150);
196
+ expect(getCodes).toHaveBeenLastCalledWith("print(x");
197
+ expect(requests()).toHaveLength(2);
198
+ respond(")");
199
+ await vi.advanceTimersByTimeAsync(0);
200
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
201
+ ")",
202
+ );
203
+ });
204
+
205
+ it("never uploads a superseded snapshot when typing restarts the debounce", async () => {
206
+ const flush = vi.spyOn(client, "flushDocumentChanges");
207
+ type("n");
208
+ await vi.advanceTimersByTimeAsync(149);
209
+ type("t");
210
+ await vi.advanceTimersByTimeAsync(149);
211
+ expect(flush).not.toHaveBeenCalled();
212
+ expect(getCodes).not.toHaveBeenCalled();
213
+ expect(sent).toEqual([]);
214
+ await vi.advanceTimersByTimeAsync(1);
215
+ expect(flush.mock.calls.filter(([code]) => code !== undefined)).toEqual([
216
+ ["print"],
217
+ ]);
218
+ expect(getCodes).toHaveBeenCalledExactlyOnceWith("print");
219
+ expect(
220
+ sent.filter(
221
+ (message) =>
222
+ "method" in message && message.method === "textDocument/didChange",
223
+ ),
224
+ ).toEqual([
225
+ {
226
+ jsonrpc: "2.0",
227
+ method: "textDocument/didChange",
228
+ params: {
229
+ textDocument: { uri: "file:///__marimo_copilot__.py", version: 2 },
230
+ contentChanges: [{ text: "import math\n\nprint" }],
231
+ },
232
+ },
233
+ ]);
234
+ expect(requests()).toHaveLength(1);
235
+ respond("(latest)");
236
+ await vi.advanceTimersByTimeAsync(0);
237
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
238
+ "(latest)",
239
+ );
240
+ });
241
+
242
+ it("does not flush a cancelled callback after moving away and back before its deadline", async () => {
243
+ const flush = vi.spyOn(client, "flushDocumentChanges");
244
+ type("n");
245
+ await vi.advanceTimersByTimeAsync(149);
246
+ view.dispatch({ selection: { anchor: 0 } });
247
+ view.dispatch({ selection: { anchor: 4 } });
248
+ await vi.advanceTimersByTimeAsync(1000);
249
+ expect(flush).not.toHaveBeenCalled();
250
+ expect(getCodes).not.toHaveBeenCalled();
251
+ expect(sent).toEqual([]);
252
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toBeNull();
253
+ });
254
+
255
+ it("cancels on cursor movement and never shows an old reply", async () => {
256
+ type("n");
257
+ await vi.advanceTimersByTimeAsync(150);
258
+ view.dispatch({ selection: { anchor: 0 } });
259
+ await vi.advanceTimersByTimeAsync(0);
260
+ expect(sent.at(-1)).toEqual({
261
+ jsonrpc: "2.0",
262
+ method: "$/cancelRequest",
263
+ params: { id: requests()[0].id },
264
+ });
265
+ respond("stale");
266
+ await vi.advanceTimersByTimeAsync(0);
267
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toBeNull();
268
+ });
269
+
270
+ it("cancels an in-flight suggestion on typing before requesting the next version", async () => {
271
+ type("n");
272
+ await vi.advanceTimersByTimeAsync(150);
273
+ type("t");
274
+ await vi.advanceTimersByTimeAsync(0);
275
+ expect(sent.at(-1)).toEqual({
276
+ jsonrpc: "2.0",
277
+ method: "$/cancelRequest",
278
+ params: { id: requests()[0].id },
279
+ });
280
+ await vi.advanceTimersByTimeAsync(150);
281
+ respond("(new)");
282
+ await vi.advanceTimersByTimeAsync(0);
283
+ emit({
284
+ jsonrpc: "2.0",
285
+ id: requests()[0].id,
286
+ result: [{ insertText: "stale" }],
287
+ });
288
+ await vi.advanceTimersByTimeAsync(0);
289
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
290
+ "(new)",
291
+ );
292
+ });
293
+ it("does not display a response after focus is lost", async () => {
294
+ type("n");
295
+ await vi.advanceTimersByTimeAsync(150);
296
+ vi.spyOn(view, "hasFocus", "get").mockReturnValue(false);
297
+ respond("stale");
298
+ await vi.advanceTimersByTimeAsync(0);
299
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toBeNull();
300
+ });
301
+
302
+ it("passes cancellation through to custom provider HTTP requests", async () => {
303
+ const result = Promise.withResolvers<string>();
304
+ const post = vi.spyOn(API, "post").mockReturnValue(result.promise);
305
+ useCustomEditor();
306
+ type("n");
307
+ await vi.advanceTimersByTimeAsync(150);
308
+ expect(post).toHaveBeenCalledExactlyOnceWith(
309
+ "/ai/inline_completion",
310
+ { prefix: "prin", suffix: "", language: "python" },
311
+ { signal: expect.objectContaining({ aborted: false }) },
312
+ );
313
+ const signal = post.mock.calls[0][2]?.signal;
314
+ type("t");
315
+ expect(signal?.aborted).toBe(true);
316
+ result.resolve("stale");
317
+ await vi.advanceTimersByTimeAsync(0);
318
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toBeNull();
319
+ });
320
+ it.each(["HTTP failure", "AbortError"])(
321
+ "recovers on the next edit after custom provider %s",
322
+ async (failure) => {
323
+ useCustomEditor();
324
+ const error = vi
325
+ .spyOn(console, "error")
326
+ .mockImplementation(() => undefined);
327
+ const post = vi
328
+ .spyOn(API, "post")
329
+ .mockRejectedValueOnce(
330
+ failure === "AbortError"
331
+ ? new DOMException("aborted", "AbortError")
332
+ : new Error("HTTP 503"),
333
+ )
334
+ .mockResolvedValueOnce("(recovered)");
335
+ type("n");
336
+ await vi.advanceTimersByTimeAsync(150);
337
+ expect(post).toHaveBeenCalledTimes(1);
338
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toBeNull();
339
+ expect(error).toHaveBeenCalledTimes(failure === "AbortError" ? 0 : 1);
340
+ type("t");
341
+ await vi.advanceTimersByTimeAsync(150);
342
+ expect(post).toHaveBeenCalledTimes(2);
343
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
344
+ "(recovered)",
345
+ );
346
+ },
347
+ );
348
+
349
+ it.each(["empty document", "empty prefix"])(
350
+ "does not call the custom provider for an %s",
351
+ async (input) => {
352
+ useCustomEditor();
353
+ const post = vi.spyOn(API, "post").mockResolvedValue("unexpected");
354
+ view.dispatch({
355
+ changes: {
356
+ from: 0,
357
+ to: view.state.doc.length,
358
+ insert: input === "empty document" ? "" : "suffix",
359
+ },
360
+ selection: { anchor: 0 },
361
+ });
362
+ await vi.advanceTimersByTimeAsync(150);
363
+ expect(post).not.toHaveBeenCalled();
364
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toBeNull();
365
+ },
366
+ );
367
+
368
+ it("aborts custom provider HTTP work when its editor is destroyed", async () => {
369
+ useCustomEditor();
370
+ const result = Promise.withResolvers<string>();
371
+ const post = vi.spyOn(API, "post").mockReturnValue(result.promise);
372
+ const error = vi
373
+ .spyOn(console, "error")
374
+ .mockImplementation(() => undefined);
375
+ type("n");
376
+ await vi.advanceTimersByTimeAsync(150);
377
+ expect(post).toHaveBeenCalledTimes(1);
378
+ const signal = post.mock.calls[0][2]?.signal;
379
+ expect(signal?.aborted).toBe(false);
380
+ view.destroy();
381
+ expect(signal?.aborted).toBe(true);
382
+ result.reject(new Error("connection closed after destruction"));
383
+ await vi.advanceTimersByTimeAsync(150);
384
+ expect(post).toHaveBeenCalledTimes(1);
385
+ expect(error).not.toHaveBeenCalled();
386
+ });
387
+
388
+ it.each(["server error", "timeout"])(
389
+ "recovers on the next edit after a Copilot %s",
390
+ async (failure) => {
391
+ type("n");
392
+ await vi.advanceTimersByTimeAsync(150);
393
+ expect(requests()).toHaveLength(1);
394
+ const id = requests()[0].id;
395
+ if (failure === "timeout") {
396
+ await vi.advanceTimersByTimeAsync(10_000);
397
+ } else {
398
+ emit({
399
+ jsonrpc: "2.0",
400
+ id,
401
+ error: { code: -32603, message: "unavailable" },
402
+ });
403
+ await vi.advanceTimersByTimeAsync(0);
404
+ }
405
+ expect(
406
+ sent.filter(
407
+ (message) =>
408
+ "method" in message && message.method === "$/cancelRequest",
409
+ ),
410
+ ).toEqual(
411
+ failure === "timeout"
412
+ ? [{ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } }]
413
+ : [],
414
+ );
415
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toBeNull();
416
+ type("t");
417
+ await vi.advanceTimersByTimeAsync(150);
418
+ expect(requests()).toHaveLength(2);
419
+ respond("(recovered)");
420
+ await vi.advanceTimersByTimeAsync(0);
421
+ emit({ jsonrpc: "2.0", id, result: [{ insertText: "stale" }] });
422
+ await vi.advanceTimersByTimeAsync(0);
423
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
424
+ "(recovered)",
425
+ );
426
+ },
427
+ );
428
+
429
+ it("keeps the focused cell last when another cell changes during the debounce", async () => {
430
+ type("n");
431
+ await client.textDocumentDidChange({
432
+ textDocument: { uri: "file:///__marimo_copilot__.py", version: 1 },
433
+ contentChanges: [{ text: "other_cell = 42" }],
434
+ });
435
+ await vi.advanceTimersByTimeAsync(150);
436
+ expect(getCodes).toHaveBeenCalledExactlyOnceWith("prin");
437
+ expect(requests()[0].params).toMatchObject({
438
+ textDocument: { version: 3 },
439
+ position: { line: 2, character: 4 },
440
+ });
441
+ respond("t()");
442
+ await vi.advanceTimersByTimeAsync(0);
443
+ expect(view.dom.querySelector(".cm-inline-suggestion")).toHaveTextContent(
444
+ "t()",
445
+ );
446
+ });
447
+ });