@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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { syntaxTree } from "@codemirror/language";
|
|
4
|
+
import type { EditorState } from "@codemirror/state";
|
|
5
|
+
import type { SyntaxNode, Tree } from "@lezer/common";
|
|
6
|
+
import {
|
|
7
|
+
NON_CODE_NODES,
|
|
8
|
+
PyKeyword,
|
|
9
|
+
PyNode,
|
|
10
|
+
SCOPE_CREATING_NODES,
|
|
11
|
+
} from "../python-node-names";
|
|
12
|
+
import { hasSyntaxErrors } from "../reactive-references/analyzer";
|
|
13
|
+
|
|
14
|
+
export interface CodeLensTarget {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Finds the first module-scope assignment site for each name in `names`.
|
|
22
|
+
*/
|
|
23
|
+
export function findDeclarationSites(options: {
|
|
24
|
+
state: EditorState;
|
|
25
|
+
names: ReadonlySet<string>;
|
|
26
|
+
}): CodeLensTarget[] {
|
|
27
|
+
const { state, names } = options;
|
|
28
|
+
if (names.size === 0) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const tree = syntaxTree(state);
|
|
32
|
+
if (!tree || hasSyntaxErrors(tree)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const firstSites = new Map<string, CodeLensTarget>();
|
|
37
|
+
|
|
38
|
+
const collectTargetNames = (node: SyntaxNode) => {
|
|
39
|
+
if (node.name === PyNode.VariableName) {
|
|
40
|
+
const name = state.doc.sliceString(node.from, node.to);
|
|
41
|
+
if (names.has(name) && !firstSites.has(name)) {
|
|
42
|
+
firstSites.set(name, { from: node.from, to: node.to, name });
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Recurse into tuple/list unpacking, e.g. `(a, b) = ...` or `[a, b] = ...`,
|
|
47
|
+
// and parenthesized targets, e.g. `(df) = ...`.
|
|
48
|
+
// Member/subscript targets (`obj.attr = ...`) are not declarations.
|
|
49
|
+
if (
|
|
50
|
+
node.name === PyNode.TupleExpression ||
|
|
51
|
+
node.name === PyNode.ArrayExpression ||
|
|
52
|
+
node.name === PyNode.ParenthesizedExpression
|
|
53
|
+
) {
|
|
54
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
55
|
+
collectTargetNames(child);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const collectAssignmentTargets = (assign: SyntaxNode) => {
|
|
61
|
+
// Targets are everything before the last AssignOp (handles `a = b = ...`).
|
|
62
|
+
// A bare annotation (`x: int`) has no AssignOp and is skipped.
|
|
63
|
+
let lastAssignOp = -1;
|
|
64
|
+
for (let child = assign.firstChild; child; child = child.nextSibling) {
|
|
65
|
+
if (child.name === PyNode.AssignOp) {
|
|
66
|
+
lastAssignOp = child.from;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (lastAssignOp === -1) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (
|
|
73
|
+
let child = assign.firstChild;
|
|
74
|
+
child && child.from < lastAssignOp;
|
|
75
|
+
child = child.nextSibling
|
|
76
|
+
) {
|
|
77
|
+
collectTargetNames(child);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const visit = (node: SyntaxNode) => {
|
|
82
|
+
// Only module-scope assignments count as creation sites; top-level
|
|
83
|
+
// `if`/`for`/`try` bodies are still module scope.
|
|
84
|
+
if (SCOPE_CREATING_NODES.has(node.name)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (node.name === PyNode.AssignStatement) {
|
|
88
|
+
collectAssignmentTargets(node);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
92
|
+
visit(child);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
visit(tree.topNode);
|
|
96
|
+
|
|
97
|
+
return [...firstSites.values()];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Note that the cache pattern assumes users `import marimo as mo`.
|
|
101
|
+
// This is a common convention for marimo notebooks.
|
|
102
|
+
const CACHE_PATTERN = /\bmo\.(?:persistent_cache|cache)\b/g;
|
|
103
|
+
|
|
104
|
+
export interface CacheSite {
|
|
105
|
+
from: number;
|
|
106
|
+
to: number;
|
|
107
|
+
/**
|
|
108
|
+
* Variable the cache is bound to: the decorated function name
|
|
109
|
+
* (`@mo.cache def f`), the assignment target (`g = mo.cache(f)`), or the
|
|
110
|
+
* `as` binding (`with mo.persistent_cache("k") as c`).
|
|
111
|
+
*/
|
|
112
|
+
boundName: string | null;
|
|
113
|
+
/** First string argument, e.g. the persistent_cache name. */
|
|
114
|
+
cacheName: string | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stripStringQuotes(text: string): string {
|
|
118
|
+
const match = text.match(/^[A-Za-z]*("""|'''|"|')([\s\S]*)\1$/);
|
|
119
|
+
return match ? match[2] : text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extracts a static cache name from the first positional argument or an
|
|
124
|
+
* explicit `name=` argument. Other string-valued options, such as
|
|
125
|
+
* `hash_type="content"` and `save_path="cache"`, are not cache names.
|
|
126
|
+
*/
|
|
127
|
+
function findStaticCacheName(
|
|
128
|
+
argList: SyntaxNode | null,
|
|
129
|
+
text: (node: SyntaxNode) => string,
|
|
130
|
+
): string | null {
|
|
131
|
+
if (!argList) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isArgumentBoundary = (node: SyntaxNode | null) =>
|
|
136
|
+
node?.name === "," || node?.name === ")";
|
|
137
|
+
|
|
138
|
+
let firstArgument: SyntaxNode | null = null;
|
|
139
|
+
for (let child = argList.firstChild; child; child = child.nextSibling) {
|
|
140
|
+
if (child.name === "(" || child.name === ")" || child.name === ",") {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
firstArgument ??= child;
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
child.name === PyNode.VariableName &&
|
|
147
|
+
text(child) === "name" &&
|
|
148
|
+
child.nextSibling?.name === PyNode.AssignOp
|
|
149
|
+
) {
|
|
150
|
+
const value = child.nextSibling.nextSibling;
|
|
151
|
+
if (
|
|
152
|
+
value?.name === PyNode.String &&
|
|
153
|
+
isArgumentBoundary(value.nextSibling)
|
|
154
|
+
) {
|
|
155
|
+
return stripStringQuotes(text(value));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!firstArgument) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
firstArgument.name === PyNode.String &&
|
|
165
|
+
isArgumentBoundary(firstArgument.nextSibling)
|
|
166
|
+
) {
|
|
167
|
+
return stripStringQuotes(text(firstArgument));
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolves a `mo.cache` / `mo.persistent_cache` match into a CacheSite:
|
|
174
|
+
* extends `to` past call arguments (`mo.cache(pin_modules=True)`), so an
|
|
175
|
+
* icon placed at `to` lands after the closing paren, and extracts the bound
|
|
176
|
+
* variable name and the cache-name string argument.
|
|
177
|
+
*/
|
|
178
|
+
function analyzeCacheSite(
|
|
179
|
+
state: EditorState,
|
|
180
|
+
tree: Tree,
|
|
181
|
+
from: number,
|
|
182
|
+
to: number,
|
|
183
|
+
): CacheSite {
|
|
184
|
+
const text = (node: SyntaxNode) => state.doc.sliceString(node.from, node.to);
|
|
185
|
+
const node = tree.resolveInner(from, 1);
|
|
186
|
+
let boundName: string | null = null;
|
|
187
|
+
let argList: SyntaxNode | null = null;
|
|
188
|
+
let end = to;
|
|
189
|
+
|
|
190
|
+
// `mo.cache(...)` as an expression wraps in a CallExpression that starts
|
|
191
|
+
// at the match
|
|
192
|
+
let call: SyntaxNode | null = null;
|
|
193
|
+
for (
|
|
194
|
+
let ancestor: SyntaxNode | null = node.parent;
|
|
195
|
+
ancestor && ancestor.from === from;
|
|
196
|
+
ancestor = ancestor.parent
|
|
197
|
+
) {
|
|
198
|
+
if (ancestor.name === PyNode.CallExpression) {
|
|
199
|
+
call = ancestor;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (call) {
|
|
205
|
+
end = call.to;
|
|
206
|
+
argList = call.getChild(PyNode.ArgList);
|
|
207
|
+
const parent = call.parent;
|
|
208
|
+
if (parent?.name === PyNode.AssignStatement) {
|
|
209
|
+
// `g = mo.cache(f)` — the direct VariableName child is the target
|
|
210
|
+
const target = parent.getChild(PyNode.VariableName);
|
|
211
|
+
boundName = target && target.from < call.from ? text(target) : null;
|
|
212
|
+
} else if (parent?.name === PyNode.WithStatement) {
|
|
213
|
+
// `with mo.persistent_cache("k") as c:` — `as c` follows the call
|
|
214
|
+
let child = parent.firstChild;
|
|
215
|
+
while (child) {
|
|
216
|
+
if (child.from < call.to || child.name === PyKeyword.As) {
|
|
217
|
+
child = child.nextSibling;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (child.name === PyNode.VariableName) {
|
|
221
|
+
boundName = text(child);
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else if (node.parent?.name === PyNode.Decorator) {
|
|
227
|
+
// `@mo.cache(...)` decorators are flat: the ArgList is a direct sibling
|
|
228
|
+
const decorator = node.parent;
|
|
229
|
+
const decoratorArgs = decorator.getChild(PyNode.ArgList);
|
|
230
|
+
if (decoratorArgs?.from === to) {
|
|
231
|
+
argList = decoratorArgs;
|
|
232
|
+
end = decoratorArgs.to;
|
|
233
|
+
}
|
|
234
|
+
// The decorated function's name
|
|
235
|
+
const fn = decorator.parent?.getChild(PyNode.FunctionDefinition);
|
|
236
|
+
const fnName = fn?.getChild(PyNode.VariableName);
|
|
237
|
+
boundName = fnName ? text(fnName) : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const cacheName = findStaticCacheName(argList, text);
|
|
241
|
+
return { from, to: end, boundName, cacheName };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* True when the `mo` at `from` is a standalone module reference (the object of
|
|
246
|
+
* `mo.cache`), not an attribute of something else. This filters out chains
|
|
247
|
+
* like `obj.mo.cache(f)` — where `mo` is a `PropertyName` rather than a
|
|
248
|
+
* `VariableName` — and mentions inside comments or strings.
|
|
249
|
+
*/
|
|
250
|
+
function isStandaloneMoReference(
|
|
251
|
+
state: EditorState,
|
|
252
|
+
tree: Tree,
|
|
253
|
+
from: number,
|
|
254
|
+
): boolean {
|
|
255
|
+
const node = tree.resolveInner(from, 1);
|
|
256
|
+
if (NON_CODE_NODES.has(node.name)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
// In `obj.mo.cache`, `mo` parses as a `PropertyName`; a standalone `mo`
|
|
260
|
+
// (including whitespace-separated `mo . cache`) parses as a `VariableName`.
|
|
261
|
+
return (
|
|
262
|
+
node.name === PyNode.VariableName &&
|
|
263
|
+
state.doc.sliceString(node.from, node.to) === "mo"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Finds occurrences of `mo.cache` / `mo.persistent_cache` (as a decorator,
|
|
269
|
+
* call, or context manager) with a simple text match, skipping mentions in
|
|
270
|
+
* comments, strings, and attribute chains (`obj.mo.cache`). `to` extends past
|
|
271
|
+
* call arguments when present.
|
|
272
|
+
*/
|
|
273
|
+
export function findCacheSites(state: EditorState): CacheSite[] {
|
|
274
|
+
const tree = syntaxTree(state);
|
|
275
|
+
if (!tree || hasSyntaxErrors(tree)) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
return [...state.doc.toString().matchAll(CACHE_PATTERN)]
|
|
279
|
+
.filter((match) => isStandaloneMoReference(state, tree, match.index ?? 0))
|
|
280
|
+
.map((match) => {
|
|
281
|
+
const from = match.index ?? 0;
|
|
282
|
+
return analyzeCacheSite(state, tree, from, from + match[0].length);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { atom } from "jotai";
|
|
4
|
+
import type { CellId } from "@/core/cells/ids";
|
|
5
|
+
import { dataConnectionsMapAtom } from "@/core/datasets/data-source-connections";
|
|
6
|
+
import { INTERNAL_SQL_ENGINES } from "@/core/datasets/engines";
|
|
7
|
+
import { datasetTablesAtom } from "@/core/datasets/state";
|
|
8
|
+
import { store } from "@/core/state/jotai";
|
|
9
|
+
import { storageNamespacesAtom } from "@/core/storage/state";
|
|
10
|
+
import { variablesAtom } from "@/core/variables/state";
|
|
11
|
+
import type { VariableName } from "@/core/variables/types";
|
|
12
|
+
import { Logger } from "@/utils/Logger";
|
|
13
|
+
|
|
14
|
+
export type CodeLensKind = "table" | "connection" | "bucket" | "cache";
|
|
15
|
+
|
|
16
|
+
export interface CodeLensSpec {
|
|
17
|
+
/** Document position the icon is anchored at */
|
|
18
|
+
pos: number;
|
|
19
|
+
kind: CodeLensKind;
|
|
20
|
+
/** Variable name for entities; a stable identity key for cache sites */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Extra context for cache sites */
|
|
23
|
+
cache?: {
|
|
24
|
+
boundName: string | null;
|
|
25
|
+
cacheName: string | null;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sets `name` to `kind`, logging (and overwriting) if the name was already
|
|
31
|
+
* claimed by a different kind. This should be rare.
|
|
32
|
+
*/
|
|
33
|
+
function setEntity(
|
|
34
|
+
entities: Map<string, CodeLensKind>,
|
|
35
|
+
name: string,
|
|
36
|
+
kind: CodeLensKind,
|
|
37
|
+
): void {
|
|
38
|
+
const existing = entities.get(name);
|
|
39
|
+
if (existing !== undefined && existing !== kind) {
|
|
40
|
+
Logger.error(
|
|
41
|
+
`[code-lens] "${name}" is claimed by both a ${existing} and a ${kind} entity; keeping ${kind}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
entities.set(name, kind);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* All datasource (dataframe/SQL engine) and storage bucket names in the
|
|
49
|
+
* notebook, keyed by name, independent of any particular cell.
|
|
50
|
+
*/
|
|
51
|
+
const lensEntityKindsAtom = atom((get) => {
|
|
52
|
+
const entities = new Map<string, CodeLensKind>();
|
|
53
|
+
for (const table of get(datasetTablesAtom)) {
|
|
54
|
+
if (table.variable_name) {
|
|
55
|
+
setEntity(entities, table.variable_name, "table");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const name of get(dataConnectionsMapAtom).keys()) {
|
|
59
|
+
if (!INTERNAL_SQL_ENGINES.has(name)) {
|
|
60
|
+
setEntity(entities, name, "connection");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const namespace of get(storageNamespacesAtom)) {
|
|
64
|
+
setEntity(entities, namespace.name, "bucket");
|
|
65
|
+
}
|
|
66
|
+
return entities;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
interface LensEntitiesByCell {
|
|
70
|
+
byCell: Map<CellId, Map<string, CodeLensKind>>;
|
|
71
|
+
/**
|
|
72
|
+
* Entities the kernel hasn't reported a variable for yet (still executing,
|
|
73
|
+
* or just typed), so they can't be attributed to a declaring cell.
|
|
74
|
+
*/
|
|
75
|
+
pending: Map<string, CodeLensKind>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Groups `lensEntityKindsAtom` by declaring cell, once per store update.
|
|
80
|
+
* With C cells and T datasource/bucket entities, this turns
|
|
81
|
+
* an O(C x T) per-round cost (every cell re-scanning every entity) into O(T) here.
|
|
82
|
+
*/
|
|
83
|
+
const lensEntitiesByCellAtom = atom((get): LensEntitiesByCell => {
|
|
84
|
+
const kinds = get(lensEntityKindsAtom);
|
|
85
|
+
const variables = get(variablesAtom);
|
|
86
|
+
const byCell = new Map<CellId, Map<string, CodeLensKind>>();
|
|
87
|
+
const pending = new Map<string, CodeLensKind>();
|
|
88
|
+
|
|
89
|
+
for (const [name, kind] of kinds) {
|
|
90
|
+
const variable = variables[name as VariableName];
|
|
91
|
+
if (variable == null) {
|
|
92
|
+
pending.set(name, kind);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const declaringCell of variable.declaredBy) {
|
|
96
|
+
let forCell = byCell.get(declaringCell);
|
|
97
|
+
if (!forCell) {
|
|
98
|
+
forCell = new Map();
|
|
99
|
+
byCell.set(declaringCell, forCell);
|
|
100
|
+
}
|
|
101
|
+
forCell.set(name, kind);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { byCell, pending };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Variables declared in `cellId` that are datasources (dataframes and SQL
|
|
110
|
+
* engines) or storage buckets, keyed by variable name.
|
|
111
|
+
*/
|
|
112
|
+
export function getLensEntities(cellId: CellId): Map<string, CodeLensKind> {
|
|
113
|
+
const { byCell, pending } = store.get(lensEntitiesByCellAtom);
|
|
114
|
+
const entities = new Map(byCell.get(cellId));
|
|
115
|
+
// Be permissive when the kernel hasn't reported a variable yet: any cell
|
|
116
|
+
// could turn out to be the declaring one.
|
|
117
|
+
for (const [name, kind] of pending) {
|
|
118
|
+
if (!entities.has(name)) {
|
|
119
|
+
entities.set(name, kind);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return entities;
|
|
123
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { type Extension, StateEffect, StateField } from "@codemirror/state";
|
|
4
|
+
import {
|
|
5
|
+
Decoration,
|
|
6
|
+
type DecorationSet,
|
|
7
|
+
EditorView,
|
|
8
|
+
showTooltip,
|
|
9
|
+
type Tooltip,
|
|
10
|
+
ViewPlugin,
|
|
11
|
+
type ViewUpdate,
|
|
12
|
+
WidgetType,
|
|
13
|
+
} from "@codemirror/view";
|
|
14
|
+
import { type DebouncedFunc, debounce } from "lodash-es";
|
|
15
|
+
import type { CellId } from "@/core/cells/ids";
|
|
16
|
+
import { getFeatureFlag } from "@/core/config/feature-flag";
|
|
17
|
+
import { dataConnectionsMapAtom } from "@/core/datasets/data-source-connections";
|
|
18
|
+
import { datasetTablesAtom } from "@/core/datasets/state";
|
|
19
|
+
import { store } from "@/core/state/jotai";
|
|
20
|
+
import { storageNamespacesAtom } from "@/core/storage/state";
|
|
21
|
+
import { variablesAtom } from "@/core/variables/state";
|
|
22
|
+
import { languageAdapterState } from "../language/extension";
|
|
23
|
+
import { openLensTarget } from "./actions";
|
|
24
|
+
import { findCacheSites, findDeclarationSites } from "./analyzer";
|
|
25
|
+
import { type CodeLensSpec, getLensEntities } from "./entities";
|
|
26
|
+
import { LENS_ICONS, LENS_TOOLTIPS } from "./icons";
|
|
27
|
+
import { mountLensPopover } from "./popover";
|
|
28
|
+
|
|
29
|
+
// Delay (in ms) before showing the hover popover, matching the app's
|
|
30
|
+
// tooltip delay
|
|
31
|
+
export const CODE_LENS_HOVER_DELAY_MS = 400;
|
|
32
|
+
|
|
33
|
+
const setCodeLenses = StateEffect.define<CodeLensSpec[]>();
|
|
34
|
+
const setHoveredLens = StateEffect.define<CodeLensSpec | null>();
|
|
35
|
+
|
|
36
|
+
// Pending hover timers, keyed by widget element so `destroy` can cancel them
|
|
37
|
+
const HOVER_TIMERS = new WeakMap<HTMLElement, number>();
|
|
38
|
+
|
|
39
|
+
class CodeLensWidget extends WidgetType {
|
|
40
|
+
private readonly spec: CodeLensSpec;
|
|
41
|
+
|
|
42
|
+
constructor(spec: CodeLensSpec) {
|
|
43
|
+
super();
|
|
44
|
+
this.spec = spec;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override eq(other: CodeLensWidget): boolean {
|
|
48
|
+
return (
|
|
49
|
+
// `pos` is captured by the DOM hover/click handlers, so a reused widget
|
|
50
|
+
// whose anchor moved must not be treated as equal
|
|
51
|
+
this.spec.pos === other.spec.pos &&
|
|
52
|
+
this.spec.kind === other.spec.kind &&
|
|
53
|
+
this.spec.name === other.spec.name &&
|
|
54
|
+
this.spec.cache?.boundName === other.spec.cache?.boundName &&
|
|
55
|
+
this.spec.cache?.cacheName === other.spec.cache?.cacheName
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override toDOM(view: EditorView): HTMLElement {
|
|
60
|
+
const { spec } = this;
|
|
61
|
+
const element = document.createElement("span");
|
|
62
|
+
element.className = "mo-code-lens";
|
|
63
|
+
element.setAttribute("role", "button");
|
|
64
|
+
// Focusable so the action is reachable and activatable by keyboard
|
|
65
|
+
element.tabIndex = 0;
|
|
66
|
+
// No `title`: the native tooltip is replaced by the hover popover
|
|
67
|
+
element.setAttribute("aria-label", LENS_TOOLTIPS[spec.kind]);
|
|
68
|
+
// Static, trusted markup (see icons.ts)
|
|
69
|
+
element.innerHTML = LENS_ICONS[spec.kind];
|
|
70
|
+
const hidePopover = () => {
|
|
71
|
+
window.clearTimeout(HOVER_TIMERS.get(element));
|
|
72
|
+
HOVER_TIMERS.delete(element);
|
|
73
|
+
if (view.state.field(codeLensHoverField, false)) {
|
|
74
|
+
view.dispatch({ effects: setHoveredLens.of(null) });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
element.onmouseenter = () => {
|
|
78
|
+
const timer = window.setTimeout(() => {
|
|
79
|
+
view.dispatch({ effects: setHoveredLens.of(spec) });
|
|
80
|
+
}, CODE_LENS_HOVER_DELAY_MS);
|
|
81
|
+
HOVER_TIMERS.set(element, timer);
|
|
82
|
+
};
|
|
83
|
+
element.onmouseleave = hidePopover;
|
|
84
|
+
element.onmousemove = (event) => {
|
|
85
|
+
// Keep the editor's built-in hover documentation tooltip from also
|
|
86
|
+
// triggering over the icon
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
};
|
|
89
|
+
element.onmousedown = (event) => {
|
|
90
|
+
// Don't move the cursor or steal focus from the editor
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
};
|
|
94
|
+
element.onclick = (event) => {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
event.stopPropagation();
|
|
97
|
+
hidePopover();
|
|
98
|
+
openLensTarget(spec.kind);
|
|
99
|
+
};
|
|
100
|
+
element.onkeydown = (event) => {
|
|
101
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
event.stopPropagation();
|
|
104
|
+
hidePopover();
|
|
105
|
+
openLensTarget(spec.kind);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return element;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override destroy(dom: HTMLElement): void {
|
|
112
|
+
window.clearTimeout(HOVER_TIMERS.get(dom));
|
|
113
|
+
HOVER_TIMERS.delete(dom);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override ignoreEvent(): boolean {
|
|
117
|
+
// The widget handles its own events
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createLensTooltip(spec: CodeLensSpec): Tooltip {
|
|
123
|
+
return {
|
|
124
|
+
pos: spec.pos,
|
|
125
|
+
above: true,
|
|
126
|
+
create: () => {
|
|
127
|
+
const dom = document.createElement("div");
|
|
128
|
+
// Same chrome as the SQL completion/hover popovers
|
|
129
|
+
dom.classList.add("mo-cm-tooltip", "docs-documentation");
|
|
130
|
+
const unmount = mountLensPopover(dom, spec);
|
|
131
|
+
return { dom, resize: false, destroy: unmount };
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const codeLensHoverField = StateField.define<Tooltip | null>({
|
|
137
|
+
create() {
|
|
138
|
+
return null;
|
|
139
|
+
},
|
|
140
|
+
update(tooltip, tr) {
|
|
141
|
+
for (const effect of tr.effects) {
|
|
142
|
+
if (effect.is(setHoveredLens)) {
|
|
143
|
+
return effect.value ? createLensTooltip(effect.value) : null;
|
|
144
|
+
}
|
|
145
|
+
if (effect.is(setCodeLenses)) {
|
|
146
|
+
// Lenses were rebuilt; the hovered widget may be gone
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return tr.docChanged ? null : tooltip;
|
|
151
|
+
},
|
|
152
|
+
provide: (field) => showTooltip.from(field),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const codeLensField = StateField.define<DecorationSet>({
|
|
156
|
+
create() {
|
|
157
|
+
return Decoration.none;
|
|
158
|
+
},
|
|
159
|
+
update(decorations, tr) {
|
|
160
|
+
decorations = decorations.map(tr.changes);
|
|
161
|
+
for (const effect of tr.effects) {
|
|
162
|
+
if (effect.is(setCodeLenses)) {
|
|
163
|
+
decorations = Decoration.set(
|
|
164
|
+
effect.value.map((spec) =>
|
|
165
|
+
Decoration.widget({
|
|
166
|
+
widget: new CodeLensWidget(spec),
|
|
167
|
+
side: 1,
|
|
168
|
+
}).range(spec.pos),
|
|
169
|
+
),
|
|
170
|
+
// Variable and cache lenses are collected separately, so sort
|
|
171
|
+
true,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return decorations;
|
|
176
|
+
},
|
|
177
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Plugin that keeps code lens decorations in sync with the document and the
|
|
182
|
+
* datasource/bucket state.
|
|
183
|
+
*/
|
|
184
|
+
class CodeLensPlugin {
|
|
185
|
+
private readonly view: EditorView;
|
|
186
|
+
private readonly cellId: CellId;
|
|
187
|
+
private readonly includeCache: boolean;
|
|
188
|
+
private readonly unsubscribes: Array<() => void>;
|
|
189
|
+
|
|
190
|
+
// Delay (in ms) before recomputing lenses after user changes or store updates
|
|
191
|
+
private readonly debounceMs = 300;
|
|
192
|
+
private readonly scheduleUpdate: DebouncedFunc<() => void>;
|
|
193
|
+
|
|
194
|
+
constructor(view: EditorView, cellId: CellId, includeCache: boolean) {
|
|
195
|
+
this.view = view;
|
|
196
|
+
this.cellId = cellId;
|
|
197
|
+
this.includeCache = includeCache;
|
|
198
|
+
this.scheduleUpdate = debounce(() => this.run(), this.debounceMs);
|
|
199
|
+
const onStoreChange = () => this.scheduleUpdate();
|
|
200
|
+
this.unsubscribes = [
|
|
201
|
+
store.sub(datasetTablesAtom, onStoreChange),
|
|
202
|
+
store.sub(dataConnectionsMapAtom, onStoreChange),
|
|
203
|
+
store.sub(storageNamespacesAtom, onStoreChange),
|
|
204
|
+
// The declaring-cell filter depends on variables
|
|
205
|
+
store.sub(variablesAtom, onStoreChange),
|
|
206
|
+
];
|
|
207
|
+
this.scheduleUpdate();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
update(update: ViewUpdate) {
|
|
211
|
+
// Recompute on edits, and when the cell's language changes (e.g. Python ->
|
|
212
|
+
// SQL) so stale Python-only icons are cleared even if the text is unchanged
|
|
213
|
+
const adapterChanged =
|
|
214
|
+
update.startState.field(languageAdapterState, false)?.type !==
|
|
215
|
+
update.state.field(languageAdapterState, false)?.type;
|
|
216
|
+
if (update.docChanged || adapterChanged) {
|
|
217
|
+
this.scheduleUpdate();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
destroy() {
|
|
222
|
+
this.scheduleUpdate.cancel();
|
|
223
|
+
for (const unsubscribe of this.unsubscribes) {
|
|
224
|
+
unsubscribe();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private run() {
|
|
229
|
+
const { state } = this.view;
|
|
230
|
+
const lenses: CodeLensSpec[] = [];
|
|
231
|
+
|
|
232
|
+
// Only python cells: SQL/markdown docs aren't python, and a cache icon
|
|
233
|
+
// inside a SQL string would be misleading.
|
|
234
|
+
const adapterType = state.field(languageAdapterState, false)?.type;
|
|
235
|
+
if (adapterType == null || adapterType === "python") {
|
|
236
|
+
const entities = getLensEntities(this.cellId);
|
|
237
|
+
const targets = findDeclarationSites({
|
|
238
|
+
state,
|
|
239
|
+
names: new Set(entities.keys()),
|
|
240
|
+
});
|
|
241
|
+
for (const target of targets) {
|
|
242
|
+
const kind = entities.get(target.name);
|
|
243
|
+
if (kind) {
|
|
244
|
+
lenses.push({ pos: target.to, kind, name: target.name });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (this.includeCache) {
|
|
248
|
+
for (const site of findCacheSites(state)) {
|
|
249
|
+
lenses.push({
|
|
250
|
+
pos: site.to,
|
|
251
|
+
kind: "cache",
|
|
252
|
+
name: `cache:${site.from}`,
|
|
253
|
+
cache: { boundName: site.boundName, cacheName: site.cacheName },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Defer dispatch to avoid triggering during an editor update cycle
|
|
260
|
+
queueMicrotask(() => {
|
|
261
|
+
this.view.dispatch({ effects: setCodeLenses.of(lenses) });
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const codeLensTheme = EditorView.baseTheme({
|
|
267
|
+
".mo-code-lens": {
|
|
268
|
+
display: "inline-flex",
|
|
269
|
+
verticalAlign: "baseline",
|
|
270
|
+
// Optically center the icon against the text
|
|
271
|
+
transform: "translateY(1.5px)",
|
|
272
|
+
marginLeft: "0.3em",
|
|
273
|
+
cursor: "pointer",
|
|
274
|
+
opacity: "0.5",
|
|
275
|
+
},
|
|
276
|
+
".mo-code-lens:hover": {
|
|
277
|
+
opacity: "1",
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Inline icons linking datasource/bucket variables and `mo.cache` /
|
|
283
|
+
* `mo.persistent_cache` calls to their panels.
|
|
284
|
+
* Gated behind the `editor_code_lens` feature flag.
|
|
285
|
+
*/
|
|
286
|
+
export function codeLensBundle(cellId: CellId): Extension {
|
|
287
|
+
if (!getFeatureFlag("editor_code_lens")) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
return [
|
|
291
|
+
codeLensField,
|
|
292
|
+
codeLensHoverField,
|
|
293
|
+
ViewPlugin.define(
|
|
294
|
+
(view) => new CodeLensPlugin(view, cellId, getFeatureFlag("cache_panel")),
|
|
295
|
+
),
|
|
296
|
+
codeLensTheme,
|
|
297
|
+
];
|
|
298
|
+
}
|