-
Notifications
You must be signed in to change notification settings - Fork 128
notebook multiselect revamp for actions #10678
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
base: main
Are you sure you want to change the base?
notebook multiselect revamp for actions #10678
Conversation
|
E2E Tests 🚀 |
d50e141 to
a3561dc
Compare
The delete cell action uses the `{ multiSelect: true }` option to handle multi-select scenarios. This causes `cell.delete()` to be called once for each selected cell, creating separate undo operations for each cell which is not correct for a multi-select scenario. When cells are deleted via multi-select, the deletes should happen as one atomic transaction and undo should bring back all deleted cells in the multi-select.
To fix these, we are using the `deleteCells` function instead of the `deleteCell` function which internally handled multiple cells and the undo/redo logic for all cells the operation should work.
I have also updated the action to extend NotebookAction2 to clearly communicate that this action relies on the notebook instance and is not run per cell.
Actions that rely on a function which internally handled multi-select scenarios and undo/redo history should not use the CellAction2 class. The CellAction2 class for multi-select scenarios applies the action to each cell individually and tracks each operation in the undo/redo history as a separate transaction. This is confusing for users because the expectation is that all cells are part of a single transaction as far as undo/redo is concerned. For example: When cutting 3 cells together, the undo operation should bring all 3 cells back. Prior to this change, undo would only bring the last cell back and the user would have to undo the operation per cell.
Update paste action to be a notebook level action since the internal pasteCells command handled multi-selection and utilizes the active cell for anchoring the pasted cells.
973caaa to
cc40353
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change in this file allows undo/redo to work when a notebook file has no cells
| /** | ||
| * Runs the cell action for each selected cell as an operation per cell | ||
| * and not as a single operation on all selected cells. This means that | ||
| * each cell action will be undoable/redoable individually and not as a | ||
| * single transaction for all cells. | ||
| */ | ||
| if (this.options?.multiSelect) { | ||
| // Handle multiple selected cells | ||
| const selectedCells = getSelectedCells(activeNotebook.selectionStateMachine.state.get()); | ||
|
|
||
| for (const cell of selectedCells) { | ||
| this.runCellAction(cell, activeNotebook, accessor); | ||
| } | ||
| } else { | ||
| // Handle single cell | ||
| // Handle single cell (the active cell which will also be the editing cell if in edit mode) | ||
| const state = activeNotebook.selectionStateMachine.state.get(); | ||
| // Always check editing cell if actionBar is present (action bar items should work in edit mode). | ||
| // Otherwise, only check editing cell if editMode option is enabled. | ||
| const cell = getActiveCell(state) || ((isCellActionBarAction(this) || this.options?.editMode) ? getEditingCell(state) : undefined); | ||
| const cell = getActiveCell(state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seeM I updated the logic in here for determining the cell we should operate the cell action on when not in a mulit-selection scenario. I think having this just return the active cell makes sense since the active cell is the only one that can be in edit mode.
Let me know if this seems right to you!
| @@ -700,17 +692,17 @@ | |||
| order: 100, | |||
| group: 'Cell' | |||
| }, | |||
| keybinding: { | |||
| when: POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED, | |||
| weight: KeybindingWeight.EditorContrib, | |||
| primary: KeyCode.Backspace, | |||
| secondary: [KeyChord(KeyCode.KeyD, KeyCode.KeyD)] | |||
| } | |||
| }, { multiSelect: true, editMode: false }); | |||
| }); | |||
| } | |||
|
|
|||
| override runCellAction(cell: IPositronNotebookCell, _notebook: IPositronNotebookInstance, _accessor: ServicesAccessor) { | |||
| cell.delete(); | |||
| override runNotebookAction(notebook: IPositronNotebookInstance, _accessor: ServicesAccessor) { | |||
| notebook.deleteCells(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that the { multiSelect: true } option on CellAction2 does not play well with undo/redo operations. This is because it runs the action for each cell which creates a separate undo operation per cell instead of handling it as one atomic transaction.
To fix this, we need operations that effect multiple cells to be handled in one transaction. Most of the operations that support multiSelect are on the notebook instance and internally handle multiple cells (cut/copy/paste). We just needed to update the delete function to behave similarly.
After making that change I also updated all of the actions that support multi-selection to use the NotebookAction2 class since they don't need a specific cell to work (they use the active cell or selected cells).
| when: ContextKeyExpr.or( | ||
| POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED, | ||
| ContextKeyExpr.equals('activeEditor', POSITRON_NOTEBOOK_EDITOR_ID) | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pasting cells into a notebook needs to work regardless of the number of cells in the notebook. This means that all we really care about is the notebook being the active editor. I added that check. here. I don't think we need to check POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED but I left it in. Do you think I should just remove this now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remolve it. One possible issue to check is whether this action consumes Cmd+v when editing a cell instead of the regular paste action
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made deleteCells public so we could use it for the delete action and renamed moveCellUp/moveCellDown to be plural since it supports multi-select.
| // Run all code cells below the current cell | ||
| for (let i = cell.index + 1; i < cells.length; i++) { | ||
| // Run all code cells below the current cell (including the current cell) | ||
| for (let i = cell.index; i < cells.length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change just allows the current cell to also be executed: #10482
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file changes the "paste" and "move cell" functions to use the active/selected cell for the operation.
The delete cell function has been updated to use the active/selected cells for the operation if a set of cells aren't provided.
cc @nstrayer will these changes effect the assistant tool calls you added? Do I need to rewrite some of these to support an optional array of cells?
| }); | ||
|
|
||
| test("`+ Code` and `+ Markdown` buttons insert the cell after the active cell and make it the new active cell)", async function ({ app }) { | ||
| test("`+ Code` and `+ Markdown` buttons insert the cell after the active cell and make it the new active cell", async function ({ app }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just fixing a typo in the test name here
seeM
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works really well in my testing, very smooth! As you pointed out, we need to double-check with @nstrayer whether the changes affect any tools before merging
There are some considerations if we want our commands to be compatible with VSCode counter parts but I don't think that's a priority right now and shouldn't be a heavy lift in future.
| when: ContextKeyExpr.or( | ||
| POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED, | ||
| ContextKeyExpr.equals('activeEditor', POSITRON_NOTEBOOK_EDITOR_ID) | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remolve it. One possible issue to check is whether this action consumes Cmd+v when editing a cell instead of the regular paste action
Thanks for mentioning this. I did need to add a fix for this to make sure we weren't in edit mode like we do for all other cell actions.
We should chat a bit about this! I'm not too familiar with why this is important and what the VS Code counterparts are but happy to keep things in sync while doing any work. |
This fix updates the POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED key to not be false when there are no cells in a notebook. This ensures the paste action (and any other action that uses the POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED context key) to only fire when the notebook editor has focus! We can't rely on the POSITRON_NOTEBOOK_CELL_EDITOR_FOCUSED context key because it is false when there are no cells in the notebook and we can't rely on the activeEditor context keys because it is always true and causes the action to fire EVERY time a user presses v in an input.
|
The last couple commits should fix the issue where a positron notebook didn't have focus when it is empty. This caused a number of actions to not be able to operate in an empty notebook (specifically undo/redo/paste). We use the When a user removed the last cell in a notebook the We don't actually want the notebook container to lose focus when its empty. We need to make it programmatically focused when its empty. Doing so will prevent the Side note: I also removed the |
Addresses #10116 and #10397
This PR revamps a good portion of our notebook cell actions to support multi-selections and ensure that undo/redo work as expected.
Updated actions that now support multi-select properly with undo/redo:
I also fixed #10482 which fixes the "Run Below Cells" action to also include the current cell in its execution.
Run Below Cells - BEFORE
execute_cell_belows_before.mov
Run Below Cells - AFTER
execute_cell_belows_after.mov
Move Cells with Undo- AFTER
Screen.Recording.2025-11-20.at.2.24.18.PM.mov
Cut/Copy/Paste/Delete with Undo- AFTER
Screen.Recording.2025-11-20.at.2.20.16.PM.mov
Release Notes
New Features
Bug Fixes
QA Notes
@:positron-notebooks