Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2aa7825
fix: error: Argument 1 to add_common_config has incompatible type Lay…
Shamik-07 Apr 24, 2026
4ad8b3d
feat: adding command matches to the outer and inner function scope de…
Shamik-07 Apr 24, 2026
0a1c771
feat: get word under cursor returns both word and position
Shamik-07 Apr 24, 2026
8a170b1
test: checks nearest local parameters and checks across cells.
Shamik-07 Apr 24, 2026
bee4c91
test: local definitions are preferred over reactive gloabls and priva…
Shamik-07 Apr 24, 2026
0fa0c14
Merge branch 'main' into fix/def_declaration
Shamik-07 Apr 27, 2026
af8a66e
Merge branch 'main' into fix/def_declaration
Shamik-07 Apr 28, 2026
6612d01
Merge branch 'main' into fix/def_declaration
Shamik-07 Apr 29, 2026
2767c77
Merge branch 'main' into fix/def_declaration
Shamik-07 Apr 30, 2026
264c7e8
Merge branch 'main' into fix/def_declaration
Shamik-07 May 1, 2026
d3875e6
Merge branch 'main' into fix/def_declaration
Shamik-07 May 4, 2026
3475ee0
Merge branch 'main' into fix/def_declaration
Shamik-07 May 5, 2026
5f9c696
Merge branch 'main' into fix/def_declaration
Shamik-07 May 6, 2026
b8b9669
Merge branch 'main' into fix/def_declaration
Shamik-07 May 7, 2026
4a8eb4b
Merge branch 'main' into fix/def_declaration
Shamik-07 May 8, 2026
02fc173
Add failing tests for go-to-definition scope bugs
manzt May 11, 2026
bf12fd1
Merge pull request #1 from marimo-team/fix/def_declaration-tests
Shamik-07 May 11, 2026
7c634fa
fix: renaming SetComprehension to SetComprehensionExpression as Lezer…
Shamik-07 May 11, 2026
345e6e4
fix: skipping class scopes after a function boundary in getscopechain…
Shamik-07 May 11, 2026
2853f19
test: fixing the code comments in commands test.
Shamik-07 May 11, 2026
b2551f5
Merge branch 'main' into fix/def_declaration
Shamik-07 May 11, 2026
9638f27
docs: renaming pnpm build watch column name as production esque.
Shamik-07 May 11, 2026
1a458cd
Merge branch 'main' into fix/def_declaration
Shamik-07 May 12, 2026
7aa5372
Merge branch 'main' into fix/def_declaration
Shamik-07 May 13, 2026
515ede9
Merge branch 'main' into fix/def_declaration
Shamik-07 May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ If an issue does not have one of these two labels, assume it is **not ready** fo
### Why is maintainer approval required?

**Deliberate design.** marimo is an intentionally designed project. We
put just as much thought into the features we exclude as the ones we include,
put just as much thought into the features we exclude as the ones we include,
in order to provide our users with a simple, consistent, delightful, and
powerful experience.

Expand Down Expand Up @@ -294,7 +294,7 @@ For the frontend, you can choose to run slower hot reloading for an environment

<table>
<tr>
<th>Production</th>
<th>Production-esque</th>
<th>Development</th>
</tr>
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,158 @@ print(x)`);
`);
});

test("selects the nearest in-scope local definition", async () => {
const code = `\
a = 10

def my_func():
a = 20
print(a)`;
view = createEditor(code);
const result = goToVariableDefinition(view, "a", code.lastIndexOf("a"));

expect(result).toBe(true);
await tick();
expect(renderEditorView(view)).toMatchInlineSnapshot(`
"
a = 10

def my_func():
a = 20
^
print(a)
"
`);
});

test("selects the nearest in-scope parameter definition", async () => {
const code = `\
a = 10

def my_func(a):
print(a)`;
view = createEditor(code);
const result = goToVariableDefinition(view, "a", code.lastIndexOf("a"));

expect(result).toBe(true);
await tick();
expect(renderEditorView(view)).toMatchInlineSnapshot(`
"
a = 10

def my_func(a):
^
print(a)
"
`);
});

test("selects the comprehension target inside a set comprehension", async () => {
const code = `\
x = 100
s = {x for x in range(10)}`;
view = createEditor(code);
// Go-to-definition on the `x` before `for` (the expression part of the
// comprehension).
const usagePosition = code.indexOf("{x") + 1;
const result = goToVariableDefinition(view, "x", usagePosition);

expect(result).toBe(true);
await tick();
// Should jump to the comprehension target `x` (after `for`), not the
// outer `x = 100`. The Lezer Python grammar emits
// `SetComprehensionExpression`, and we now correctly match it in
// SCOPE_CREATING_NODES, so the comprehension creates a scope and the
// for-target is collected correctly.
expect(renderEditorView(view)).toMatchInlineSnapshot(`
"
x = 100
s = {x for x in range(10)}
^
"
`);
});

