@marimo-team/islands 0.23.9-dev8 → 0.23.9
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/{ConnectedDataExplorerComponent-OzrfMM5L.js → ConnectedDataExplorerComponent-CyV83R2m.js} +4 -4
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +1 -0
- package/dist/assets/{worker-CpBbwbQo.js → worker-ip3AI_sN.js} +2 -2
- package/dist/{chat-ui-BDI3FMI8.js → chat-ui-ChD4VvCo.js} +3060 -3033
- package/dist/{code-visibility-SqsoLwxQ.js → code-visibility-BkuwTYAm.js} +1368 -1204
- package/dist/{formats-DQ5qjo_Q.js → formats-DHxc-FdY.js} +1 -1
- package/dist/{glide-data-editor-DqRY9naW.js → glide-data-editor-BOmK9ETQ.js} +2 -2
- package/dist/{html-to-image-CiSinpSR.js → html-to-image-BHv7CEU_.js} +2145 -2153
- package/dist/{input-CZD2z6X2.js → input-_2sjvfne.js} +1 -1
- package/dist/main.js +680 -705
- package/dist/{mermaid-IU93XzmY.js → mermaid-lXOw5Py9.js} +2 -2
- package/dist/{process-output-5qJjMRKh.js → process-output-BvySRgli.js} +33 -25
- package/dist/{reveal-component-v4zHgynl.js → reveal-component-DeBkkDcg.js} +312 -291
- package/dist/{spec-a6DaqW__.js → spec-B96zNUEA.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-ZVVIBmdk.js → toDate-x-WRDCH7.js} +1 -1
- package/dist/{useAsyncData-C008zUPi.js → useAsyncData-iRgKDT5s.js} +1 -1
- package/dist/{useDeepCompareMemoize-BrA3_n61.js → useDeepCompareMemoize-CkQ57VS2.js} +1 -1
- package/dist/{useLifecycle-BNaoJ5a4.js → useLifecycle-BBO9PIph.js} +1 -1
- package/dist/{useTheme-7O0YWlE5.js → useTheme-DHIrRQOe.js} +34 -21
- package/dist/{vega-component-DJNmOdUj.js → vega-component-Dq-SH463.js} +5 -5
- package/package.json +1 -1
- package/src/components/ai/__tests__/ai-utils.test.ts +43 -38
- package/src/components/ai/ai-model-dropdown.tsx +2 -2
- package/src/components/app-config/ai-config.tsx +147 -16
- package/src/components/app-config/user-config-form.tsx +37 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +269 -0
- package/src/components/chat/chat-panel.tsx +38 -5
- package/src/components/chat/chat-utils.ts +14 -58
- package/src/components/data-table/TableBottomBar.tsx +5 -8
- package/src/components/data-table/__tests__/column-explorer.test.tsx +128 -0
- package/src/components/data-table/__tests__/header-items.test.tsx +220 -10
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +95 -29
- package/src/components/data-table/column-header.tsx +17 -12
- package/src/components/data-table/data-table.tsx +4 -0
- package/src/components/data-table/export-actions.tsx +19 -12
- package/src/components/data-table/header-items.tsx +40 -16
- package/src/components/data-table/hooks/use-column-visibility.ts +14 -0
- package/src/components/data-table/schemas.ts +2 -2
- package/src/components/data-table/table-explorer-panel/table-explorer-panel.tsx +16 -6
- package/src/components/databases/display.tsx +2 -0
- package/src/components/datasources/__tests__/utils.test.ts +82 -0
- package/src/components/datasources/utils.ts +16 -15
- package/src/components/editor/Disconnected.tsx +1 -60
- package/src/components/editor/__tests__/viewer-banner.test.tsx +89 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -0
- package/src/components/editor/actions/useCellActionButton.tsx +3 -3
- package/src/components/editor/actions/useNotebookActions.tsx +5 -2
- package/src/components/editor/cell/code/cell-editor.tsx +25 -5
- package/src/components/editor/chrome/types.ts +13 -6
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
- package/src/components/editor/chrome/wrapper/footer-items/ai-status.tsx +10 -1
- package/src/components/editor/chrome/wrapper/sidebar.tsx +7 -5
- package/src/components/editor/errors/auto-fix.tsx +3 -3
- package/src/components/editor/header/__tests__/status.test.tsx +0 -15
- package/src/components/editor/header/app-header.tsx +1 -4
- package/src/components/editor/header/status.tsx +4 -13
- package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -0
- package/src/components/editor/navigation/navigation.ts +5 -0
- package/src/components/editor/output/MarimoErrorOutput.tsx +103 -25
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -39
- package/src/components/editor/renderers/cell-array.tsx +27 -24
- package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +30 -17
- package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +17 -8
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +10 -12
- package/src/components/editor/viewer-banner.tsx +82 -0
- package/src/components/slides/minimap.tsx +45 -9
- package/src/components/slides/reveal-component.tsx +82 -37
- package/src/components/slides/slide-cell-view.tsx +12 -1
- package/src/components/slides/slide-form.tsx +11 -3
- package/src/components/static-html/static-banner.tsx +28 -22
- package/src/core/ai/__tests__/model-registry.test.ts +72 -60
- package/src/core/ai/model-registry.ts +33 -28
- package/src/core/cells/__tests__/actions.test.ts +48 -0
- package/src/core/cells/actions.ts +5 -6
- package/src/core/codemirror/__tests__/setup.test.ts +29 -0
- package/src/core/codemirror/cells/traceback-decorations.ts +1 -1
- package/src/core/codemirror/cm.ts +50 -3
- package/src/core/codemirror/completion/hints.ts +4 -1
- package/src/core/codemirror/format.ts +1 -0
- package/src/core/codemirror/keymaps/vim.ts +63 -0
- package/src/core/codemirror/language/languages/sql/sql.ts +1 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +2 -0
- package/src/core/config/__tests__/config-schema.test.ts +4 -0
- package/src/core/config/config-schema.ts +4 -0
- package/src/core/config/config.ts +16 -0
- package/src/core/edit-app.tsx +3 -0
- package/src/core/islands/bootstrap.ts +2 -0
- package/src/core/kernel/__tests__/handlers.test.ts +5 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
- package/src/core/websocket/types.ts +0 -6
- package/src/core/websocket/useMarimoKernelConnection.tsx +3 -12
- package/src/css/app/Cell.css +0 -1
- package/src/plugins/impl/DataTablePlugin.tsx +48 -22
- package/src/plugins/impl/chat/ChatPlugin.tsx +7 -1
- package/src/plugins/impl/chat/__tests__/chat-ui.test.ts +278 -0
- package/src/plugins/impl/chat/chat-ui.tsx +106 -59
- package/src/plugins/impl/chat/types.ts +5 -0
- package/src/utils/__tests__/json-parser.test.ts +1 -69
- package/src/utils/json/json-parser.ts +0 -30
- package/dist/assets/__vite-browser-external-CAdMKBac.js +0 -1
|
@@ -1,46 +1,52 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
|
-
// Mock the models.json import
|
|
5
4
|
vi.mock("@marimo-team/llm-info/models.json", () => {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
5
|
+
const make = (
|
|
6
|
+
overrides: Partial<AiModel> & Pick<AiModel, "name" | "model">,
|
|
7
|
+
): AiModel => ({
|
|
8
|
+
description: "",
|
|
9
|
+
roles: ["chat", "edit"],
|
|
10
|
+
capabilities: [],
|
|
11
|
+
input_types: [],
|
|
12
|
+
output_types: [],
|
|
13
|
+
release_date: "1970-01-01",
|
|
14
|
+
...overrides,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const multiModel = make({
|
|
18
|
+
name: "Multi Provider Model",
|
|
19
|
+
model: "multi-model",
|
|
20
|
+
description: "Model available on multiple providers",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const models: Record<string, AiModel[]> = {
|
|
24
|
+
openai: [
|
|
25
|
+
make({
|
|
26
|
+
name: "GPT-4",
|
|
27
|
+
model: "gpt-4",
|
|
28
|
+
description: "OpenAI GPT-4 model",
|
|
29
|
+
}),
|
|
30
|
+
multiModel,
|
|
31
|
+
],
|
|
32
|
+
anthropic: [
|
|
33
|
+
make({
|
|
34
|
+
name: "Claude 3",
|
|
35
|
+
model: "claude-3-sonnet",
|
|
36
|
+
description: "Anthropic Claude 3 Sonnet",
|
|
37
|
+
}),
|
|
38
|
+
multiModel,
|
|
39
|
+
],
|
|
40
|
+
google: [
|
|
41
|
+
make({
|
|
42
|
+
name: "Gemini Pro",
|
|
43
|
+
model: "gemini-pro",
|
|
44
|
+
description: "Google Gemini Pro model",
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
43
47
|
};
|
|
48
|
+
|
|
49
|
+
return { models };
|
|
44
50
|
});
|
|
45
51
|
|
|
46
52
|
import type { AiModel } from "@marimo-team/llm-info";
|
|
@@ -107,14 +113,15 @@ describe("AiModelRegistry", () => {
|
|
|
107
113
|
});
|
|
108
114
|
|
|
109
115
|
const ids = [...registry.getModelsMap().keys()];
|
|
110
|
-
// Include custom and all default ones
|
|
116
|
+
// Include custom and all default ones; iteration follows provider
|
|
117
|
+
// sections in the source data (openai → anthropic → google).
|
|
111
118
|
expect(ids).toEqual([
|
|
112
119
|
"openai/custom-gpt",
|
|
113
120
|
"openai/gpt-4",
|
|
114
|
-
"anthropic/claude-3-sonnet",
|
|
115
|
-
"google/gemini-pro",
|
|
116
121
|
"openai/multi-model",
|
|
122
|
+
"anthropic/claude-3-sonnet",
|
|
117
123
|
"anthropic/multi-model",
|
|
124
|
+
"google/gemini-pro",
|
|
118
125
|
]);
|
|
119
126
|
});
|
|
120
127
|
});
|
|
@@ -125,9 +132,9 @@ describe("AiModelRegistry", () => {
|
|
|
125
132
|
const openaiModels = registry.getModelsByProvider("openai");
|
|
126
133
|
|
|
127
134
|
expect(openaiModels).toHaveLength(2); // gpt-4 and multi-model
|
|
128
|
-
expect(
|
|
129
|
-
|
|
130
|
-
)
|
|
135
|
+
expect(openaiModels.every((model) => model.provider === "openai")).toBe(
|
|
136
|
+
true,
|
|
137
|
+
);
|
|
131
138
|
});
|
|
132
139
|
|
|
133
140
|
it("should return empty array for provider with no models", () => {
|
|
@@ -147,9 +154,9 @@ describe("AiModelRegistry", () => {
|
|
|
147
154
|
expect(customModel?.name).toBe("custom-gpt");
|
|
148
155
|
expect(customModel?.model).toBe("custom-gpt");
|
|
149
156
|
expect(customModel?.description).toBe("Custom model");
|
|
150
|
-
expect(customModel?.
|
|
157
|
+
expect(customModel?.provider).toBe("openai");
|
|
151
158
|
expect(customModel?.roles).toEqual([]);
|
|
152
|
-
expect(customModel?.
|
|
159
|
+
expect(customModel?.capabilities).toEqual([]);
|
|
153
160
|
});
|
|
154
161
|
|
|
155
162
|
it("should filter models based on displayed models", () => {
|
|
@@ -309,7 +316,10 @@ describe("AiModelRegistry", () => {
|
|
|
309
316
|
|
|
310
317
|
expect(multiModelInOpenai).toBeDefined();
|
|
311
318
|
expect(multiModelInAnthropic).toBeDefined();
|
|
312
|
-
|
|
319
|
+
// Same model id, but each entry belongs to its own provider.
|
|
320
|
+
expect(multiModelInOpenai?.provider).toBe("openai");
|
|
321
|
+
expect(multiModelInAnthropic?.provider).toBe("anthropic");
|
|
322
|
+
expect(multiModelInOpenai?.name).toBe(multiModelInAnthropic?.name);
|
|
313
323
|
});
|
|
314
324
|
|
|
315
325
|
it("should handle displayed models filter with non-existent models", () => {
|
|
@@ -339,20 +349,20 @@ describe("AiModelRegistry", () => {
|
|
|
339
349
|
expect(model).toHaveProperty("name");
|
|
340
350
|
expect(model).toHaveProperty("model");
|
|
341
351
|
expect(model).toHaveProperty("description");
|
|
342
|
-
expect(model).toHaveProperty("
|
|
352
|
+
expect(model).toHaveProperty("provider");
|
|
343
353
|
expect(model).toHaveProperty("roles");
|
|
344
|
-
expect(model).toHaveProperty("
|
|
354
|
+
expect(model).toHaveProperty("capabilities");
|
|
345
355
|
expect(model).toHaveProperty("custom");
|
|
346
356
|
|
|
347
357
|
expect(typeof model.name).toBe("string");
|
|
348
358
|
expect(typeof model.model).toBe("string");
|
|
349
359
|
expect(typeof model.description).toBe("string");
|
|
350
|
-
expect(
|
|
360
|
+
expect(typeof model.provider).toBe("string");
|
|
351
361
|
expect(Array.isArray(model.roles)).toBe(true);
|
|
352
|
-
expect(
|
|
362
|
+
expect(Array.isArray(model.capabilities)).toBe(true);
|
|
353
363
|
expect(typeof model.custom).toBe("boolean");
|
|
354
364
|
|
|
355
|
-
expect(model.
|
|
365
|
+
expect(model.provider).toBe(provider);
|
|
356
366
|
}
|
|
357
367
|
}
|
|
358
368
|
});
|
|
@@ -367,31 +377,33 @@ describe("AiModelRegistry", () => {
|
|
|
367
377
|
|
|
368
378
|
expect(customModel).toMatchInlineSnapshot(`
|
|
369
379
|
{
|
|
380
|
+
"capabilities": [],
|
|
370
381
|
"custom": true,
|
|
371
382
|
"description": "Custom model",
|
|
383
|
+
"input_types": [],
|
|
372
384
|
"model": "custom-gpt",
|
|
373
385
|
"name": "custom-gpt",
|
|
374
|
-
"
|
|
375
|
-
|
|
376
|
-
|
|
386
|
+
"output_types": [],
|
|
387
|
+
"provider": "openai",
|
|
388
|
+
"release_date": "1970-01-01",
|
|
377
389
|
"roles": [],
|
|
378
|
-
"thinking": false,
|
|
379
390
|
}
|
|
380
391
|
`);
|
|
381
392
|
expect(defaultModel).toMatchInlineSnapshot(`
|
|
382
393
|
{
|
|
394
|
+
"capabilities": [],
|
|
383
395
|
"custom": false,
|
|
384
396
|
"description": "OpenAI GPT-4 model",
|
|
397
|
+
"input_types": [],
|
|
385
398
|
"model": "gpt-4",
|
|
386
399
|
"name": "GPT-4",
|
|
387
|
-
"
|
|
388
|
-
|
|
389
|
-
|
|
400
|
+
"output_types": [],
|
|
401
|
+
"provider": "openai",
|
|
402
|
+
"release_date": "1970-01-01",
|
|
390
403
|
"roles": [
|
|
391
404
|
"chat",
|
|
392
405
|
"edit",
|
|
393
406
|
],
|
|
394
|
-
"thinking": false,
|
|
395
407
|
}
|
|
396
408
|
`);
|
|
397
409
|
});
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Role,
|
|
7
|
-
} from "@marimo-team/llm-info";
|
|
8
|
-
import { models } from "@marimo-team/llm-info/models.json";
|
|
9
|
-
import { providers } from "@marimo-team/llm-info/providers.json";
|
|
3
|
+
import type { AiModel as AiModelType, AiProvider } from "@marimo-team/llm-info";
|
|
4
|
+
import { models as modelsJson } from "@marimo-team/llm-info/models.json";
|
|
5
|
+
import { providers as providersJson } from "@marimo-team/llm-info/providers.json";
|
|
10
6
|
import { Logger } from "@/utils/Logger";
|
|
11
7
|
import { MultiMap } from "@/utils/multi-map";
|
|
12
8
|
import { once } from "@/utils/once";
|
|
@@ -14,13 +10,19 @@ import type { ProviderId } from "./ids/ids";
|
|
|
14
10
|
import { AiModelId, type QualifiedModelId, type ShortModelId } from "./ids/ids";
|
|
15
11
|
|
|
16
12
|
export interface AiModel extends AiModelType {
|
|
17
|
-
roles: Role[];
|
|
18
13
|
model: ShortModelId;
|
|
19
|
-
|
|
14
|
+
/** The provider this entry belongs to. */
|
|
15
|
+
provider: ProviderId;
|
|
20
16
|
/** Whether this is a custom model. */
|
|
21
17
|
custom: boolean;
|
|
22
18
|
}
|
|
23
19
|
|
|
20
|
+
// JSON shape matches the `AiModel` schema (Zod-validated at codegen time).
|
|
21
|
+
const models = modelsJson as unknown as Partial<
|
|
22
|
+
Record<ProviderId, AiModelType[]>
|
|
23
|
+
>;
|
|
24
|
+
const providers = providersJson as unknown as readonly AiProvider[];
|
|
25
|
+
|
|
24
26
|
interface KnownModelMaps {
|
|
25
27
|
/** Map of qualified model ID to model info */
|
|
26
28
|
modelMap: ReadonlyMap<QualifiedModelId, AiModel>;
|
|
@@ -32,24 +34,25 @@ export const getKnownModelMaps = once((): KnownModelMaps => {
|
|
|
32
34
|
const modelMap = new Map<QualifiedModelId, AiModel>();
|
|
33
35
|
const defaultModelByProvider = new Map<ProviderId, QualifiedModelId>();
|
|
34
36
|
|
|
35
|
-
for (const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
for (const [providerKey, providerModels] of Object.entries(models)) {
|
|
38
|
+
if (!providerModels) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const provider = providerKey as ProviderId;
|
|
42
|
+
for (const raw of providerModels) {
|
|
43
|
+
const modelId = raw.model as ShortModelId;
|
|
44
|
+
const modelInfo: AiModel = {
|
|
45
|
+
...raw,
|
|
46
|
+
model: modelId,
|
|
47
|
+
provider,
|
|
48
|
+
custom: false,
|
|
49
|
+
};
|
|
47
50
|
|
|
48
|
-
for (const provider of modelInfo.providers) {
|
|
49
51
|
const qualifiedModelId: QualifiedModelId = `${provider}/${modelId}`;
|
|
50
52
|
modelMap.set(qualifiedModelId, modelInfo);
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
const supportsChatOrEdit =
|
|
55
|
+
modelInfo.roles.includes("chat") || modelInfo.roles.includes("edit");
|
|
53
56
|
if (supportsChatOrEdit && !defaultModelByProvider.has(provider)) {
|
|
54
57
|
defaultModelByProvider.set(provider, qualifiedModelId);
|
|
55
58
|
}
|
|
@@ -67,9 +70,8 @@ const getProviderMap = once(
|
|
|
67
70
|
const providerMap = new Map<ProviderId, AiProvider>();
|
|
68
71
|
const providerToOrderIdx = new Map<ProviderId, number>();
|
|
69
72
|
providers.forEach((provider, idx) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
providerToOrderIdx.set(providerId, idx);
|
|
73
|
+
providerMap.set(provider.id, provider);
|
|
74
|
+
providerToOrderIdx.set(provider.id, idx);
|
|
73
75
|
});
|
|
74
76
|
return { providerMap, providerToOrderIdx };
|
|
75
77
|
},
|
|
@@ -158,9 +160,12 @@ export class AiModelRegistry {
|
|
|
158
160
|
name: modelId.shortModelId,
|
|
159
161
|
model: modelId.shortModelId,
|
|
160
162
|
description: "Custom model",
|
|
161
|
-
|
|
163
|
+
provider: modelId.providerId,
|
|
162
164
|
roles: [],
|
|
163
|
-
|
|
165
|
+
capabilities: [],
|
|
166
|
+
input_types: [],
|
|
167
|
+
output_types: [],
|
|
168
|
+
release_date: "1970-01-01",
|
|
164
169
|
custom: true,
|
|
165
170
|
};
|
|
166
171
|
customModelsMap.set(model, modelInfo);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { MockNotebook } from "@/__mocks__/notebook";
|
|
5
|
+
import { scrollAndHighlightCell } from "@/components/editor/links/cell-link";
|
|
6
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
7
|
+
import type { CellId } from "@/core/cells/ids";
|
|
8
|
+
import { createCellRuntimeState } from "@/core/cells/types";
|
|
9
|
+
import { store } from "@/core/state/jotai";
|
|
10
|
+
import { notebookScrollToRunning } from "../actions";
|
|
11
|
+
|
|
12
|
+
vi.mock("@/components/editor/links/cell-link", () => ({
|
|
13
|
+
scrollAndHighlightCell: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("notebookScrollToRunning", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
store.set(notebookAtom, MockNotebook.notebookState({ cellData: {} }));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("scrolls to the first running cell in notebook order", () => {
|
|
23
|
+
const runtimeOnlyCellId = "runtime-only" as CellId;
|
|
24
|
+
const idleCellId = "idle-cell" as CellId;
|
|
25
|
+
const runningCellId = "running-cell" as CellId;
|
|
26
|
+
|
|
27
|
+
const notebook = MockNotebook.notebookState({
|
|
28
|
+
cellData: {
|
|
29
|
+
[idleCellId]: {},
|
|
30
|
+
[runningCellId]: {},
|
|
31
|
+
},
|
|
32
|
+
cellRuntime: {
|
|
33
|
+
[idleCellId]: { status: "idle" },
|
|
34
|
+
[runningCellId]: { status: "running" },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
notebook.cellRuntime = {
|
|
38
|
+
[runtimeOnlyCellId]: createCellRuntimeState({ status: "running" }),
|
|
39
|
+
...notebook.cellRuntime,
|
|
40
|
+
};
|
|
41
|
+
store.set(notebookAtom, notebook);
|
|
42
|
+
|
|
43
|
+
notebookScrollToRunning();
|
|
44
|
+
|
|
45
|
+
expect(scrollAndHighlightCell).toHaveBeenCalledOnce();
|
|
46
|
+
expect(scrollAndHighlightCell).toHaveBeenCalledWith(runningCellId, "focus");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { scrollAndHighlightCell } from "@/components/editor/links/cell-link";
|
|
4
|
-
import { Objects } from "@/utils/objects";
|
|
5
4
|
import { store } from "../state/jotai";
|
|
6
5
|
import { notebookAtom } from "./cells";
|
|
7
6
|
|
|
@@ -10,12 +9,12 @@ import { notebookAtom } from "./cells";
|
|
|
10
9
|
*/
|
|
11
10
|
export function notebookScrollToRunning() {
|
|
12
11
|
// find cell that is currently in "running" state
|
|
13
|
-
const { cellRuntime } = store.get(notebookAtom);
|
|
14
|
-
const
|
|
15
|
-
(
|
|
12
|
+
const { cellIds, cellRuntime } = store.get(notebookAtom);
|
|
13
|
+
const cellId = cellIds.inOrderIds.find(
|
|
14
|
+
(id) => cellRuntime[id]?.status === "running",
|
|
16
15
|
);
|
|
17
|
-
if (!
|
|
16
|
+
if (!cellId) {
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
|
-
scrollAndHighlightCell(
|
|
19
|
+
scrollAndHighlightCell(cellId, "focus");
|
|
21
20
|
}
|
|
@@ -134,6 +134,35 @@ describe("snapshot all duplicate keymaps", () => {
|
|
|
134
134
|
});
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
test("auto_close_pairs: false removes closeBrackets keymaps", () => {
|
|
138
|
+
const withAutoClose = EditorState.create({
|
|
139
|
+
extensions: setup(),
|
|
140
|
+
});
|
|
141
|
+
const withoutAutoClose = EditorState.create({
|
|
142
|
+
extensions: setup({
|
|
143
|
+
completionConfig: {
|
|
144
|
+
...getOpts().completionConfig,
|
|
145
|
+
auto_close_pairs: false,
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const keysWith = withAutoClose.facet(keymap).flat();
|
|
151
|
+
const keysWithout = withoutAutoClose.facet(keymap).flat();
|
|
152
|
+
|
|
153
|
+
// closeBracketsKeymap contributes Backspace and Enter handlers
|
|
154
|
+
expect(keysWith.length).toBeGreaterThan(keysWithout.length);
|
|
155
|
+
|
|
156
|
+
const hasBracketPairHandler = (state: EditorState) =>
|
|
157
|
+
state
|
|
158
|
+
.facet(keymap)
|
|
159
|
+
.flat()
|
|
160
|
+
.some((k) => k.run?.name === "deleteBracketPair");
|
|
161
|
+
|
|
162
|
+
expect(hasBracketPairHandler(withAutoClose)).toBe(true);
|
|
163
|
+
expect(hasBracketPairHandler(withoutAutoClose)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
137
166
|
test("placeholder adds another extension", () => {
|
|
138
167
|
const opts = getOpts();
|
|
139
168
|
const withAI = new PythonLanguageAdapter()
|
|
@@ -147,7 +147,7 @@ export function errorLineHighlighter(
|
|
|
147
147
|
backgroundColor: "color-mix(in srgb, var(--red-4) 40%, transparent)",
|
|
148
148
|
},
|
|
149
149
|
"&.cm-focused .cm-error-line.cm-activeLine": {
|
|
150
|
-
backgroundColor: "color-mix(in srgb, var(--red-
|
|
150
|
+
backgroundColor: "color-mix(in srgb, var(--red-5) 40%, transparent)",
|
|
151
151
|
},
|
|
152
152
|
}),
|
|
153
153
|
];
|
|
@@ -83,6 +83,11 @@ export interface CodeMirrorSetupOpts {
|
|
|
83
83
|
diagnosticsConfig: DiagnosticsConfig;
|
|
84
84
|
displayConfig: Pick<DisplayConfig, "reference_highlighting">;
|
|
85
85
|
inlineAiTooltip: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* CSS selector for the element that CodeMirror tooltips (completions, hover,
|
|
88
|
+
* signature help) should be appended to. Defaults to `#App`.
|
|
89
|
+
*/
|
|
90
|
+
tooltipParentSelector?: string;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
function getPlaceholderType(opts: CodeMirrorSetupOpts) {
|
|
@@ -90,6 +95,46 @@ function getPlaceholderType(opts: CodeMirrorSetupOpts) {
|
|
|
90
95
|
return showPlaceholder ? "marimo-import" : enableAI ? "ai" : "none";
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
const CODEMIRROR_TOOLTIP_PORTAL_CLASS = "cm-tooltip-portal";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the element that editor tooltips (completions, hover, signature help)
|
|
102
|
+
* should be appended to.
|
|
103
|
+
*
|
|
104
|
+
* The default `#App` parent is returned directly. Custom parents are useful
|
|
105
|
+
* when editors live inside a fullscreen subtree, dialog, or scoped typography
|
|
106
|
+
* region. In those cases we append tooltips to a dedicated `not-prose` portal
|
|
107
|
+
* inside the requested parent, reusing it across cells so surrounding typography
|
|
108
|
+
* styles don't leak into editor popups.
|
|
109
|
+
*/
|
|
110
|
+
function resolveCodeMirrorTooltipParent(
|
|
111
|
+
selector: string | undefined,
|
|
112
|
+
): HTMLElement | undefined {
|
|
113
|
+
if (selector == null) {
|
|
114
|
+
return document.querySelector<HTMLElement>("#App") ?? undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const host = document.querySelector<HTMLElement>(selector);
|
|
118
|
+
if (host == null) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const existing = host.querySelector<HTMLElement>(
|
|
123
|
+
`:scope > .${CODEMIRROR_TOOLTIP_PORTAL_CLASS}`,
|
|
124
|
+
);
|
|
125
|
+
if (existing != null) {
|
|
126
|
+
return existing;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const portal = document.createElement("div");
|
|
130
|
+
// `not-prose` escapes scoped typography; `contents` keeps the wrapper
|
|
131
|
+
// layout-neutral. Tooltips are `position: fixed`, so the wrapper having no
|
|
132
|
+
// box doesn't affect positioning.
|
|
133
|
+
portal.className = `${CODEMIRROR_TOOLTIP_PORTAL_CLASS} not-prose contents`;
|
|
134
|
+
host.append(portal);
|
|
135
|
+
return portal;
|
|
136
|
+
}
|
|
137
|
+
|
|
93
138
|
/**
|
|
94
139
|
* Setup CodeMirror for a cell
|
|
95
140
|
*/
|
|
@@ -180,8 +225,10 @@ export const basicBundle = (opts: CodeMirrorSetupOpts): Extension[] => {
|
|
|
180
225
|
cellId,
|
|
181
226
|
lspConfig,
|
|
182
227
|
diagnosticsConfig,
|
|
228
|
+
tooltipParentSelector,
|
|
183
229
|
} = opts;
|
|
184
230
|
const placeholderType = getPlaceholderType(opts);
|
|
231
|
+
const autoClosePairs = completionConfig.auto_close_pairs !== false;
|
|
185
232
|
|
|
186
233
|
return [
|
|
187
234
|
///// View
|
|
@@ -199,7 +246,7 @@ export const basicBundle = (opts: CodeMirrorSetupOpts): Extension[] => {
|
|
|
199
246
|
position: "fixed",
|
|
200
247
|
// This the z-index multiple tooltips being stacked
|
|
201
248
|
// For example, if we have a hover tooltip and a completion tooltip
|
|
202
|
-
parent:
|
|
249
|
+
parent: resolveCodeMirrorTooltipParent(tooltipParentSelector),
|
|
203
250
|
}),
|
|
204
251
|
scrollActiveLineIntoViewExtension(),
|
|
205
252
|
theme === "dark" ? darkTheme : lightTheme,
|
|
@@ -208,10 +255,10 @@ export const basicBundle = (opts: CodeMirrorSetupOpts): Extension[] => {
|
|
|
208
255
|
copilotBundle(completionConfig),
|
|
209
256
|
foldGutter(),
|
|
210
257
|
stringsAutoCloseBraces(),
|
|
211
|
-
closeBrackets(),
|
|
258
|
+
autoClosePairs ? closeBrackets() : [],
|
|
212
259
|
completionKeymap(acceptCompletionOnEnter),
|
|
213
260
|
// to avoid clash with charDeleteBackward keymap
|
|
214
|
-
Prec.high(keymap.of(closeBracketsKeymap)),
|
|
261
|
+
autoClosePairs ? Prec.high(keymap.of(closeBracketsKeymap)) : [],
|
|
215
262
|
bracketMatching(),
|
|
216
263
|
indentOnInput(),
|
|
217
264
|
indentUnit.of(" "),
|
|
@@ -16,7 +16,10 @@ import { AUTOCOMPLETER, Autocompleter } from "./Autocompleter";
|
|
|
16
16
|
export function hintTooltip(lspConfig: LSPConfig) {
|
|
17
17
|
return [
|
|
18
18
|
// Hover tooltip is already covered by LSP
|
|
19
|
-
lspConfig?.pylsp?.enabled && hasCapability("pylsp")
|
|
19
|
+
(lspConfig?.pylsp?.enabled && hasCapability("pylsp")) ||
|
|
20
|
+
(lspConfig?.ty?.enabled && hasCapability("ty")) ||
|
|
21
|
+
(lspConfig?.pyrefly?.enabled && hasCapability("pyrefly")) ||
|
|
22
|
+
(lspConfig?.basedpyright?.enabled && hasCapability("basedpyright"))
|
|
20
23
|
? []
|
|
21
24
|
: hoverTooltip(
|
|
22
25
|
async (view, pos) => {
|
|
@@ -112,6 +112,35 @@ export function vimKeymapExtension(): Extension[] {
|
|
|
112
112
|
];
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function scrollCursorTo(cm: CodeMirror, position: "center" | "start" | "end") {
|
|
116
|
+
const view = cm.cm6;
|
|
117
|
+
if (!view) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const coords = view.coordsAtPos(view.state.selection.main.head);
|
|
121
|
+
if (!coords) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const appEl = document.getElementById("App");
|
|
125
|
+
if (!appEl) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const viewportHeight = appEl.clientHeight;
|
|
129
|
+
let delta: number;
|
|
130
|
+
switch (position) {
|
|
131
|
+
case "center":
|
|
132
|
+
delta = (coords.top + coords.bottom) / 2 - viewportHeight / 2;
|
|
133
|
+
break;
|
|
134
|
+
case "start":
|
|
135
|
+
delta = coords.top;
|
|
136
|
+
break;
|
|
137
|
+
case "end":
|
|
138
|
+
delta = coords.bottom - viewportHeight;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
appEl.scrollBy({ top: delta, behavior: "smooth" });
|
|
142
|
+
}
|
|
143
|
+
|
|
115
144
|
const addCustomVimCommandsOnce = once(() => {
|
|
116
145
|
// Go to definition
|
|
117
146
|
Vim.defineAction("goToDefinition", (cm: CodeMirror) => {
|
|
@@ -120,6 +149,40 @@ const addCustomVimCommandsOnce = once(() => {
|
|
|
120
149
|
});
|
|
121
150
|
Vim.mapCommand("gd", "action", "goToDefinition", {}, { context: "normal" });
|
|
122
151
|
|
|
152
|
+
// Scroll cursor to center/top/bottom of viewport (mirrors zz/zt/zb in classic vim)
|
|
153
|
+
Vim.defineAction("scrollCursorToCenter", (cm: CodeMirror) =>
|
|
154
|
+
scrollCursorTo(cm, "center"),
|
|
155
|
+
);
|
|
156
|
+
Vim.mapCommand(
|
|
157
|
+
"zz",
|
|
158
|
+
"action",
|
|
159
|
+
"scrollCursorToCenter",
|
|
160
|
+
{},
|
|
161
|
+
{ context: "normal" },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
Vim.defineAction("scrollCursorToTop", (cm: CodeMirror) =>
|
|
165
|
+
scrollCursorTo(cm, "start"),
|
|
166
|
+
);
|
|
167
|
+
Vim.mapCommand(
|
|
168
|
+
"zt",
|
|
169
|
+
"action",
|
|
170
|
+
"scrollCursorToTop",
|
|
171
|
+
{},
|
|
172
|
+
{ context: "normal" },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
Vim.defineAction("scrollCursorToBottom", (cm: CodeMirror) =>
|
|
176
|
+
scrollCursorTo(cm, "end"),
|
|
177
|
+
);
|
|
178
|
+
Vim.mapCommand(
|
|
179
|
+
"zb",
|
|
180
|
+
"action",
|
|
181
|
+
"scrollCursorToBottom",
|
|
182
|
+
{},
|
|
183
|
+
{ context: "normal" },
|
|
184
|
+
);
|
|
185
|
+
|
|
123
186
|
// Save command
|
|
124
187
|
Vim.defineEx("write", "w", (cm: CodeMirror) => {
|
|
125
188
|
const view = cm.cm6;
|
|
@@ -52,6 +52,7 @@ const KNOWN_DIALECTS_ARRAY = [
|
|
|
52
52
|
"databricks",
|
|
53
53
|
"datafusion",
|
|
54
54
|
"microsoft sql server",
|
|
55
|
+
"dremio",
|
|
55
56
|
] as const;
|
|
56
57
|
const KNOWN_DIALECTS: ReadonlySet<string> = new Set(KNOWN_DIALECTS_ARRAY);
|
|
57
58
|
type KnownDialect = (typeof KNOWN_DIALECTS_ARRAY)[number];
|
|
@@ -115,6 +116,7 @@ export function guessDialect(
|
|
|
115
116
|
case "spark":
|
|
116
117
|
case "databricks":
|
|
117
118
|
case "datafusion":
|
|
119
|
+
case "dremio":
|
|
118
120
|
Logger.debug("Unsupported dialect", { dialect });
|
|
119
121
|
return ModifiedStandardSQL;
|
|
120
122
|
default:
|