Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go to definition functionality added to hotkey and context menu #1411

Merged
merged 16 commits into from
May 20, 2024

Conversation

robmck1995
Copy link
Contributor

Fixes #1126

For this one, I set the hotkey to Mod+Shift+, but let me know if there's a better one.

I'm unsure if this is structured correctly, specifically, the utility functions specified in cell-editor.tsx. Let me know if there is a better way to structure it. Basically, I wasn't sure how to ensure we could use the same functionality with the right-click menu and the hotkeys. The solution works, but there is a bit of code repetition.

Created this with the help of Glide! The change in this document only covers adding the hotkey, I manually added the right-click after as it was a small change:
https://glide.agenticlabs.com/sharing?userId=65bae76b7fbfb1c20ac1f3b8&taskId=ghM1mqX

Copy link

vercel bot commented May 19, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
marimo-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 20, 2024 10:06pm
marimo-storybook ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 20, 2024 10:06pm

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @robmck1995 - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 5 issues found
  • 🟢 Security: all looks good
  • 🟡 Testing: 3 issues found
  • 🟡 Complexity: 1 issue found

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment to tell me if it was helpful.

Comment on lines 137 to 146
const focusByVariableName = useCallback(() => {
if (editorViewRef.current) {
const { state } = editorViewRef.current;
const variableName = getWordUnderCursor(state);
const focusCellId = getCellIdOfDefinition(variables, variableName);

if (focusCellId) {
focusCellAtDefinition({
cellId: focusCellId,
variableName: variableName,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Missing dependency in useCallback hook.

The focusByVariableName function uses getWordUnderCursor and getCellIdOfDefinition, but these are not included in the dependency array. This could lead to stale closures if these functions change.

Comment on lines 444 to 445
const focusCellId = variable.declaredBy[0];
return focusCellId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Potential issue with multiple declarations.

The getCellIdOfDefinition function only returns the first cell ID where the variable is declared. If a variable is declared in multiple cells, this might not be the desired behavior. Consider handling multiple declarations or documenting this behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably fine as multiple definitions, while supported, is not a state the notebook should stay in? So the first definition is probably fine?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea this is ok - you can define something twice, but its an error and going to the first one will be ok

Comment on lines 90 to 99
{
label: "Go to Definition",
icon: <SearchIcon size={13} strokeWidth={1.5} />,
handle: () => {
const { getEditorView } = props;
const editorView = getEditorView();
if (editorView) {
const variableName = getWordUnderCursor(editorView?.state);
const focusCellId = getCellIdOfDefinition(variables, variableName);
if (focusCellId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Error handling for undefined variable.

In the 'Go to Definition' action, if getWordUnderCursor returns an undefined or empty string, the function should handle this case gracefully, possibly with a user notification.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that getWordUnderCursor should always return some string. And getCellIdofDefinition will only return a value if the variable is defined in the variables array so I think this should not cause errors, but not 100% sure about this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can play around with some cases - vscode does always show you Go To Definition, but does no-op when you don't have a work under it

Comment on lines 57 to 58
} else if (codeFocus) {
goToDefinition(editor, codeFocus);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Clarify the behavior of goToDefinition.

The goToDefinition function is called if codeFocus is neither '<>' nor '<>'. Ensure that goToDefinition handles all edge cases and that its behavior is well-documented.

@@ -38,6 +39,7 @@
createBelow,
moveUp,
moveDown,
focusByVariableName,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider keeping the splitCell functionality and modularizing keybinding logic.

The new code introduces increased complexity and removes existing functionality. Here are the main points:

  1. Increased Interface Complexity: The MovementCallbacks interface now includes an additional method focusByVariableName, which increases the surface area of the interface and makes it harder to maintain.
  2. Removed Functionality: The splitCell functionality has been removed, which might be critical. Removing such functionality without proper context or replacement can lead to incomplete features or broken functionality.
  3. Keybinding Changes: The keybinding for splitCell has been replaced with goToDefinition. This change should be clearly documented and justified. The new keybinding logic adds complexity by introducing a new behavior that developers need to understand and maintain.
  4. Code Duplication: The new keybinding logic for focusByVariableName is similar to the existing keybinding logic, leading to code duplication and making the codebase harder to maintain and more prone to bugs.

To address these issues, consider the following approach:

  1. Keep the splitCell Functionality: Retain the splitCell functionality if it is still needed and add the new functionality without removing the existing one.
  2. Modularize Keybinding Logic: Extract common keybinding logic into reusable functions to reduce duplication.

Here is a revised code snippet:

import { CellId, HTMLCellId } from "@/core/cells/ids";
import { Extension, Prec } from "@codemirror/state";
import { formatKeymapExtension } from "../extensions";
import { CellActions } from "@/core/cells/cells";
import { getEditorCodeAsPython } from "../language/utils";
import { formattingChangeEffect } from "../format";
import { closeCompletion, completionStatus } from "@codemirror/autocomplete";
import { isAtEndOfEditor, isAtStartOfEditor } from "../utils";

export interface MovementCallbacks
  extends Pick<CellActions, "sendToTop" | "sendToBottom" | "moveToNextCell"> {
  onRun: () => void;
  deleteCell: () => void;
  createAbove: () => void;
  createBelow: () => void;
  moveUp: () => void;
  moveDown: () => void;
  focusUp: () => void;
  focusDown: () => void;
  toggleHideCode: () => boolean;
  aiCellCompletion: () => boolean;
  focusByVariableName?: () => void; // Optional if not always needed
  splitCell?: (params: { cellId: CellId; cursorPos: number }) => void; // Optional if not always needed
}

/**
 * Extensions for cell movement
 */
export function cellMovementBundle(
  cellId: CellId,
  callbacks: MovementCallbacks,
): Extension[] {
  const {
    onRun,
    deleteCell,
    createAbove,
    createBelow,
    moveUp,
    moveDown,
    focusUp,
    focusDown,
    sendToTop,
    sendToBottom,
    moveToNextCell,
    toggleHideCode,
    aiCellCompletion,
    focusByVariableName,
    splitCell,
  } = callbacks;

  const hotkeys: KeyBinding[] = [
    {
      key: HOTKEYS.getHotkey("cell.run").key,
      preventDefault: true,
      stopPropagation: true,
      run: (ev) => {
        onRun();
        return true;
      },
    },
    {
      key: HOTKEYS.getHotkey("cell.goToDefinition").key,
      preventDefault: true,
      stopPropagation: true,
      run: () => {
        if (focusByVariableName) {
          focusByVariableName();
        }
        return true;
      },
    },
    {
      key: HOTKEYS.getHotkey("cell.splitCell").key,
      preventDefault: true,
      stopPropagation: true,
      run: (ev) => {
        if (splitCell) {
          const cursorPos = ev.state.selection.main.head;
          splitCell({ cellId, cursorPos });
          requestAnimationFrame(() => {
            ev.contentDOM.blur();
            moveToNextCell({ cellId, before: false }); // focus new cell
          });
        }
        return true;
      },
    },
  ];

  // Highest priority so that we can override the default keymap
  return [Prec.highest(keymap.of(hotkeys))];
}

export interface CodeCallbacks {
  updateCellCode: CellActions["updateCellCode"];
}

/**
 * Extensions for cell code editing
 */
export function cellCodeEditingBundle(
  cellId: CellId,
  callbacks: CodeCallbacks,
): Extension[] {
  const { updateCellCode } = callbacks;

  // Add your code editing extensions here

  return [];
}

This approach maintains the existing functionality while adding the new feature in a less complex and more maintainable way.

"cell.goToDefinition": {
name: "Go to Definition",
group: "Navigation",
key: "Mod-Shift-,",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vscode's Go To Definition is F12. Maybe we can just keep it like that?

@@ -14,7 +15,7 @@ export function focusAndScrollCellIntoView({
cellId: CellId;
cell: RefObject<CellHandle>;
config: CellConfig;
codeFocus: "top" | "bottom" | undefined;
codeFocus: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can still keep this as codeFocus: "<<top>>" | "<<bottom>>" | string | undefined; which can help document the signature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually might be better to just create a new field for variableName - otherwise we lose type-safety downstream

@@ -231,6 +231,25 @@ const {
scrollKey: null,
};
},
focusCellAtDefinition: (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though its kinda similar to focusCell, I think we can make this a function outside of the reducer in core/codemirror/go-to-definition. You can access the store by store.get(atomName).

@@ -410,3 +434,29 @@ const CellCodeMirrorEditor = React.forwardRef(
CellCodeMirrorEditor.displayName = "CellCodeMirrorEditor";

export const CellEditor = memo(CellEditorInternal);

export const getWordUnderCursor = (state: EditorState) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can move all this logic to another file core/codemirror/go-to-definition.ts.
we should be able to just expose one function goToDefinition(view: EditorView)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect

Comment on lines 90 to 99
{
label: "Go to Definition",
icon: <SearchIcon size={13} strokeWidth={1.5} />,
handle: () => {
const { getEditorView } = props;
const editorView = getEditorView();
if (editorView) {
const variableName = getWordUnderCursor(editorView?.state);
const focusCellId = getCellIdOfDefinition(variables, variableName);
if (focusCellId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can play around with some cases - vscode does always show you Go To Definition, but does no-op when you don't have a work under it

@@ -52,6 +52,7 @@ export function cellMovementBundle(
toggleHideCode,
aiCellCompletion,
} = callbacks;
const variables = useVariables()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use store.get(variables) and can likely push this into goToDefinition

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add, thanks for the tip

Copy link
Contributor

@mscolnick mscolnick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice contrib!

@mscolnick mscolnick merged commit d2a253f into marimo-team:main May 20, 2024
27 checks passed
Copy link

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.6.1-dev16

akshayka added a commit that referenced this pull request May 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Go to cell defining something
2 participants