@marimo-team/islands 0.23.12-dev2 → 0.23.12-dev20

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.
Files changed (106) hide show
  1. package/dist/{ConnectedDataExplorerComponent-WqG-xX4l.js → ConnectedDataExplorerComponent-Du3_nUzI.js} +13 -13
  2. package/dist/{ErrorBoundary-BNx_OSVo.js → ErrorBoundary-DE6tzZf-.js} +2 -2
  3. package/dist/{any-language-editor-rPSlOll9.js → any-language-editor-DN1R-1KZ.js} +5 -5
  4. package/dist/{button-vQhauTmO.js → button-BacYv-bE.js} +7 -1
  5. package/dist/{capabilities-BEHzIS99.js → capabilities-D_4LYhSU.js} +1 -1
  6. package/dist/{chat-ui-k2kqhCv5.js → chat-ui-CsPewo4h.js} +16 -16
  7. package/dist/{check-nrzHDi45.js → check-C9OoNtR4.js} +1 -1
  8. package/dist/{code-visibility-DZ_6U5hT.js → code-visibility-02AuLxDs.js} +664 -663
  9. package/dist/{copy-UhDed7D4.js → copy-COam1EG7.js} +2 -2
  10. package/dist/{dist-DYGLrbYQ.js → dist--2Bqjvs0.js} +2 -2
  11. package/dist/{error-banner-BHAkVFc2.js → error-banner-DFPfz_Qf.js} +2 -2
  12. package/dist/{esm-Bqu9AE2K.js → esm-M837UxV5.js} +1 -1
  13. package/dist/{extends-9Yl5BEcg.js → extends-9MVIxxRo.js} +4 -4
  14. package/dist/{formats-BV4bOfMI.js → formats-d6MhLuQ9.js} +4 -4
  15. package/dist/{glide-data-editor-BDTq6YUb.js → glide-data-editor-DkzAInWG.js} +9 -9
  16. package/dist/{html-to-image-C86pQALH.js → html-to-image-DXwLcQ6l.js} +95 -88
  17. package/dist/{input-AKkGXdyV.js → input-CbEz_aj_.js} +6 -6
  18. package/dist/{label-E3ZJXHu8.js → label-WfTSU8L4.js} +2 -2
  19. package/dist/{loader-YPuQvn1Y.js → loader-Boph2xIS.js} +1 -1
  20. package/dist/main.js +1753 -1626
  21. package/dist/{mermaid-QFAR9YgY.js → mermaid-CJW9vIyO.js} +5 -5
  22. package/dist/{process-output-nNw4OpSj.js → process-output-C6_e1pT_.js} +3 -3
  23. package/dist/{reveal-component-BxDb5eK0.js → reveal-component-CX0nM3qj.js} +11 -11
  24. package/dist/{spec-B45_YCNI.js → spec-Bv-XlYiv.js} +4 -4
  25. package/dist/{strings-Cq2s9_EQ.js → strings-Dq_j3Rxw.js} +4 -4
  26. package/dist/style.css +2 -2
  27. package/dist/{swiper-component-BNa_4kh2.js → swiper-component-5HoSsPi1.js} +2 -2
  28. package/dist/{toDate-Do1xRzAo.js → toDate-D-l5s8nn.js} +3 -3
  29. package/dist/{tooltip-Bz3OAwrU.js → tooltip-Czds6Qr8.js} +3 -3
  30. package/dist/{types-D8gEGs4R.js → types-C2Ir191_.js} +1 -1
  31. package/dist/{useAsyncData-CL3o2p4i.js → useAsyncData-1Dhzjfwf.js} +1 -1
  32. package/dist/{useDateFormatter-BC6iSz9g.js → useDateFormatter-CMnRuVmN.js} +2 -2
  33. package/dist/{useDeepCompareMemoize-BPx2MuOK.js → useDeepCompareMemoize-CDWT3BDz.js} +1 -1
  34. package/dist/{useIframeCapabilities-C6Ta3EyP.js → useIframeCapabilities-DWIYvDh7.js} +1 -1
  35. package/dist/{useLifecycle-C3Ec71q0.js → useLifecycle-AHlswLw-.js} +3 -3
  36. package/dist/{useTheme-ZhT6uIu3.js → useTheme-BrYvK-_A.js} +2 -2
  37. package/dist/{vega-component-C3AWYGAL.js → vega-component-Pk6lyc_a.js} +10 -10
  38. package/dist/{zod-DXqkaI_w.js → zod-CijjQh4u.js} +1 -1
  39. package/package.json +3 -3
  40. package/src/components/ai/display-helpers.tsx +5 -5
  41. package/src/components/app-config/ai-config.tsx +5 -5
  42. package/src/components/app-config/mcp-config.tsx +3 -3
  43. package/src/components/chat/acp/agent-panel.tsx +3 -3
  44. package/src/components/chat/acp/blocks.tsx +36 -38
  45. package/src/components/chat/acp/common.tsx +12 -16
  46. package/src/components/chat/acp/scroll-to-bottom-button.tsx +1 -1
  47. package/src/components/chat/acp/session-tabs.tsx +2 -2
  48. package/src/components/chat/chat-history-popover.tsx +1 -1
  49. package/src/components/chat/chat-panel.tsx +47 -23
  50. package/src/components/data-table/TableBottomBar.tsx +4 -1
  51. package/src/components/data-table/columns.tsx +2 -2
  52. package/src/components/data-table/data-table.tsx +26 -17
  53. package/src/components/data-table/filter-pill-editor.tsx +1 -1
  54. package/src/components/dependency-graph/minimap-content.tsx +1 -1
  55. package/src/components/editor/RecoveryButton.tsx +1 -1
  56. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  57. package/src/components/editor/actions/useNotebookActions.tsx +4 -4
  58. package/src/components/editor/ai/__tests__/completion-utils.test.ts +91 -1
  59. package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
  60. package/src/components/editor/ai/completion-utils.ts +86 -1
  61. package/src/components/editor/cell/CreateCellButton.tsx +1 -1
  62. package/src/components/editor/chrome/panels/empty-state.tsx +1 -1
  63. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +1 -1
  64. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +1 -1
  65. package/src/components/editor/columns/cell-column.tsx +1 -1
  66. package/src/components/editor/columns/sortable-column.tsx +2 -2
  67. package/src/components/editor/output/MarimoErrorOutput.tsx +1 -1
  68. package/src/components/editor/output/TextOutput.tsx +2 -2
  69. package/src/components/home/components.tsx +4 -4
  70. package/src/components/icons/github.tsx +21 -0
  71. package/src/components/icons/youtube.tsx +21 -0
  72. package/src/components/slides/minimap.tsx +2 -2
  73. package/src/components/slides/reveal-component.tsx +1 -1
  74. package/src/components/storage/components.tsx +3 -7
  75. package/src/components/ui/alert.tsx +1 -1
  76. package/src/components/ui/command.tsx +2 -2
  77. package/src/components/ui/reorderable-list.tsx +1 -1
  78. package/src/components/ui/table.tsx +2 -5
  79. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +67 -0
  80. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +47 -0
  81. package/src/core/codemirror/go-to-definition/commands.ts +47 -30
  82. package/src/core/codemirror/go-to-definition/utils.ts +0 -1
  83. package/src/core/codemirror/language/languages/sql/renderers.tsx +60 -68
  84. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +54 -0
  85. package/src/core/codemirror/reactive-references/analyzer.ts +44 -35
  86. package/src/core/hotkeys/hotkeys.ts +1 -0
  87. package/src/core/islands/__tests__/bridge.test.ts +25 -0
  88. package/src/core/islands/__tests__/parse.test.ts +585 -1
  89. package/src/core/islands/__tests__/test-utils.tsx +10 -1
  90. package/src/core/islands/bridge.ts +6 -1
  91. package/src/core/islands/constants.ts +2 -0
  92. package/src/core/islands/parse.ts +293 -13
  93. package/src/plugins/impl/DataTablePlugin.tsx +20 -1
  94. package/src/plugins/impl/FileBrowserPlugin.tsx +165 -74
  95. package/src/plugins/impl/MatrixPlugin.tsx +2 -2
  96. package/src/plugins/impl/TabsPlugin.tsx +1 -1
  97. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
  98. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +314 -0
  99. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +4 -1
  100. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +34 -0
  101. package/src/plugins/impl/anywidget/__tests__/model.test.ts +19 -0
  102. package/src/plugins/impl/anywidget/model.ts +15 -0
  103. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -1
  104. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +155 -98
  105. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +154 -1
  106. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +10 -0
