@marimo-team/islands 0.23.16-dev6 → 0.23.16-dev62
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-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
- package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
- package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
- package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
- package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
- package/dist/{chat-ui-BdSDiVjk.js → chat-ui-DsZJj75A.js} +3117 -3120
- package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
- package/dist/{code-visibility-CfBftiVM.js → common-Bpnmu1Yz.js} +1085 -1053
- package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
- package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
- package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
- package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
- package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
- package/dist/{formats-BFhkZJwd.js → formats-Dwq1THhr.js} +3 -3
- package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
- package/dist/{html-to-image-DDENewl2.js → html-to-image-CZ1kLKkq.js} +7350 -6356
- package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
- package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
- package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
- package/dist/main.js +259 -194
- package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
- package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
- package/dist/{process-output-2QPcgBQg.js → process-output-bVfbcx5_.js} +32 -28
- package/dist/{reveal-component-BclLkYnV.js → reveal-component-JIt3oSeU.js} +642 -634
- package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
- package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
- package/dist/toDate-CqhBWjvO.js +713 -0
- package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
- package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
- package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
- package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
- package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
- package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
- package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
- package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
- package/dist/{vega-component-CrFoZT1S.js → vega-component-C7nO5zcf.js} +10 -10
- package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
- package/package.json +3 -4
- package/src/__mocks__/requests.ts +56 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/__tests__/columns.test.tsx +9 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/columns.tsx +11 -1
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
- package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
- package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
- package/src/components/editor/actions/pdf-export.ts +11 -14
- package/src/components/editor/actions/useNotebookActions.tsx +28 -19
- package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
- package/src/components/editor/cell/code/cell-editor.tsx +13 -7
- package/src/components/editor/chrome/__tests__/state.test.ts +7 -1
- package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
- package/src/components/editor/chrome/panels/__tests__/panel-accordion-state.test.ts +42 -0
- package/src/components/editor/chrome/panels/file-explorer-panel.tsx +6 -17
- package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
- package/src/components/editor/chrome/panels/panel-accordion-state.ts +51 -0
- package/src/components/editor/chrome/panels/session-panel.tsx +5 -16
- package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
- package/src/components/editor/chrome/state.ts +10 -0
- package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
- package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
- package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
- package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
- package/src/components/editor/connections/components.tsx +6 -2
- package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +27 -5
- package/src/components/editor/connections/database/__tests__/as-code.test.ts +33 -0
- package/src/components/editor/connections/database/as-code.ts +37 -13
- package/src/components/editor/connections/database/schemas.ts +6 -5
- package/src/components/editor/connections/form-renderers.tsx +99 -1
- package/src/components/editor/connections/json-credentials.ts +57 -0
- package/src/components/editor/connections/paths.ts +21 -0
- package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
- package/src/components/editor/connections/secret-combobox.tsx +41 -20
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
- package/src/components/editor/connections/storage/as-code.ts +79 -36
- package/src/components/editor/connections/storage/schemas.ts +85 -44
- package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
- package/src/components/editor/file-tree/file-viewer.tsx +11 -7
- package/src/components/editor/file-tree/renderers.tsx +21 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -2
- package/src/components/editor/viewer-banner.tsx +6 -1
- package/src/components/forms/form.tsx +59 -24
- package/src/components/forms/options.ts +2 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/reveal-component.tsx +22 -17
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/core/ai/context/providers/file.ts +8 -29
- package/src/core/cells/__tests__/cells.test.ts +12 -2
- package/src/core/cells/__tests__/logs.test.ts +5 -5
- package/src/core/cells/cells.ts +17 -4
- package/src/core/cells/document-changes.ts +1 -0
- package/src/core/cells/logs.ts +14 -4
- package/src/core/codemirror/cm.ts +3 -0
- package/src/core/codemirror/code-lens/__tests__/actions.test.ts +68 -0
- package/src/core/codemirror/code-lens/__tests__/analyzer.test.ts +224 -0
- package/src/core/codemirror/code-lens/__tests__/entities.test.ts +175 -0
- package/src/core/codemirror/code-lens/__tests__/extension.test.ts +325 -0
- package/src/core/codemirror/code-lens/__tests__/perf.test.ts +121 -0
- package/src/core/codemirror/code-lens/__tests__/popover.test.ts +93 -0
- package/src/core/codemirror/code-lens/actions.ts +33 -0
- package/src/core/codemirror/code-lens/analyzer.ts +284 -0
- package/src/core/codemirror/code-lens/entities.ts +123 -0
- package/src/core/codemirror/code-lens/extension.ts +298 -0
- package/src/core/codemirror/code-lens/icons.ts +36 -0
- package/src/core/codemirror/code-lens/popover.tsx +240 -0
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
- package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
- package/src/core/codemirror/copilot/language-server.ts +182 -70
- package/src/core/codemirror/copilot/transport.ts +15 -207
- package/src/core/codemirror/copilot/types.ts +9 -2
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +93 -90
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +627 -61
- package/src/core/codemirror/language/languages/python.ts +23 -11
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
- package/src/core/codemirror/language/languages/sql/sql.ts +251 -16
- package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
- package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
- package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
- package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
- package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
- package/src/core/codemirror/lsp/log-messages.ts +37 -0
- package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
- package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
- package/src/core/codemirror/lsp/transports.ts +8 -1
- package/src/core/codemirror/lsp/types.ts +68 -8
- package/src/core/codemirror/lsp/utils.ts +14 -0
- package/src/core/codemirror/python-node-names.ts +90 -0
- package/src/core/codemirror/reactive-references/analyzer.ts +52 -57
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/config/feature-flag.tsx +3 -1
- package/src/core/datasets/data-source-discovery.ts +5 -0
- package/src/core/datasets/request-registry.ts +11 -0
- package/src/core/edit-app.tsx +1 -2
- package/src/core/export/__tests__/hooks.test.ts +2 -4
- package/src/core/export/hooks.ts +4 -3
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +18 -0
- package/src/core/kernel/messages.ts +2 -0
- package/src/core/lsp/__tests__/transport.test.ts +509 -337
- package/src/core/lsp/transport.ts +304 -132
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +47 -1
- package/src/core/network/__tests__/requests-network.test.ts +48 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +3 -0
- package/src/core/network/requests-network.ts +31 -8
- package/src/core/network/requests-static.ts +3 -0
- package/src/core/network/requests-toasting.tsx +3 -0
- package/src/core/network/types.ts +27 -6
- package/src/core/rtc/state.ts +9 -0
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/__tests__/bridge.test.ts +99 -5
- package/src/core/wasm/bridge.ts +42 -9
- package/src/core/wasm/worker/types.ts +10 -3
- package/src/core/wasm/worker/worker.ts +7 -8
- package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
- package/src/css/app/codemirror.css +16 -2
- package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
- package/src/hooks/useDataSourceDiscovery.ts +18 -0
- package/src/plugins/impl/DataTablePlugin.tsx +4 -1
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +158 -0
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +55 -0
- package/src/plugins/impl/anywidget/model.ts +20 -5
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +33 -21
- package/src/plugins/impl/data-frames/forms/renderers.tsx +11 -8
- package/src/stories/log-viewer.stories.tsx +12 -6
- package/src/utils/__tests__/blob.test.ts +27 -1
- package/src/utils/__tests__/download.test.tsx +44 -4
- package/src/utils/blob.ts +24 -1
- package/src/utils/download.ts +14 -15
- package/dist/toDate-B3IRfHhw.js +0 -692
|
@@ -38,7 +38,11 @@ import { createLspMarkdownRenderer } from "../../lsp/markdown-renderer";
|
|
|
38
38
|
import { NotebookLanguageServerClient } from "../../lsp/notebook-lsp";
|
|
39
39
|
import { createTransport } from "../../lsp/transports";
|
|
40
40
|
import { CellDocumentUri, type ILanguageServerClient } from "../../lsp/types";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
getLspRootUri,
|
|
43
|
+
getLspWorkspaceFolders,
|
|
44
|
+
isKnownCellDocumentUri,
|
|
45
|
+
} from "../../lsp/utils";
|
|
42
46
|
import {
|
|
43
47
|
clickablePlaceholderExtension,
|
|
44
48
|
smartPlaceholderExtension,
|
|
@@ -338,6 +342,21 @@ export class PythonLanguageAdapter implements LanguageAdapter<{}> {
|
|
|
338
342
|
clients.length === 1
|
|
339
343
|
? (clients[0] as NotebookLanguageServerClient)
|
|
340
344
|
: new FederatedLanguageServerClient(clients);
|
|
345
|
+
const openExternalLocation = (result: { uri: string }) => {
|
|
346
|
+
Logger.debug("openExternalLocation", result);
|
|
347
|
+
// Neither the merged document nor a cell is a file on disk. The
|
|
348
|
+
// plugin handles same-cell locations itself; revealing a location in
|
|
349
|
+
// another cell isn't supported yet.
|
|
350
|
+
if (
|
|
351
|
+
client.documentUri === result.uri ||
|
|
352
|
+
isKnownCellDocumentUri(result.uri)
|
|
353
|
+
) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
getRequestClient().openFile({
|
|
357
|
+
path: result.uri.replace("file://", ""),
|
|
358
|
+
});
|
|
359
|
+
};
|
|
341
360
|
|
|
342
361
|
return [
|
|
343
362
|
languageServerWithClient({
|
|
@@ -346,6 +365,7 @@ export class PythonLanguageAdapter implements LanguageAdapter<{}> {
|
|
|
346
365
|
allowHTMLContent: true,
|
|
347
366
|
markdownRenderer: createLspMarkdownRenderer(),
|
|
348
367
|
useSnippetOnCompletion: true,
|
|
368
|
+
clientSideFiltering: true,
|
|
349
369
|
hoverConfig: hoverOptions,
|
|
350
370
|
completionConfig: autocompleteOptions,
|
|
351
371
|
// Default to false
|
|
@@ -362,16 +382,8 @@ export class PythonLanguageAdapter implements LanguageAdapter<{}> {
|
|
|
362
382
|
rename: hotkeys.getHotkey("cell.renameSymbol").key,
|
|
363
383
|
},
|
|
364
384
|
completionMatchBefore,
|
|
365
|
-
onGoToDefinition:
|
|
366
|
-
|
|
367
|
-
if (client.documentUri === result.uri) {
|
|
368
|
-
// Local definition
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
getRequestClient().openFile({
|
|
372
|
-
path: result.uri.replace("file://", ""),
|
|
373
|
-
});
|
|
374
|
-
},
|
|
385
|
+
onGoToDefinition: openExternalLocation,
|
|
386
|
+
onShowLocation: openExternalLocation,
|
|
375
387
|
}),
|
|
376
388
|
documentUri.of(CellDocumentUri.of(cellId)),
|
|
377
389
|
];
|
|
@@ -109,7 +109,11 @@ class SQLCompletionStore {
|
|
|
109
109
|
return {
|
|
110
110
|
shouldAddLocalTables: true,
|
|
111
111
|
schema: builder.build(),
|
|
112
|
-
defaultSchema:
|
|
112
|
+
defaultSchema:
|
|
113
|
+
default_schema ??
|
|
114
|
+
(defaultDb?.schemas.length === 1
|
|
115
|
+
? defaultDb.schemas[0]?.name
|
|
116
|
+
: undefined),
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
|
|
@@ -52,7 +52,7 @@ const SOURCE_TYPE_COLORS = {
|
|
|
52
52
|
catalog: "bg-(--purple-4) text-(--purple-11)",
|
|
53
53
|
} as const;
|
|
54
54
|
|
|
55
|
-
const CONTAINER_STYLES = "p-3 min-w-[250px] flex flex-col divide-y";
|
|
55
|
+
export const CONTAINER_STYLES = "p-3 min-w-[250px] flex flex-col divide-y";
|
|
56
56
|
|
|
57
57
|
const columnsText = new PluralWord("column", "columns");
|
|
58
58
|
const rowsText = new PluralWord("row", "rows");
|
|
@@ -61,7 +61,7 @@ const tablesText = new PluralWord("table", "tables");
|
|
|
61
61
|
const databasesText = new PluralWord("database", "databases");
|
|
62
62
|
|
|
63
63
|
// Helper components and functions
|
|
64
|
-
const SectionHeader: React.FC<{
|
|
64
|
+
export const SectionHeader: React.FC<{
|
|
65
65
|
icon: React.ReactNode;
|
|
66
66
|
title: string;
|
|
67
67
|
badge?: React.ReactNode;
|
|
@@ -73,7 +73,7 @@ const SectionHeader: React.FC<{
|
|
|
73
73
|
</div>
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
-
const MetadataRow: React.FC<{
|
|
76
|
+
export const MetadataRow: React.FC<{
|
|
77
77
|
label: string;
|
|
78
78
|
value: React.ReactNode;
|
|
79
79
|
}> = ({ label, value }) => (
|
|
@@ -12,12 +12,17 @@ import {
|
|
|
12
12
|
type ViewUpdate,
|
|
13
13
|
} from "@codemirror/view";
|
|
14
14
|
import {
|
|
15
|
+
aliasColumnCompletionSource,
|
|
16
|
+
createCteCompletionSource,
|
|
15
17
|
defaultSqlHoverTheme,
|
|
16
18
|
NodeSqlParser,
|
|
17
19
|
type NodeSqlParserResult,
|
|
20
|
+
QueryContextAnalyzer,
|
|
18
21
|
type SupportedDialects as ParserDialects,
|
|
19
22
|
type SqlParseError,
|
|
20
23
|
sqlExtension,
|
|
24
|
+
SqlStructureAnalyzer,
|
|
25
|
+
unqualifiedColumnCompletionSource,
|
|
21
26
|
} from "@marimo-team/codemirror-sql";
|
|
22
27
|
import { DuckDBDialect } from "@marimo-team/codemirror-sql/dialects";
|
|
23
28
|
import { type SQLMetadata, SQLParser } from "@marimo-team/smart-cells";
|
|
@@ -61,7 +66,7 @@ import { getSQLMode, type SQLMode } from "./sql-mode";
|
|
|
61
66
|
import { isKnownDialect } from "./utils";
|
|
62
67
|
|
|
63
68
|
const DEFAULT_DIALECT = DuckDBDialect;
|
|
64
|
-
const DEFAULT_PARSER_DIALECT = "DuckDB";
|
|
69
|
+
const DEFAULT_PARSER_DIALECT: ParserDialects = "DuckDB";
|
|
65
70
|
|
|
66
71
|
// A compartment for the SQL config, so we can update the config of codemirror
|
|
67
72
|
const sqlConfigCompartment = new Compartment();
|
|
@@ -142,6 +147,7 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
142
147
|
_placeholderType: PlaceholderType,
|
|
143
148
|
lspConfig: LSPConfig & { diagnostics: DiagnosticsConfig },
|
|
144
149
|
): Extension[] {
|
|
150
|
+
const analysis = createSQLAnalysis();
|
|
145
151
|
const extensions = [
|
|
146
152
|
// This can be updated with a dispatch effect
|
|
147
153
|
sqlConfigCompartment.of(sql({ dialect: DEFAULT_DIALECT })),
|
|
@@ -167,6 +173,10 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
167
173
|
variableCompletionSource,
|
|
168
174
|
// Completions for dialect keywords
|
|
169
175
|
customKeywordCompletionSource(),
|
|
176
|
+
// Completions based on the current query
|
|
177
|
+
analysis.cteCompletionSource,
|
|
178
|
+
analysis.aliasCompletionSource,
|
|
179
|
+
analysis.columnCompletionSource,
|
|
170
180
|
],
|
|
171
181
|
}),
|
|
172
182
|
];
|
|
@@ -185,10 +195,23 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
185
195
|
|
|
186
196
|
extensions.push(
|
|
187
197
|
sqlExtension({
|
|
198
|
+
schema: getSchema,
|
|
188
199
|
enableLinting: true,
|
|
189
200
|
linterConfig: {
|
|
190
201
|
delay: 250, // Delay before running validation
|
|
191
202
|
parser: parser,
|
|
203
|
+
// CustomSqlParser performs backend DuckDB validation in validateSql.
|
|
204
|
+
perStatement: false,
|
|
205
|
+
},
|
|
206
|
+
enableSemanticLinting: true,
|
|
207
|
+
semanticLinterConfig: {
|
|
208
|
+
parser: analysis.parser,
|
|
209
|
+
structureAnalyzer: analysis.structureAnalyzer,
|
|
210
|
+
severity: {
|
|
211
|
+
unknownTable: "off",
|
|
212
|
+
unknownColumn: "warning",
|
|
213
|
+
ambiguousColumn: "warning",
|
|
214
|
+
},
|
|
192
215
|
},
|
|
193
216
|
enableGutterMarkers: true,
|
|
194
217
|
gutterConfig: {
|
|
@@ -198,14 +221,21 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
198
221
|
parser: parser,
|
|
199
222
|
},
|
|
200
223
|
hoverConfig: {
|
|
201
|
-
schema: getSchema, // Use the same schema as autocomplete
|
|
202
224
|
hoverTime: 300, // 300ms hover delay
|
|
203
225
|
enableKeywords: true, // Show keyword information
|
|
204
226
|
enableTables: true, // Show table information
|
|
205
227
|
enableColumns: true, // Show column information
|
|
206
|
-
parser: parser,
|
|
228
|
+
parser: analysis.parser,
|
|
229
|
+
contextAnalyzer: analysis.contextAnalyzer,
|
|
207
230
|
theme: defaultSqlHoverTheme(theme),
|
|
208
231
|
},
|
|
232
|
+
enableNavigation: true,
|
|
233
|
+
navigationConfig: {
|
|
234
|
+
contextAnalyzer: analysis.contextAnalyzer,
|
|
235
|
+
keymap: false,
|
|
236
|
+
parser: analysis.parser,
|
|
237
|
+
structureAnalyzer: analysis.structureAnalyzer,
|
|
238
|
+
},
|
|
209
239
|
}),
|
|
210
240
|
EditorView.updateListener.of((update) => {
|
|
211
241
|
if (update.focusChanged) {
|
|
@@ -223,8 +253,127 @@ export class SQLLanguageAdapter implements LanguageAdapter<SQLLanguageAdapterMet
|
|
|
223
253
|
}
|
|
224
254
|
}
|
|
225
255
|
|
|
256
|
+
function createSQLAnalysis() {
|
|
257
|
+
const parser = new NodeSqlParser({
|
|
258
|
+
getParserOptions: (state: EditorState) => {
|
|
259
|
+
return {
|
|
260
|
+
database: getAnalysisDialect(state),
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const contextAnalyzer = new DialectAwareQueryContextAnalyzer(parser);
|
|
265
|
+
const structureAnalyzer = new DialectAwareSqlStructureAnalyzer(parser);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
parser,
|
|
269
|
+
contextAnalyzer,
|
|
270
|
+
structureAnalyzer,
|
|
271
|
+
cteCompletionSource: createCteCompletionSource({
|
|
272
|
+
parser,
|
|
273
|
+
contextAnalyzer,
|
|
274
|
+
structureAnalyzer,
|
|
275
|
+
}),
|
|
276
|
+
aliasCompletionSource: aliasColumnCompletionSource({
|
|
277
|
+
schema: getSchema,
|
|
278
|
+
parser,
|
|
279
|
+
contextAnalyzer,
|
|
280
|
+
}),
|
|
281
|
+
columnCompletionSource: unqualifiedColumnCompletionSource({
|
|
282
|
+
schema: getSchema,
|
|
283
|
+
parser,
|
|
284
|
+
contextAnalyzer,
|
|
285
|
+
}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getAnalysisDialect(state: EditorState): ParserDialects {
|
|
290
|
+
return guessParserDialect(state) ?? DEFAULT_PARSER_DIALECT;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class DialectAwareQueryContextAnalyzer extends QueryContextAnalyzer {
|
|
294
|
+
private readonly analyzers = new Map<ParserDialects, QueryContextAnalyzer>();
|
|
295
|
+
private readonly analysisParser: NodeSqlParser;
|
|
296
|
+
|
|
297
|
+
constructor(analysisParser: NodeSqlParser) {
|
|
298
|
+
super(analysisParser);
|
|
299
|
+
this.analysisParser = analysisParser;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
override getContext(
|
|
303
|
+
sql: string,
|
|
304
|
+
opts: { state: EditorState },
|
|
305
|
+
): ReturnType<QueryContextAnalyzer["getContext"]> {
|
|
306
|
+
return this.getAnalyzer(opts.state).getContext(sql, opts);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
override clearCache(): void {
|
|
310
|
+
for (const analyzer of this.analyzers.values()) {
|
|
311
|
+
analyzer.clearCache();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private getAnalyzer(state: EditorState): QueryContextAnalyzer {
|
|
316
|
+
const dialect = getAnalysisDialect(state);
|
|
317
|
+
let analyzer = this.analyzers.get(dialect);
|
|
318
|
+
if (!analyzer) {
|
|
319
|
+
analyzer = new QueryContextAnalyzer(this.analysisParser);
|
|
320
|
+
this.analyzers.set(dialect, analyzer);
|
|
321
|
+
}
|
|
322
|
+
return analyzer;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
class DialectAwareSqlStructureAnalyzer extends SqlStructureAnalyzer {
|
|
327
|
+
private readonly analyzers = new Map<ParserDialects, SqlStructureAnalyzer>();
|
|
328
|
+
private readonly analysisParser: NodeSqlParser;
|
|
329
|
+
|
|
330
|
+
constructor(analysisParser: NodeSqlParser) {
|
|
331
|
+
super(analysisParser);
|
|
332
|
+
this.analysisParser = analysisParser;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
override analyzeDocument(
|
|
336
|
+
state: EditorState,
|
|
337
|
+
): ReturnType<SqlStructureAnalyzer["analyzeDocument"]> {
|
|
338
|
+
return this.getAnalyzer(state).analyzeDocument(state);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
override getStatementAtPosition(
|
|
342
|
+
state: EditorState,
|
|
343
|
+
position: number,
|
|
344
|
+
): ReturnType<SqlStructureAnalyzer["getStatementAtPosition"]> {
|
|
345
|
+
return this.getAnalyzer(state).getStatementAtPosition(state, position);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
override getStatementsInRange(
|
|
349
|
+
state: EditorState,
|
|
350
|
+
from: number,
|
|
351
|
+
to: number,
|
|
352
|
+
): ReturnType<SqlStructureAnalyzer["getStatementsInRange"]> {
|
|
353
|
+
return this.getAnalyzer(state).getStatementsInRange(state, from, to);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
override clearCache(): void {
|
|
357
|
+
for (const analyzer of this.analyzers.values()) {
|
|
358
|
+
analyzer.clearCache();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private getAnalyzer(state: EditorState): SqlStructureAnalyzer {
|
|
363
|
+
const dialect = getAnalysisDialect(state);
|
|
364
|
+
let analyzer = this.analyzers.get(dialect);
|
|
365
|
+
if (!analyzer) {
|
|
366
|
+
analyzer = new SqlStructureAnalyzer(this.analysisParser);
|
|
367
|
+
this.analyzers.set(dialect, analyzer);
|
|
368
|
+
}
|
|
369
|
+
return analyzer;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
226
373
|
class CustomSqlParser extends NodeSqlParser {
|
|
227
374
|
private validationTimeout: number | null = null;
|
|
375
|
+
private pendingValidationResolve: ((errors: SqlParseError[]) => void) | null =
|
|
376
|
+
null;
|
|
228
377
|
private readonly VALIDATION_DELAY_MS = 300; // Wait 300ms after user stops typing
|
|
229
378
|
private isFocused = false; // Only validate if the editor is focused
|
|
230
379
|
|
|
@@ -232,37 +381,51 @@ class CustomSqlParser extends NodeSqlParser {
|
|
|
232
381
|
this.isFocused = focused;
|
|
233
382
|
}
|
|
234
383
|
|
|
384
|
+
private resolvePendingValidation(errors: SqlParseError[]): void {
|
|
385
|
+
this.pendingValidationResolve?.(errors);
|
|
386
|
+
this.pendingValidationResolve = null;
|
|
387
|
+
}
|
|
388
|
+
|
|
235
389
|
private async validateWithDelay(
|
|
236
390
|
sql: string,
|
|
237
|
-
engine:
|
|
391
|
+
engine: ConnectionName,
|
|
238
392
|
dialect: ParserDialects | null,
|
|
239
393
|
): Promise<SqlParseError[]> {
|
|
240
|
-
// Clear any existing delay call
|
|
394
|
+
// Clear any existing delay call, resolving its promise so a superseded
|
|
395
|
+
// request doesn't hang forever.
|
|
241
396
|
if (this.validationTimeout) {
|
|
242
397
|
window.clearTimeout(this.validationTimeout);
|
|
398
|
+
this.resolvePendingValidation([]);
|
|
243
399
|
}
|
|
244
400
|
|
|
245
401
|
// Set up a new request to be called after the delay
|
|
246
402
|
return new Promise((resolve) => {
|
|
403
|
+
this.pendingValidationResolve = resolve;
|
|
247
404
|
this.validationTimeout = window.setTimeout(async () => {
|
|
405
|
+
this.validationTimeout = null;
|
|
406
|
+
|
|
248
407
|
// Only validate if the editor is still focused
|
|
249
408
|
if (!this.isFocused) {
|
|
250
|
-
|
|
409
|
+
this.resolvePendingValidation([]);
|
|
251
410
|
return;
|
|
252
411
|
}
|
|
253
412
|
|
|
254
413
|
try {
|
|
255
|
-
|
|
414
|
+
// For validate mode, we run EXPLAIN queries on the engine, which can be
|
|
415
|
+
// expensive for remote databases. So, we only run for internal engines.
|
|
416
|
+
const sqlMode = INTERNAL_SQL_ENGINES.has(engine)
|
|
417
|
+
? getSQLMode()
|
|
418
|
+
: "default";
|
|
256
419
|
const result = await validateSQL(sql, engine, dialect, sqlMode);
|
|
257
420
|
if (result.error) {
|
|
258
421
|
Logger.error("Failed to validate SQL", { error: result.error });
|
|
259
|
-
|
|
422
|
+
this.resolvePendingValidation([]);
|
|
260
423
|
return;
|
|
261
424
|
}
|
|
262
|
-
|
|
425
|
+
this.resolvePendingValidation(result.parse_result?.errors ?? []);
|
|
263
426
|
} catch (error) {
|
|
264
427
|
Logger.error("Failed to validate SQL", { error });
|
|
265
|
-
|
|
428
|
+
this.resolvePendingValidation([]);
|
|
266
429
|
}
|
|
267
430
|
}, this.VALIDATION_DELAY_MS);
|
|
268
431
|
});
|
|
@@ -273,19 +436,23 @@ class CustomSqlParser extends NodeSqlParser {
|
|
|
273
436
|
opts: { state: EditorState },
|
|
274
437
|
): Promise<SqlParseError[]> {
|
|
275
438
|
const metadata = getSQLMetadata(opts.state);
|
|
439
|
+
const dialect = connectionNameToParserDialect(metadata.engine);
|
|
276
440
|
|
|
277
441
|
// Only validate if the editor is focused
|
|
278
442
|
if (!this.isFocused) {
|
|
279
443
|
return [];
|
|
280
444
|
}
|
|
281
445
|
|
|
282
|
-
// Only perform custom validation for DuckDB
|
|
283
|
-
if (!
|
|
446
|
+
// Only perform custom validation for DuckDB as we have a custom validation endpoint for it.
|
|
447
|
+
if (!isDuckDBConnection(metadata.engine, dialect)) {
|
|
284
448
|
return super.validateSql(sql, opts);
|
|
285
449
|
}
|
|
286
450
|
|
|
287
|
-
|
|
288
|
-
|
|
451
|
+
return this.validateWithDelay(
|
|
452
|
+
sql,
|
|
453
|
+
metadata.engine,
|
|
454
|
+
dialect ?? DEFAULT_PARSER_DIALECT,
|
|
455
|
+
);
|
|
289
456
|
}
|
|
290
457
|
|
|
291
458
|
override async parse(
|
|
@@ -294,9 +461,10 @@ class CustomSqlParser extends NodeSqlParser {
|
|
|
294
461
|
): Promise<NodeSqlParserResult> {
|
|
295
462
|
const metadata = getSQLMetadata(opts.state);
|
|
296
463
|
const engine = metadata.engine;
|
|
464
|
+
const dialect = connectionNameToParserDialect(engine);
|
|
297
465
|
|
|
298
466
|
// For now, always return success for DuckDB
|
|
299
|
-
if (engine
|
|
467
|
+
if (isDuckDBConnection(engine, dialect)) {
|
|
300
468
|
return { success: true, errors: [] };
|
|
301
469
|
}
|
|
302
470
|
|
|
@@ -304,6 +472,13 @@ class CustomSqlParser extends NodeSqlParser {
|
|
|
304
472
|
}
|
|
305
473
|
}
|
|
306
474
|
|
|
475
|
+
function isDuckDBConnection(
|
|
476
|
+
engine: ConnectionName,
|
|
477
|
+
dialect: ParserDialects | null,
|
|
478
|
+
): boolean {
|
|
479
|
+
return engine === DUCKDB_ENGINE || dialect === "DuckDB";
|
|
480
|
+
}
|
|
481
|
+
|
|
307
482
|
/**
|
|
308
483
|
* Update the SQL dialect in the editor view.
|
|
309
484
|
*/
|
|
@@ -344,7 +519,61 @@ function getSchema(view: EditorView): SQLNamespace {
|
|
|
344
519
|
return {};
|
|
345
520
|
}
|
|
346
521
|
|
|
347
|
-
|
|
522
|
+
const schema = config.schema;
|
|
523
|
+
if (!isNamespaceMap(schema)) {
|
|
524
|
+
return schema;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const defaultSchema = config.defaultSchema;
|
|
528
|
+
const defaultSchemaNamespace = defaultSchema
|
|
529
|
+
? schema[defaultSchema]
|
|
530
|
+
: undefined;
|
|
531
|
+
|
|
532
|
+
// The completion schema contains both default-schema and fully qualified
|
|
533
|
+
// paths. Promote default-schema tables so unqualified references resolve to
|
|
534
|
+
// one exact match instead of appearing ambiguous.
|
|
535
|
+
if (
|
|
536
|
+
defaultSchemaNamespace &&
|
|
537
|
+
!Array.isArray(defaultSchemaNamespace) &&
|
|
538
|
+
typeof defaultSchemaNamespace === "object" &&
|
|
539
|
+
"children" in defaultSchemaNamespace &&
|
|
540
|
+
isNamespaceMap(defaultSchemaNamespace.children)
|
|
541
|
+
) {
|
|
542
|
+
return {
|
|
543
|
+
...schema,
|
|
544
|
+
...Object.fromEntries(
|
|
545
|
+
Object.entries(defaultSchemaNamespace.children).filter(
|
|
546
|
+
([name]) => !Object.hasOwn(schema, name),
|
|
547
|
+
),
|
|
548
|
+
),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return schema;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isNamespaceMap(
|
|
556
|
+
namespace: SQLNamespace,
|
|
557
|
+
): namespace is Record<string, SQLNamespace> {
|
|
558
|
+
if (Array.isArray(namespace)) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!("self" in namespace) || !("children" in namespace)) {
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// A self/children wrapper stores a Completion in `self`. A table or schema
|
|
567
|
+
// literally named "self" stores another namespace there.
|
|
568
|
+
const self = namespace.self;
|
|
569
|
+
const isSelfChildrenWrapper =
|
|
570
|
+
typeof self === "object" &&
|
|
571
|
+
self !== null &&
|
|
572
|
+
!Array.isArray(self) &&
|
|
573
|
+
"label" in self &&
|
|
574
|
+
typeof self.label === "string";
|
|
575
|
+
|
|
576
|
+
return !isSelfChildrenWrapper;
|
|
348
577
|
}
|
|
349
578
|
|
|
350
579
|
function guessParserDialect(state: EditorState): ParserDialects | null {
|
|
@@ -529,3 +758,9 @@ async function validateSQL(
|
|
|
529
758
|
}
|
|
530
759
|
return result;
|
|
531
760
|
}
|
|
761
|
+
|
|
762
|
+
export const exportedForTesting = {
|
|
763
|
+
createSQLAnalysis,
|
|
764
|
+
CustomSqlParser,
|
|
765
|
+
getSchema,
|
|
766
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, type Mocked, vi } from "vitest";
|
|
4
|
+
import type * as LSP from "vscode-languageserver-protocol";
|
|
5
|
+
import { FederatedLanguageServerClient } from "../federated-lsp";
|
|
6
|
+
import type { ILanguageServerClient } from "../types";
|
|
7
|
+
|
|
8
|
+
function createClient(
|
|
9
|
+
supportedMethods: string[] = [],
|
|
10
|
+
): Mocked<ILanguageServerClient> {
|
|
11
|
+
const client: Mocked<ILanguageServerClient> = {
|
|
12
|
+
ready: true,
|
|
13
|
+
capabilities: {},
|
|
14
|
+
initializePromise: Promise.resolve(),
|
|
15
|
+
clientCapabilities: {},
|
|
16
|
+
initialize: vi.fn(),
|
|
17
|
+
close: vi.fn(),
|
|
18
|
+
hasCapability: vi.fn((method) => supportedMethods.includes(method)),
|
|
19
|
+
textDocumentDidOpen: vi.fn().mockResolvedValue(false),
|
|
20
|
+
textDocumentDidChange: vi.fn(),
|
|
21
|
+
textDocumentDidClose: vi.fn(),
|
|
22
|
+
textDocumentWillSave: vi.fn(),
|
|
23
|
+
textDocumentWillSaveWaitUntil: vi.fn().mockResolvedValue(null),
|
|
24
|
+
textDocumentDidSave: vi.fn(),
|
|
25
|
+
textDocumentHover: vi.fn().mockResolvedValue({ contents: [] }),
|
|
26
|
+
textDocumentCompletion: vi.fn().mockResolvedValue(null),
|
|
27
|
+
completionItemResolve: vi.fn(),
|
|
28
|
+
textDocumentDefinition: vi.fn().mockResolvedValue(null),
|
|
29
|
+
textDocumentCodeAction: vi.fn().mockResolvedValue(null),
|
|
30
|
+
codeActionResolve: vi.fn(),
|
|
31
|
+
textDocumentRename: vi.fn().mockResolvedValue(null),
|
|
32
|
+
textDocumentPrepareRename: vi.fn().mockResolvedValue(null),
|
|
33
|
+
textDocumentSignatureHelp: vi.fn().mockResolvedValue(null),
|
|
34
|
+
onNotification: vi.fn().mockReturnValue(() => true),
|
|
35
|
+
};
|
|
36
|
+
return client;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("FederatedLanguageServerClient", () => {
|
|
40
|
+
it("routes requests using dynamic method capabilities", async () => {
|
|
41
|
+
const staticOnlyClient = createClient();
|
|
42
|
+
const dynamicClient = createClient(["textDocument/definition"]);
|
|
43
|
+
const definition: LSP.Location = {
|
|
44
|
+
uri: "file:///definition.py",
|
|
45
|
+
range: {
|
|
46
|
+
start: { line: 1, character: 2 },
|
|
47
|
+
end: { line: 1, character: 5 },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
dynamicClient.textDocumentDefinition.mockResolvedValue(definition);
|
|
51
|
+
const client = new FederatedLanguageServerClient([
|
|
52
|
+
staticOnlyClient,
|
|
53
|
+
dynamicClient,
|
|
54
|
+
]);
|
|
55
|
+
const params: LSP.DefinitionParams = {
|
|
56
|
+
textDocument: { uri: "file:///notebook.py" },
|
|
57
|
+
position: { line: 0, character: 0 },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await expect(client.textDocumentDefinition(params)).resolves.toEqual(
|
|
61
|
+
definition,
|
|
62
|
+
);
|
|
63
|
+
expect(client.hasCapability("textDocument/definition")).toBe(true);
|
|
64
|
+
expect(staticOnlyClient.textDocumentDefinition).not.toHaveBeenCalled();
|
|
65
|
+
expect(dynamicClient.textDocumentDefinition).toHaveBeenCalledWith(params);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("prefers the first client's willSaveWaitUntil edits regardless of latency", async () => {
|
|
69
|
+
const slowEdits: LSP.TextEdit[] = [
|
|
70
|
+
{
|
|
71
|
+
range: {
|
|
72
|
+
start: { line: 0, character: 0 },
|
|
73
|
+
end: { line: 0, character: 1 },
|
|
74
|
+
},
|
|
75
|
+
newText: "first",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
const first = createClient();
|
|
79
|
+
const second = createClient();
|
|
80
|
+
// Answers last, but still wins
|
|
81
|
+
first.textDocumentWillSaveWaitUntil.mockImplementation(
|
|
82
|
+
() => new Promise((resolve) => setTimeout(() => resolve(slowEdits), 5)),
|
|
83
|
+
);
|
|
84
|
+
second.textDocumentWillSaveWaitUntil.mockResolvedValue([
|
|
85
|
+
{
|
|
86
|
+
range: {
|
|
87
|
+
start: { line: 1, character: 0 },
|
|
88
|
+
end: { line: 1, character: 1 },
|
|
89
|
+
},
|
|
90
|
+
newText: "second",
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
const client = new FederatedLanguageServerClient([first, second]);
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
client.textDocumentWillSaveWaitUntil({
|
|
97
|
+
textDocument: { uri: "file:///notebook.py" },
|
|
98
|
+
reason: 1,
|
|
99
|
+
}),
|
|
100
|
+
).resolves.toEqual(slowEdits);
|
|
101
|
+
expect(second.textDocumentWillSaveWaitUntil).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("skips clients that reject willSaveWaitUntil", async () => {
|
|
105
|
+
const failing = createClient();
|
|
106
|
+
const working = createClient();
|
|
107
|
+
const edits: LSP.TextEdit[] = [
|
|
108
|
+
{
|
|
109
|
+
range: {
|
|
110
|
+
start: { line: 0, character: 0 },
|
|
111
|
+
end: { line: 0, character: 1 },
|
|
112
|
+
},
|
|
113
|
+
newText: "ok",
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
failing.textDocumentWillSaveWaitUntil.mockRejectedValue(
|
|
117
|
+
new Error("server exploded"),
|
|
118
|
+
);
|
|
119
|
+
working.textDocumentWillSaveWaitUntil.mockResolvedValue(edits);
|
|
120
|
+
const client = new FederatedLanguageServerClient([failing, working]);
|
|
121
|
+
|
|
122
|
+
await expect(
|
|
123
|
+
client.textDocumentWillSaveWaitUntil({
|
|
124
|
+
textDocument: { uri: "file:///notebook.py" },
|
|
125
|
+
reason: 1,
|
|
126
|
+
}),
|
|
127
|
+
).resolves.toEqual(edits);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("fans out document lifecycle and save notifications", async () => {
|
|
131
|
+
const first = createClient();
|
|
132
|
+
const second = createClient();
|
|
133
|
+
second.textDocumentDidOpen.mockResolvedValue(true);
|
|
134
|
+
const client = new FederatedLanguageServerClient([first, second]);
|
|
135
|
+
const openParams: LSP.DidOpenTextDocumentParams = {
|
|
136
|
+
textDocument: {
|
|
137
|
+
uri: "file:///notebook.py",
|
|
138
|
+
languageId: "python",
|
|
139
|
+
version: 1,
|
|
140
|
+
text: "value = 1",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const closeParams: LSP.DidCloseTextDocumentParams = {
|
|
144
|
+
textDocument: { uri: "file:///notebook.py" },
|
|
145
|
+
};
|
|
146
|
+
const saveParams: LSP.DidSaveTextDocumentParams = {
|
|
147
|
+
textDocument: { uri: "file:///notebook.py" },
|
|
148
|
+
text: "value = 1",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
await expect(client.textDocumentDidOpen(openParams)).resolves.toBe(true);
|
|
152
|
+
await client.textDocumentDidClose(closeParams);
|
|
153
|
+
await client.textDocumentDidSave(saveParams);
|
|
154
|
+
|
|
155
|
+
for (const child of [first, second]) {
|
|
156
|
+
expect(child.textDocumentDidOpen).toHaveBeenCalledWith(openParams);
|
|
157
|
+
expect(child.textDocumentDidClose).toHaveBeenCalledWith(closeParams);
|
|
158
|
+
expect(child.textDocumentDidSave).toHaveBeenCalledWith(saveParams);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|