@marimo-team/frontend 0.25.2-dev114 → 0.25.2-dev115
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/assets/{CellStatus-B4z-D5H1.js → CellStatus-CtjqYbWH.js} +1 -1
- package/dist/assets/{ErrorBoundary-R-fjO4Cz.js → ErrorBoundary-F1payLgE.js} +2 -2
- package/dist/assets/{JsonOutput-BeLVZDXh.js → JsonOutput-Bhgwb5kV.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-B2Ajs_V-.js → MarimoErrorOutput-4jDnOIVr.js} +1 -1
- package/dist/assets/{RenderHTML-Chx9oQ4S.js → RenderHTML-B5VPTO14.js} +1 -1
- package/dist/assets/{RunButton-DnSopQoo.js → RunButton-BIzGc4XI.js} +1 -1
- package/dist/assets/{add-cell-with-ai-9oZmQ0tX.js → add-cell-with-ai-Cmm2fga_.js} +10 -10
- package/dist/assets/{add-connection-dialog-C82vMzr5.js → add-connection-dialog-DCpxcHW4.js} +1 -1
- package/dist/assets/{agent-panel-8bu-4RMv.js → agent-panel-BsG9PQlJ.js} +5 -5
- package/dist/assets/ai-model-dropdown-C4Dg70H_.js +5 -0
- package/dist/assets/{api-BJPYk7E_.js → api-BeC_VSGv.js} +1 -1
- package/dist/assets/app-config-button-qYCFYvje.js +1 -0
- package/dist/assets/{blob-BJi30p8H.js → blob-QvTM0DdL.js} +1 -1
- package/dist/assets/{cell-editor-CKwmt_18.js → cell-editor-DdAAUVJx.js} +11 -11
- package/dist/assets/{cell-link-BI2FitQ_.js → cell-link-CIHoy1VE.js} +1 -1
- package/dist/assets/{cells-D73Y_xmw.js → cells-BWyR7TCJ.js} +1 -1
- package/dist/assets/{chat-display-CqK2mMHm.js → chat-display-BzvZ1dG6.js} +1 -1
- package/dist/assets/chat-panel-CpLCiU26.js +4 -0
- package/dist/assets/{chat-ui-CK-mgi6S.js → chat-ui-t3zRs2SX.js} +1 -1
- package/dist/assets/{column-preview-biRDE_Oz.js → column-preview-BJr1QW8R.js} +1 -1
- package/dist/assets/command-palette-BQWj0cos.js +1 -0
- package/dist/assets/{common-8ezkeamF.js → common-2ZN9KNv-.js} +1 -1
- package/dist/assets/{components-FsfAPhUd.js → components-_sJpwVD4.js} +1 -1
- package/dist/assets/{datasource-Dniq1d2k.js → datasource-C_fKqG0e.js} +1 -1
- package/dist/assets/{dependency-graph-panel-Ci62Tv1x.js → dependency-graph-panel-DSol2xZ3.js} +1 -1
- package/dist/assets/{documentation-panel-B-0JfpSn.js → documentation-panel-BcxEbDPv.js} +1 -1
- package/dist/assets/{download-CEG5vQri.js → download-B5cBmz55.js} +1 -1
- package/dist/assets/{edit-page-VV7jun46.js → edit-page-DMQmQsPo.js} +6 -6
- package/dist/assets/{error-entries-Frq0QZL-.js → error-entries-PrZ4UIKa.js} +1 -1
- package/dist/assets/{error-panel-BeSB6k3e.js → error-panel-Cuohhvyw.js} +1 -1
- package/dist/assets/feedback-button-5oy5W7Kx.js +1 -0
- package/dist/assets/{feedback-button-DALE40Cq.js → feedback-button-aEZd8wqo.js} +1 -1
- package/dist/assets/{file-explorer-panel-DkhJMce7.js → file-explorer-panel-DMrCYHTB.js} +1 -1
- package/dist/assets/{file-name-input-DsLb3kUE.js → file-name-input-CRkt1da3.js} +1 -1
- package/dist/assets/{floating-outline-Bivo15Ee.js → floating-outline-BnO_h00w.js} +1 -1
- package/dist/assets/{gallery-page-BipSEe8i.js → gallery-page-g3iLOyQO.js} +1 -1
- package/dist/assets/{glide-data-editor-DWxXZHhh.js → glide-data-editor-WZMQjVJd.js} +1 -1
- package/dist/assets/{home-page-4xyvW9LC.js → home-page-vBOIWWG_.js} +1 -1
- package/dist/assets/{hooks-B7y2bbmR.js → hooks-DlgJEnzq.js} +1 -1
- package/dist/assets/{index-DLi7reLY.js → index-DTa3czvL.js} +4 -4
- package/dist/assets/{layout-B5DZz9ah.js → layout-88dS_AsT.js} +3 -3
- package/dist/assets/{logs-panel-CswvaDqE.js → logs-panel-Ct5o61k4.js} +1 -1
- package/dist/assets/{markdown-renderer-CErbddaa.js → markdown-renderer-bBVFObv0.js} +1 -1
- package/dist/assets/{name-cell-input-DXf9tZ25.js → name-cell-input-RN-BHucn.js} +1 -1
- package/dist/assets/{outline-panel-BPlLwpVm.js → outline-panel-B_GILrNI.js} +1 -1
- package/dist/assets/{packages-panel-CoIvCGdp.js → packages-panel-r_YSs3uG.js} +1 -1
- package/dist/assets/{panel-accordion-state-B50Y_3hb.js → panel-accordion-state-BF3wKXuD.js} +1 -1
- package/dist/assets/{panels-j8cWK9B8.js → panels-DNYjDBgD.js} +1 -1
- package/dist/assets/{process-output-Bfoxbqpj.js → process-output-C_JmjPkt.js} +1 -1
- package/dist/assets/{readonly-code-display-BusLGSPF.js → readonly-code-display-Bsv-J4Xx.js} +1 -1
- package/dist/assets/{readonly-python-code-CTbOf95g.js → readonly-python-code-bOYKcbg4.js} +1 -1
- package/dist/assets/{reveal-component-D3wlU8v1.js → reveal-component-CLJ6dooc.js} +1 -1
- package/dist/assets/{run-page-B03JZs0U.js → run-page-FdfJRz8z.js} +1 -1
- package/dist/assets/{runtime-details-state-Ddpsoriz.js → runtime-details-state-ZkKdmmAJ.js} +1 -1
- package/dist/assets/{scratchpad-panel-B9RdvhAM.js → scratchpad-panel-D3bf52na.js} +1 -1
- package/dist/assets/{session-panel-D3E0Ag30.js → session-panel-DJ2aIlDv.js} +1 -1
- package/dist/assets/{snippets-panel-Bby1zIJE.js → snippets-panel-C-lvTkYT.js} +1 -1
- package/dist/assets/{state-ENE4B_JH.js → state-C1RQhToR.js} +1 -1
- package/dist/assets/{state-zdyODrb0.js → state-DMZ8VJcq.js} +1 -1
- package/dist/assets/{tracing-De9k9D6u.js → tracing-f3Wv0MqG.js} +1 -1
- package/dist/assets/{tracing-panel-DvISFiEI.js → tracing-panel-BDSbtHAO.js} +2 -2
- package/dist/assets/{tree-actions-D5HC2yfb.js → tree-actions-PNciswRG.js} +1 -1
- package/dist/assets/{useAddCell-DAJ9Wd-E.js → useAddCell-mcL2PJTv.js} +1 -1
- package/dist/assets/{useCellActionButton-CWhycKft.js → useCellActionButton-CYjo2N8X.js} +1 -1
- package/dist/assets/{useDeleteCell-8ByYNceO.js → useDeleteCell-hqk8CkzR.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-Dz37hZtr.js → useDependencyPanelTab-rJGeCoZQ.js} +1 -1
- package/dist/assets/useNotebookActions-BEpiNBZu.js +1 -0
- package/dist/assets/{useRunCells-BVoQp_ne.js → useRunCells-DIY1IVXx.js} +1 -1
- package/dist/assets/{useSplitCell-Ba_Zm0ID.js → useSplitCell-BGcgAf0Z.js} +1 -1
- package/dist/index.html +20 -20
- package/package.json +3 -3
- package/src/core/codemirror/copilot/__tests__/completion-integration.test.ts +447 -0
- package/src/core/codemirror/copilot/__tests__/inline-completion.test.ts +395 -0
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +511 -2
- package/src/core/codemirror/copilot/__tests__/rpc-cancellation.test.ts +201 -0
- package/src/core/codemirror/copilot/extension.ts +22 -11
- package/src/core/codemirror/copilot/language-server.ts +109 -60
- package/src/core/network/__tests__/api.test.ts +114 -1
- package/src/core/network/api.ts +3 -2
- package/dist/assets/ai-model-dropdown-BKQARUae.js +0 -5
- package/dist/assets/app-config-button-DKoDBgwu.js +0 -1
- package/dist/assets/chat-panel-BgJcxa4q.js +0 -4
- package/dist/assets/command-palette-CHUS5_VG.js +0 -1
- package/dist/assets/feedback-button-CrqeO2wx.js +0 -1
- package/dist/assets/useNotebookActions-CYbHF8MU.js +0 -1
|
@@ -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
|
+
});
|