test("selects the comprehension target inside a dict comprehension", async () => {
const code = `\
x = 100
d = {x: x for x in range(10)}`;
view = createEditor(code);
const usagePosition = code.indexOf("{x") + 1;
const result = goToVariableDefinition(view, "x", usagePosition);

expect(result).toBe(true);
await tick();
// Positive control: `DictionaryComprehensionExpression` matches the grammar
// and is in SCOPE_CREATING_NODES, so this should jump to the comprehension
// target `x` (after `for`).
expect(renderEditorView(view)).toMatchInlineSnapshot(`
"
x = 100
d = {x: x for x in range(10)}
^
"
`);
});

test("skips enclosing class scope when resolving from inside a method", async () => {
const code = `\
x = 100
class Foo:
x = 10
def method(self):
return x`;
view = createEditor(code);
// Go-to-definition on the `x` inside `return x`.
const usagePosition = code.lastIndexOf("x");
const result = goToVariableDefinition(view, "x", usagePosition);

expect(result).toBe(true);
await tick();
// Should jump to `x = 100` at module scope. In Python, methods do NOT see
// their enclosing class body's names β€” class scopes are skipped in LEGB
// lookup once a function boundary has been crossed. We now correctly skip
// ClassDefinition in getScopeChain once a function boundary is crossed.
expect(renderEditorView(view)).toMatchInlineSnapshot(`
"
x = 100
^
class Foo:
x = 10
def method(self):
return x
"
`);
});

test("resolves a global forward-reference from inside a function", async () => {
const code = `\
def foo():
return a

a = 10`;
view = createEditor(code);
// Go-to-definition on the `a` inside `return a`.
const usagePosition = code.indexOf("return a") + "return ".length;
const result = goToVariableDefinition(view, "a", usagePosition);

expect(result).toBe(true);
await tick();
// Should jump to `a = 10` at the bottom. Python allows forward references
// from within nested functions to module-level names. We now correctly omit
// "global" from POSITION_SENSITIVE_SCOPES, allowing forward references to
// global-level definitions declared after the usage position.
expect(renderEditorView(view)).toMatchInlineSnapshot(`
"
def foo():
return a

a = 10
^
"
`);
});

test("selects outer-scope function declaration", async () => {
view = createEditor(`\
def x():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* Copyright 2026 Marimo. All rights reserved. */

import { python } from "@codemirror/lang-python";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { afterEach, describe, expect, test } from "vitest";
import { cellId, variableName } from "@/__tests__/branded";
import { initialNotebookState, notebookAtom } from "@/core/cells/cells";
import { store } from "@/core/state/jotai";
import { variablesAtom } from "@/core/variables/state";
import { goToDefinitionAtCursorPosition } from "../utils";

async function tick(): Promise<void> {
await new Promise((resolve) => requestAnimationFrame(resolve));
}

function createEditor(content: string, selection: number) {
const state = EditorState.create({
doc: content,
selection: { anchor: selection },
extensions: [python()],
});

return new EditorView({
state,
parent: document.body,
});
}

const views: EditorView[] = [];

afterEach(() => {
for (const view of views.splice(0)) {
view.destroy();
}

store.set(notebookAtom, initialNotebookState());
store.set(variablesAtom, {});
});

describe("goToDefinitionAtCursorPosition", () => {
test("prefers the current-cell local definition over a reactive global", async () => {
const globalCell = cellId("global-cell");
const localCell = cellId("local-cell");
const globalCode = `\
a = 10
print(a)`;
const localCode = `\
def test():
a = 20
print(a)`;

const globalView = createEditor(globalCode, globalCode.length);
const localView = createEditor(localCode, localCode.lastIndexOf("a"));
views.push(globalView, localView);

const notebook = initialNotebookState();
notebook.cellHandles[globalCell] = {
current: { editorView: globalView, editorViewOrNull: globalView },
};
notebook.cellHandles[localCell] = {
current: { editorView: localView, editorViewOrNull: localView },
};

store.set(notebookAtom, notebook);
store.set(variablesAtom, {
[variableName("a")]: {
dataType: "int",
declaredBy: [globalCell],
name: variableName("a"),
usedBy: [localCell],
value: "10",
},
});

const result = goToDefinitionAtCursorPosition(localView);

expect(result).toBe(true);
await tick();
expect(localView.state.selection.main.head).toBe(
localCode.indexOf("a = 20"),
);
expect(globalView.state.selection.main.head).toBe(globalCode.length);
});

test("keeps private variables within the current cell", async () => {
const code = `\
_x = 10
output = _x + 10`;
const view = createEditor(code, code.lastIndexOf("_x"));
views.push(view);

const result = goToDefinitionAtCursorPosition(view);

expect(result).toBe(true);
await tick();
expect(view.state.selection.main.head).toBe(code.indexOf("_x = 10"));
});
});
Loading
Loading