@marimo-team/islands 0.23.7-dev50 → 0.23.7-dev52
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/{chat-ui-D8ZxPNTR.js → chat-ui-DCyW3OUK.js} +2 -2
- package/dist/{code-visibility-DjsEQuRb.js → code-visibility-Io6Yas7B.js} +2 -2
- package/dist/{html-to-image-DaPPaVDP.js → html-to-image-40ZXSWP-.js} +2519 -2354
- package/dist/main.js +5 -5
- package/dist/{process-output-n0RJTxcC.js → process-output-CCeeXIBd.js} +1 -1
- package/dist/{reveal-component-pQBz0vEK.js → reveal-component-CqXoE5Tn.js} +2 -2
- package/package.json +1 -1
- package/src/components/editor/file-tree/file-explorer.tsx +12 -2
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +152 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +99 -0
- package/src/core/codemirror/go-to-definition/commands.ts +382 -22
- package/src/core/codemirror/go-to-definition/utils.ts +23 -5
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { syntaxTree } from "@codemirror/language";
|
|
4
|
+
import type { EditorState } from "@codemirror/state";
|
|
4
5
|
import { EditorView } from "@codemirror/view";
|
|
6
|
+
import type { SyntaxNode, Tree, TreeCursor } from "@lezer/common";
|
|
7
|
+
|
|
8
|
+
const SCOPE_CREATING_NODES = new Set([
|
|
9
|
+
"FunctionDefinition",
|
|
10
|
+
"LambdaExpression",
|
|
11
|
+
"ArrayComprehensionExpression",
|
|
12
|
+
"SetComprehensionExpression",
|
|
13
|
+
"DictionaryComprehensionExpression",
|
|
14
|
+
"ComprehensionExpression",
|
|
15
|
+
"ClassDefinition",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const POSITION_SENSITIVE_SCOPES = new Set(["ClassDefinition"]);
|
|
19
|
+
|
|
20
|
+
interface ScopeContext {
|
|
21
|
+
id: number;
|
|
22
|
+
type: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface VariableDeclaration {
|
|
26
|
+
from: number;
|
|
27
|
+
scopeId: number;
|
|
28
|
+
}
|
|
5
29
|
|
|
6
30
|
function goToPosition(view: EditorView, from: number): void {
|
|
7
31
|
view.focus();
|
|
@@ -22,50 +46,386 @@ function goToPosition(view: EditorView, from: number): void {
|
|
|
22
46
|
});
|
|
23
47
|
}
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* for a given editor view.
|
|
28
|
-
* @param view The editor view which contains the variable name.
|
|
29
|
-
* @param variableName The name of the variable to select, if found in the editor.
|
|
30
|
-
*/
|
|
31
|
-
export function goToVariableDefinition(
|
|
32
|
-
view: EditorView,
|
|
49
|
+
function findFirstMatchingVariable(
|
|
50
|
+
state: EditorState,
|
|
33
51
|
variableName: string,
|
|
34
|
-
):
|
|
35
|
-
const { state } = view;
|
|
52
|
+
): number | null {
|
|
36
53
|
const tree = syntaxTree(state);
|
|
37
54
|
|
|
38
|
-
let
|
|
39
|
-
let from = 0;
|
|
55
|
+
let from: number | null = null;
|
|
40
56
|
|
|
41
57
|
tree.iterate({
|
|
42
58
|
enter: (node) => {
|
|
43
|
-
if (
|
|
59
|
+
if (from !== null) {
|
|
44
60
|
return false;
|
|
45
|
-
}
|
|
61
|
+
}
|
|
46
62
|
|
|
47
|
-
// Check if the node is an identifier and matches the variable name
|
|
48
63
|
if (
|
|
49
64
|
node.name === "VariableName" &&
|
|
50
65
|
state.doc.sliceString(node.from, node.to) === variableName
|
|
51
66
|
) {
|
|
52
67
|
from = node.from;
|
|
53
|
-
|
|
54
|
-
return false; // Stop traversal
|
|
68
|
+
return false;
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
// Skip comments and strings
|
|
58
71
|
if (node.name === "Comment" || node.name === "String") {
|
|
59
72
|
return false;
|
|
60
73
|
}
|
|
74
|
+
|
|
75
|
+
return undefined;
|
|
61
76
|
},
|
|
62
77
|
});
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
return from;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getScopeChain(tree: Tree, usagePosition: number): ScopeContext[] {
|
|
83
|
+
const scopeChain: ScopeContext[] = [];
|
|
84
|
+
let currentNode: SyntaxNode | null = tree.resolveInner(usagePosition, 0);
|
|
85
|
+
|
|
86
|
+
while (currentNode) {
|
|
87
|
+
if (SCOPE_CREATING_NODES.has(currentNode.name)) {
|
|
88
|
+
// Skip ClassDefinition if we've already seen a function/lambda.
|
|
89
|
+
const inFunctionLikeScope = scopeChain.some(
|
|
90
|
+
(scope) =>
|
|
91
|
+
scope.type === "FunctionDefinition" ||
|
|
92
|
+
scope.type === "LambdaExpression",
|
|
93
|
+
);
|
|
94
|
+
if (!(inFunctionLikeScope && currentNode.name === "ClassDefinition")) {
|
|
95
|
+
scopeChain.push({
|
|
96
|
+
id: currentNode.from,
|
|
97
|
+
type: currentNode.name,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
currentNode = currentNode.parent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
scopeChain.push({ id: -1, type: "global" });
|
|
105
|
+
return scopeChain;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function addDeclaration(
|
|
109
|
+
declarations: VariableDeclaration[],
|
|
110
|
+
scopeId: number,
|
|
111
|
+
from: number,
|
|
112
|
+
) {
|
|
113
|
+
declarations.push({ scopeId, from });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function traverseChildren(
|
|
117
|
+
cursor: TreeCursor,
|
|
118
|
+
callback: (node: SyntaxNode) => void,
|
|
119
|
+
) {
|
|
120
|
+
if (cursor.firstChild()) {
|
|
121
|
+
do {
|
|
122
|
+
callback(cursor.node);
|
|
123
|
+
} while (cursor.nextSibling());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectMatchingTargets(
|
|
128
|
+
cursor: TreeCursor,
|
|
129
|
+
state: EditorState,
|
|
130
|
+
variableName: string,
|
|
131
|
+
scopeId: number,
|
|
132
|
+
declarations: VariableDeclaration[],
|
|
133
|
+
) {
|
|
134
|
+
switch (cursor.name) {
|
|
135
|
+
case "VariableName":
|
|
136
|
+
if (state.doc.sliceString(cursor.from, cursor.to) === variableName) {
|
|
137
|
+
addDeclaration(declarations, scopeId, cursor.from);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case "TupleExpression":
|
|
142
|
+
case "ArrayExpression": {
|
|
143
|
+
const childCursor = cursor.node.cursor();
|
|
144
|
+
childCursor.firstChild();
|
|
145
|
+
do {
|
|
146
|
+
collectMatchingTargets(
|
|
147
|
+
childCursor,
|
|
148
|
+
state,
|
|
149
|
+
variableName,
|
|
150
|
+
scopeId,
|
|
151
|
+
declarations,
|
|
152
|
+
);
|
|
153
|
+
} while (childCursor.nextSibling());
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
break;
|
|
67
158
|
}
|
|
68
|
-
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collectFunctionParameters(
|
|
162
|
+
node: SyntaxNode | Tree,
|
|
163
|
+
state: EditorState,
|
|
164
|
+
variableName: string,
|
|
165
|
+
scopeId: number,
|
|
166
|
+
declarations: VariableDeclaration[],
|
|
167
|
+
) {
|
|
168
|
+
const cursor = node.cursor();
|
|
169
|
+
cursor.firstChild();
|
|
170
|
+
do {
|
|
171
|
+
if (cursor.name !== "ParamList") {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const paramCursor = cursor.node.cursor();
|
|
176
|
+
paramCursor.firstChild();
|
|
177
|
+
do {
|
|
178
|
+
if (
|
|
179
|
+
paramCursor.name === "VariableName" &&
|
|
180
|
+
state.doc.sliceString(paramCursor.from, paramCursor.to) === variableName
|
|
181
|
+
) {
|
|
182
|
+
addDeclaration(declarations, scopeId, paramCursor.from);
|
|
183
|
+
}
|
|
184
|
+
} while (paramCursor.nextSibling());
|
|
185
|
+
} while (cursor.nextSibling());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function collectForTargets(
|
|
189
|
+
node: SyntaxNode | Tree,
|
|
190
|
+
state: EditorState,
|
|
191
|
+
variableName: string,
|
|
192
|
+
scopeId: number,
|
|
193
|
+
declarations: VariableDeclaration[],
|
|
194
|
+
) {
|
|
195
|
+
const cursor = node.cursor();
|
|
196
|
+
cursor.firstChild();
|
|
197
|
+
let foundFor = false;
|
|
198
|
+
do {
|
|
199
|
+
if (cursor.name === "for") {
|
|
200
|
+
foundFor = true;
|
|
201
|
+
} else if (foundFor && cursor.name === "in") {
|
|
202
|
+
break;
|
|
203
|
+
} else if (foundFor) {
|
|
204
|
+
collectMatchingTargets(
|
|
205
|
+
cursor,
|
|
206
|
+
state,
|
|
207
|
+
variableName,
|
|
208
|
+
scopeId,
|
|
209
|
+
declarations,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
} while (cursor.nextSibling());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function collectMatchingDeclarations(
|
|
216
|
+
node: SyntaxNode | Tree,
|
|
217
|
+
state: EditorState,
|
|
218
|
+
variableName: string,
|
|
219
|
+
scopeStack: number[],
|
|
220
|
+
declarations: VariableDeclaration[],
|
|
221
|
+
) {
|
|
222
|
+
const cursor = node.cursor();
|
|
223
|
+
const nodeName = cursor.name;
|
|
224
|
+
const nodeStart = cursor.from;
|
|
225
|
+
|
|
226
|
+
const isNewScope = SCOPE_CREATING_NODES.has(nodeName);
|
|
227
|
+
const currentScopeStack = isNewScope
|
|
228
|
+
? [...scopeStack, nodeStart]
|
|
229
|
+
: scopeStack;
|
|
230
|
+
const currentScope = currentScopeStack[currentScopeStack.length - 1] ?? -1;
|
|
231
|
+
|
|
232
|
+
switch (nodeName) {
|
|
233
|
+
case "FunctionDefinition":
|
|
234
|
+
case "ClassDefinition": {
|
|
235
|
+
const subCursor = node.cursor();
|
|
236
|
+
subCursor.firstChild();
|
|
237
|
+
do {
|
|
238
|
+
if (
|
|
239
|
+
subCursor.name === "VariableName" &&
|
|
240
|
+
state.doc.sliceString(subCursor.from, subCursor.to) === variableName
|
|
241
|
+
) {
|
|
242
|
+
const parentScope = scopeStack[scopeStack.length - 1] ?? -1;
|
|
243
|
+
addDeclaration(declarations, parentScope, subCursor.from);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
} while (subCursor.nextSibling());
|
|
247
|
+
|
|
248
|
+
if (nodeName === "FunctionDefinition") {
|
|
249
|
+
collectFunctionParameters(
|
|
250
|
+
node,
|
|
251
|
+
state,
|
|
252
|
+
variableName,
|
|
253
|
+
nodeStart,
|
|
254
|
+
declarations,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case "LambdaExpression":
|
|
260
|
+
collectFunctionParameters(
|
|
261
|
+
node,
|
|
262
|
+
state,
|
|
263
|
+
variableName,
|
|
264
|
+
nodeStart,
|
|
265
|
+
declarations,
|
|
266
|
+
);
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case "ArrayComprehensionExpression":
|
|
270
|
+
case "DictionaryComprehensionExpression":
|
|
271
|
+
case "SetComprehensionExpression":
|
|
272
|
+
case "ComprehensionExpression":
|
|
273
|
+
case "ForStatement":
|
|
274
|
+
collectForTargets(node, state, variableName, currentScope, declarations);
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case "AssignStatement": {
|
|
278
|
+
const assignOpPositions: number[] = [];
|
|
279
|
+
const subCursor = node.cursor();
|
|
280
|
+
subCursor.firstChild();
|
|
281
|
+
do {
|
|
282
|
+
if (subCursor.name === "AssignOp") {
|
|
283
|
+
assignOpPositions.push(subCursor.from);
|
|
284
|
+
}
|
|
285
|
+
} while (subCursor.nextSibling());
|
|
286
|
+
|
|
287
|
+
const lastAssignOpPosition =
|
|
288
|
+
assignOpPositions[assignOpPositions.length - 1];
|
|
289
|
+
if (lastAssignOpPosition === undefined) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const targetCursor = node.cursor();
|
|
294
|
+
targetCursor.firstChild();
|
|
295
|
+
do {
|
|
296
|
+
if (targetCursor.from < lastAssignOpPosition) {
|
|
297
|
+
collectMatchingTargets(
|
|
298
|
+
targetCursor,
|
|
299
|
+
state,
|
|
300
|
+
variableName,
|
|
301
|
+
currentScope,
|
|
302
|
+
declarations,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
} while (targetCursor.nextSibling());
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "ImportStatement": {
|
|
309
|
+
const subCursor = node.cursor();
|
|
310
|
+
subCursor.firstChild();
|
|
311
|
+
do {
|
|
312
|
+
if (
|
|
313
|
+
subCursor.name === "VariableName" &&
|
|
314
|
+
state.doc.sliceString(subCursor.from, subCursor.to) === variableName
|
|
315
|
+
) {
|
|
316
|
+
addDeclaration(declarations, currentScope, subCursor.from);
|
|
317
|
+
}
|
|
318
|
+
} while (subCursor.nextSibling());
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "ImportFromStatement": {
|
|
322
|
+
const subCursor = node.cursor();
|
|
323
|
+
subCursor.firstChild();
|
|
324
|
+
let foundImport = false;
|
|
325
|
+
do {
|
|
326
|
+
if (subCursor.name === "import") {
|
|
327
|
+
foundImport = true;
|
|
328
|
+
} else if (
|
|
329
|
+
foundImport &&
|
|
330
|
+
subCursor.name === "VariableName" &&
|
|
331
|
+
state.doc.sliceString(subCursor.from, subCursor.to) === variableName
|
|
332
|
+
) {
|
|
333
|
+
addDeclaration(declarations, currentScope, subCursor.from);
|
|
334
|
+
}
|
|
335
|
+
} while (subCursor.nextSibling());
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "TryStatement":
|
|
339
|
+
case "WithStatement": {
|
|
340
|
+
const subCursor = node.cursor();
|
|
341
|
+
subCursor.firstChild();
|
|
342
|
+
let foundAs = false;
|
|
343
|
+
do {
|
|
344
|
+
if (subCursor.name === "as") {
|
|
345
|
+
foundAs = true;
|
|
346
|
+
} else if (
|
|
347
|
+
foundAs &&
|
|
348
|
+
subCursor.name === "VariableName" &&
|
|
349
|
+
state.doc.sliceString(subCursor.from, subCursor.to) === variableName
|
|
350
|
+
) {
|
|
351
|
+
addDeclaration(declarations, currentScope, subCursor.from);
|
|
352
|
+
foundAs = false;
|
|
353
|
+
}
|
|
354
|
+
} while (subCursor.nextSibling());
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
default:
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
traverseChildren(cursor, (childNode) => {
|
|
362
|
+
collectMatchingDeclarations(
|
|
363
|
+
childNode,
|
|
364
|
+
state,
|
|
365
|
+
variableName,
|
|
366
|
+
currentScopeStack,
|
|
367
|
+
declarations,
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function findScopedDefinitionPosition(
|
|
373
|
+
state: EditorState,
|
|
374
|
+
variableName: string,
|
|
375
|
+
usagePosition: number,
|
|
376
|
+
): number | null {
|
|
377
|
+
const tree = syntaxTree(state);
|
|
378
|
+
const declarations: VariableDeclaration[] = [];
|
|
379
|
+
|
|
380
|
+
collectMatchingDeclarations(tree, state, variableName, [], declarations);
|
|
381
|
+
|
|
382
|
+
const clampedUsagePosition = Math.max(
|
|
383
|
+
0,
|
|
384
|
+
Math.min(usagePosition, state.doc.length),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
for (const scope of getScopeChain(tree, clampedUsagePosition)) {
|
|
388
|
+
const match = declarations
|
|
389
|
+
.filter((declaration) => declaration.scopeId === scope.id)
|
|
390
|
+
.filter((declaration) => {
|
|
391
|
+
return POSITION_SENSITIVE_SCOPES.has(scope.type)
|
|
392
|
+
? declaration.from <= clampedUsagePosition
|
|
393
|
+
: true;
|
|
394
|
+
})
|
|
395
|
+
.toSorted((left, right) => left.from - right.from)[0];
|
|
396
|
+
|
|
397
|
+
if (match) {
|
|
398
|
+
return match.from;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* This function will select the first occurrence of the given variable name,
|
|
407
|
+
* for a given editor view.
|
|
408
|
+
* @param view The editor view which contains the variable name.
|
|
409
|
+
* @param variableName The name of the variable to select, if found in the editor.
|
|
410
|
+
* @param usagePosition The position of the variable usage, if available.
|
|
411
|
+
*/
|
|
412
|
+
export function goToVariableDefinition(
|
|
413
|
+
view: EditorView,
|
|
414
|
+
variableName: string,
|
|
415
|
+
usagePosition?: number,
|
|
416
|
+
): boolean {
|
|
417
|
+
const { state } = view;
|
|
418
|
+
const from =
|
|
419
|
+
(usagePosition !== undefined
|
|
420
|
+
? findScopedDefinitionPosition(state, variableName, usagePosition)
|
|
421
|
+
: null) ?? findFirstMatchingVariable(state, variableName);
|
|
422
|
+
|
|
423
|
+
if (from === null) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
goToPosition(view, from);
|
|
428
|
+
return true;
|
|
69
429
|
}
|
|
70
430
|
|
|
71
431
|
/**
|
|
@@ -18,10 +18,16 @@ function getWordUnderCursor(state: EditorState) {
|
|
|
18
18
|
const { from, to } = state.selection.main;
|
|
19
19
|
if (from === to) {
|
|
20
20
|
const { startToken, endToken } = getPositionAtWordBounds(state.doc, from);
|
|
21
|
-
return
|
|
21
|
+
return {
|
|
22
|
+
position: startToken,
|
|
23
|
+
word: state.doc.sliceString(startToken, endToken),
|
|
24
|
+
};
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
return
|
|
27
|
+
return {
|
|
28
|
+
position: from,
|
|
29
|
+
word: state.doc.sliceString(from, to),
|
|
30
|
+
};
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -51,15 +57,15 @@ function isPrivateVariable(variableName: string) {
|
|
|
51
57
|
*/
|
|
52
58
|
export function goToDefinitionAtCursorPosition(view: EditorView): boolean {
|
|
53
59
|
const { state } = view;
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
60
|
+
const { position, word } = getWordUnderCursor(state);
|
|
61
|
+
if (!word) {
|
|
56
62
|
return false;
|
|
57
63
|
}
|
|
58
64
|
// Close popovers/tooltips
|
|
59
65
|
closeCompletion(view);
|
|
60
66
|
view.dispatch({ effects: closeHoverTooltips });
|
|
61
67
|
|
|
62
|
-
return goToDefinition(view,
|
|
68
|
+
return goToDefinition(view, word, position);
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/**
|
|
@@ -69,7 +75,19 @@ export function goToDefinitionAtCursorPosition(view: EditorView): boolean {
|
|
|
69
75
|
export function goToDefinition(
|
|
70
76
|
view: EditorView,
|
|
71
77
|
variableName: string,
|
|
78
|
+
usagePosition?: number,
|
|
72
79
|
): boolean {
|
|
80
|
+
if (usagePosition !== undefined) {
|
|
81
|
+
const foundLocally = goToVariableDefinition(
|
|
82
|
+
view,
|
|
83
|
+
variableName,
|
|
84
|
+
usagePosition,
|
|
85
|
+
);
|
|
86
|
+
if (foundLocally) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
// The variable may exist in another cell
|
|
74
92
|
const editorWithVariable = getEditorForVariable(view, variableName);
|
|
75
93
|
if (!editorWithVariable) {
|