@@ -9,7 +9,6 @@ import {
9
9
  DatabaseIcon,
10
10
  FileIcon,
11
11
  FileTextIcon,
12
- GithubIcon,
13
12
  GraduationCapIcon,
14
13
  GridIcon,
15
14
  LayoutIcon,
@@ -17,10 +16,11 @@ import {
17
16
  MessagesSquareIcon,
18
17
  OrbitIcon,
19
18
  PackageIcon,
20
- YoutubeIcon,
21
19
  } from "lucide-react";
22
20
  import type React from "react";
23
21
  import { MarkdownIcon } from "@/components/editor/cell/code/icons";
22
+ import { GitHubIcon } from "@/components/icons/github";
23
+ import { YouTubeIcon } from "@/components/icons/youtube";
24
24
  import { Button } from "@/components/ui/button";
25
25
  import {
26
26
  DropdownMenu,
@@ -130,7 +130,7 @@ const RESOURCES = [
130
130
  {
131
131
  title: "GitHub",
132
132
  description: "View source code, report issues, or contribute",
133
- icon: GithubIcon,
133
+ icon: GitHubIcon,
134
134
  url: Constants.githubPage,
135
135
  },
136
136
  {
@@ -148,7 +148,7 @@ const RESOURCES = [
148
148
  {
149
149
  title: "YouTube",
150
150
  description: "Watch tutorials and demos",
151
- icon: YoutubeIcon,
151
+ icon: YouTubeIcon,
152
152
  url: Constants.youtube,
153
153
  },
154
154
  {
@@ -0,0 +1,21 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { SVGProps } from "react";
3
+
4
+ // Artwork from Simple Icons (https://simpleicons.org/?q=github), licensed CC0 1.0.
5
+ // The GitHub name and logo are trademarks of GitHub, Inc.
6
+ export const GitHubIcon = (props: SVGProps<SVGSVGElement>) => {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="1em"
11
+ height="1em"
12
+ viewBox="0 0 24 24"
13
+ fill="currentColor"
14
+ aria-hidden="true"
15
+ focusable="false"
16
+ {...props}
17
+ >
18
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
19
+ </svg>
20
+ );
21
+ };
@@ -0,0 +1,21 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { SVGProps } from "react";
3
+
4
+ // Artwork from Simple Icons (https://simpleicons.org/?q=youtube), licensed CC0 1.0.
5
+ // The YouTube name and logo are trademarks of Google LLC.
6
+ export const YouTubeIcon = (props: SVGProps<SVGSVGElement>) => {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="1em"
11
+ height="1em"
12
+ viewBox="0 0 24 24"
13
+ fill="currentColor"
14
+ aria-hidden="true"
15
+ focusable="false"
16
+ {...props}
17
+ >
18
+ <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
19
+ </svg>
20
+ );
21
+ };
@@ -514,7 +514,7 @@ const SlideThumbnailRow = ({
514
514
  tabIndex={0}
515
515
  data-cell-id={cell.id}
516
516
  className={cn(
517
- "relative shrink-0 appearance-none text-left p-0 bg-transparent outline-none",
517
+ "relative shrink-0 appearance-none text-left p-0 bg-transparent outline-hidden",
518
518
  className,
519
519
  )}
520
520
  style={rowStyle}
@@ -591,7 +591,7 @@ const InsertCellLine = ({
591
591
  data-testid="minimap-insert-cell"
592
592
  className={cn(
593
593
  "absolute left-0 right-0 z-30 flex h-3 items-center justify-center",
594
- "opacity-0 transition-opacity hover:opacity-80 focus-visible:opacity-100 focus:outline-none",
594
+ "opacity-0 transition-opacity hover:opacity-80 focus-visible:opacity-100 focus:outline-hidden",
595
595
  position === "below"
596
596
  ? "bottom-0 translate-y-1/2"
597
597
  : "top-0 -translate-y-1/2",
@@ -684,7 +684,7 @@ const RevealSlidesComponent = ({
684
684
  <div className="group relative" style={{ width, height }}>
685
685
  <Deck
686
686
  deckRef={deckRef}
687
- className="aspect-video w-full overflow-hidden border rounded bg-background mo-slides-theme prose-slides focus:outline-none focus-visible:outline-none"
687
+ className="aspect-video w-full overflow-hidden border rounded bg-background mo-slides-theme prose-slides focus:outline-hidden focus-visible:outline-hidden"
688
688
  config={revealConfig}
689
689
  onReady={handleDeckReady}
690
690
  onSlideChange={handleSlideChange}
@@ -6,14 +6,10 @@ import AzureIcon from "@marimo-team/llm-info/icons/azure.svg?inline";
6
6
  import CloudflareIcon from "@marimo-team/llm-info/icons/cloudflare.svg?inline";
7
7
  import CoreweaveIcon from "@marimo-team/llm-info/icons/coreweave.svg?inline";
8
8
  import CoreweaveDarkIcon from "@marimo-team/llm-info/icons/coreweave-dark.svg?inline";
9
- import {
10
- DatabaseZapIcon,
11
- GithubIcon,
12
- GlobeIcon,
13
- HardDriveIcon,
14
- } from "lucide-react";
9
+ import { DatabaseZapIcon, GlobeIcon, HardDriveIcon } from "lucide-react";
15
10
  import GoogleCloudIcon from "@/components/databases/icons/google-cloud-storage.svg?inline";
16
11
  import GoogleDriveIcon from "@/components/databases/icons/google-drive.svg?inline";
12
+ import { GitHubIcon } from "@/components/icons/github";
17
13
  import type { KnownStorageProtocol } from "@/core/storage/types";
18
14
  import { useTheme } from "@/theme/useTheme";
19
15
  import { cn } from "@/utils/cn";
@@ -32,7 +28,7 @@ const PROTOCOL_ICONS: Record<KnownStorageProtocol, IconEntry> = {
32
28
  file: HardDriveIcon,
33
29
  "in-memory": DatabaseZapIcon,
34
30
  gdrive: { src: GoogleDriveIcon },
35
- github: GithubIcon,
31
+ github: GitHubIcon,
36
32
  };
37
33
 
38
34
  export const ProtocolIcon: React.FC<{
@@ -6,7 +6,7 @@ import * as React from "react";
6
6
  import { cn } from "@/utils/cn";
7
7
 
8
8
  const alertVariants = cva(
9
- "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
9
+ "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] has-[svg]:pl-11",
10
10
  {
11
11
  variants: {
12
12
  variant: {
@@ -44,7 +44,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
44
44
  className="overflow-hidden p-0 shadow-2xl"
45
45
  usePortal={true}
46
46
  >
47
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
47
+ <Command className="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
48
48
  {children}
49
49
  </Command>
50
50
  </DialogContent>
@@ -113,7 +113,7 @@ const CommandGroup = React.forwardRef<
113
113
  <CommandPrimitive.Group
114
114
  ref={ref}
115
115
  className={cn(
116
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
116
+ "overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
117
117
  className,
118
118
  )}
119
119
  {...props}
@@ -300,7 +300,7 @@ export const ReorderableList = <T extends object>({
300
300
  <ListBoxItem
301
301
  key={getKey(item)}
302
302
  id={getKey(item)}
303
- className="active:cursor-grabbing data-[dragging]:opacity-60 outline-none"
303
+ className="active:cursor-grabbing data-dragging:opacity-60 outline-hidden"
304
304
  onHoverStart={
305
305
  onItemPreloadHint ? () => onItemPreloadHint(item) : undefined
306
306
  }
@@ -75,7 +75,7 @@ const TableHead = React.forwardRef<
75
75
  <th
76
76
  ref={ref}
77
77
  className={cn(
78
- "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
78
+ "h-10 px-2 text-left align-middle font-medium text-muted-foreground has-[[role=checkbox]]:pr-0",
79
79
  className,
80
80
  )}
81
81
  {...props}
@@ -89,10 +89,7 @@ const TableCell = React.forwardRef<
89
89
  >(({ className, ...props }, ref) => (
90
90
  <td
91
91
  ref={ref}
92
- className={cn(
93
- "p-1.5 align-middle [&:has([role=checkbox])]:pr-0",
94
- className,
95
- )}
92
+ className={cn("p-1.5 align-middle has-[[role=checkbox]]:pr-0", className)}
96
93
  {...props}
97
94
  />
98
95
  ));
@@ -253,6 +253,73 @@ a = 10`;
253
253
  `);
254
254
  });
255
255
 
256
+ test("from-import alias is the binding, not the imported name", async () => {
257
+ const code = `\
258
+ from math import sin as my_sin
259
+ print(my_sin)`;
260
+ view = createEditor(code);
261
+ const usagePosition = code.lastIndexOf("my_sin");
262
+ const result = goToVariableDefinition(view, "my_sin", usagePosition);
263
+
264
+ expect(result).toBe(true);
265
+ await tick();
266
+ // The alias `my_sin` (after `as`) is the real binding.
267
+ expect(renderEditorView(view)).toMatchInlineSnapshot(`
268
+ "
269
+ from math import sin as my_sin
270
+ ^
271
+ print(my_sin)
272
+ "
273
+ `);
274
+ });
275
+
276
+ test("module path in from-import is not a local definition", async () => {
277
+ const code = `\
278
+ from math import sin
279
+ print(math)`;
280
+ view = createEditor(code);
281
+ const usagePosition = code.lastIndexOf("math");
282
+ // `math` is a module reference in the from-clause, not a binding in this
283
+ // cell, so the scoped resolver should return false and let the caller fall
284
+ // through to cross-cell resolution.
285
+ const result = goToVariableDefinition(view, "math", usagePosition);
286
+
287
+ expect(result).toBe(false);
288
+ expect(view.state.selection.main.head).toBe(0);
289
+ });
290
+
291
+ test("imported name without `as` is a local definition", async () => {
292
+ const code = `\
293
+ from math import sin
294
+ print(sin)`;
295
+ view = createEditor(code);
296
+ const usagePosition = code.lastIndexOf("sin");
297
+ const result = goToVariableDefinition(view, "sin", usagePosition);
298
+
299
+ expect(result).toBe(true);
300
+ await tick();
301
+ expect(renderEditorView(view)).toMatchInlineSnapshot(`
302
+ "
303
+ from math import sin
304
+ ^
305
+ print(sin)
306
+ "
307
+ `);
308
+ });
309
+
310
+ test("imported name shadowed by `as` is not a binding", async () => {
311
+ const code = `\
312
+ from math import sin as my_sin
313
+ print(sin)`;
314
+ view = createEditor(code);
315
+ const usagePosition = code.lastIndexOf("sin");
316
+ // `sin` here refers to nothing in this cell (it was renamed to `my_sin`),
317
+ // so the scoped resolver should return false.
318
+ const result = goToVariableDefinition(view, "sin", usagePosition);
319
+
320
+ expect(result).toBe(false);
321
+ });
322
+
256
323
  test("selects outer-scope function declaration", async () => {
257
324
  view = createEditor(`\
258
325
  def x():
@@ -133,4 +133,51 @@ output = _x + 10`;
133
133
  await tick();
134
134
  expect(view.state.selection.main.head).toBe(code.indexOf("_x = 10"));
135
135
  });
136
+
137
+ test("falls through to cross-cell when in-cell occurrence is only a module path in a from-import", async () => {
138
+ // Regression: ImportStatement used to register every VariableName child
139
+ // (the module path and pre-`as` names) as in-cell declarations, so the
140
+ // local-first short-circuit would steal F12 from cross-cell resolution.
141
+ const moduleCell = cellId("module-cell");
142
+ const usageCell = cellId("usage-cell");
143
+ const moduleCode = `mymodule = 100`;
144
+ const usageCode = `\
145
+ from mymodule import something
146
+ print(mymodule)`;
147
+
148
+ const moduleView = createEditor(moduleCode, moduleCode.length);
149
+ const usageView = createEditor(
150
+ usageCode,
151
+ usageCode.lastIndexOf("mymodule"),
152
+ );
153
+ views.push(moduleView, usageView);
154
+
155
+ const notebook = initialNotebookState();
156
+ notebook.cellHandles[moduleCell] = {
157
+ current: { editorView: moduleView, editorViewOrNull: moduleView },
158
+ } as never;
159
+ notebook.cellHandles[usageCell] = {
160
+ current: { editorView: usageView, editorViewOrNull: usageView },
161
+ } as never;
162
+
163
+ store.set(notebookAtom, notebook);
164
+ store.set(variablesAtom, {
165
+ [variableName("mymodule")]: {
166
+ dataType: "int",
167
+ declaredBy: [moduleCell],
168
+ name: variableName("mymodule"),
169
+ usedBy: [usageCell],
170
+ value: "100",
171
+ },
172
+ });
173
+
174
+ const result = goToDefinitionAtCursorPosition(usageView);
175
+
176
+ expect(result).toBe(true);
177
+ await tick();
178
+ // Cross-cell jump: moduleView's cursor should land on `mymodule = 100`.
179
+ expect(moduleView.state.selection.main.head).toBe(
180
+ moduleCode.indexOf("mymodule"),
181
+ );
182
+ });
136
183
  });
@@ -307,33 +307,52 @@ function collectMatchingDeclarations(
307
307
  break;
308
308
  }
309
309
  case "ImportStatement": {
310
+ // The grammar emits one ImportStatement for both `import x [as y]` and
311
+ // `from m import x [as y], ...`. Direct children include the keywords
312
+ // (`from`/`import`/`as`), commas, dots, and every VariableName from the
313
+ // module path AND the import list. We only want the names that actually
314
+ // bind in the current scope: the post-`as` alias if present, otherwise
315
+ // the imported name itself. Names before `import` (the from-path) and
316
+ // the original name when an alias follows it are NOT bindings.
310
317
  const subCursor = node.cursor();
311
318
  subCursor.firstChild();
312
- do {
313
- if (
314
- subCursor.name === "VariableName" &&
315
- state.doc.sliceString(subCursor.from, subCursor.to) === variableName
316
- ) {
317
- addDeclaration(declarations, currentScope, subCursor.from);
319
+ let pastImport = false;
320
+ // Buffer the most recent post-`import` VariableName so we can defer
321
+ // committing it until we know whether `as` follows.
322
+ let pending: { from: number; matches: boolean } | null = null;
323
+ const commit = () => {
324
+ if (pending?.matches) {
325
+ addDeclaration(declarations, currentScope, pending.from);
318
326
  }
319
- } while (subCursor.nextSibling());
320
- break;
321
- }
322
- case "ImportFromStatement": {
323
- const subCursor = node.cursor();
324
- subCursor.firstChild();
325
- let foundImport = false;
327
+ pending = null;
328
+ };
326
329
  do {
327
330
  if (subCursor.name === "import") {
328
- foundImport = true;
329
- } else if (
330
- foundImport &&
331
- subCursor.name === "VariableName" &&
332
- state.doc.sliceString(subCursor.from, subCursor.to) === variableName
333
- ) {
334
- addDeclaration(declarations, currentScope, subCursor.from);
331
+ pastImport = true;
332
+ continue;
333
+ }
334
+ if (!pastImport) {
335
+ continue;
336
+ }
337
+ if (subCursor.name === "as") {
338
+ // Next VariableName is the alias and replaces `pending`.
339
+ pending = null;
340
+ continue;
341
+ }
342
+ if (subCursor.name === "VariableName") {
343
+ // Flush any previous pending name (no `as` followed it).
344
+ commit();
345
+ pending = {
346
+ from: subCursor.from,
347
+ matches:
348
+ state.doc.sliceString(subCursor.from, subCursor.to) ===
349
+ variableName,
350
+ };
351
+ } else if (subCursor.name === ",") {
352
+ commit();
335
353
  }
336
354
  } while (subCursor.nextSibling());
355
+ commit();
337
356
  break;
338
357
  }
339
358
  case "TryStatement":
@@ -410,23 +429,21 @@ function findScopedDefinitionPosition(
410
429
  * @param view The editor view which contains the variable name.
411
430
  * @param variableName The name of the variable to select, if found in the editor.
412
431
  * @param usagePosition The position of the variable usage, if available.
413
- * @param fallbackToFirstMatch Whether to fall back to the first matching
414
- * variable name when no scoped definition is found. Defaults to true.
415
432
  */
416
433
  export function goToVariableDefinition(
417
434
  view: EditorView,
418
435
  variableName: string,
419
436
  usagePosition?: number,
420
- fallbackToFirstMatch = true,
421
437
  ): boolean {
422
438
  const { state } = view;
423
- let from: number | null = null;
424
- if (usagePosition !== undefined) {
425
- from = findScopedDefinitionPosition(state, variableName, usagePosition);
426
- }
427
- if (from === null && fallbackToFirstMatch) {
428
- from = findFirstMatchingVariable(state, variableName);
429
- }
439
+ // When the caller knows the usage position, trust the scoped lookup. Falling
440
+ // back to first-match would defeat the local-vs-cross-cell decision in
441
+ // goToDefinition: if the symbol only appears as a module path in an import,
442
+ // scoped resolution returns null and we want the caller to try other cells.
443
+ const from =
444
+ usagePosition !== undefined
445
+ ? findScopedDefinitionPosition(state, variableName, usagePosition)
446
+ : findFirstMatchingVariable(state, variableName);
430
447
 
431
448
  if (from === null) {
432
449
  return false;
@@ -82,7 +82,6 @@ export function goToDefinition(
82
82
  view,
83
83
  variableName,
84
84
  usagePosition,
85
- false,
86
85
  );
87
86
  if (foundLocally) {
88
87
  return true;