Skip to content

Conversation

@dhruvisompura
Copy link
Contributor

@dhruvisompura dhruvisompura commented Nov 20, 2025

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:

  • cut
  • delete
  • copy
  • paste
  • move cell up
  • move cell down

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

  • N/A

Bug Fixes

  • N/A

QA Notes

@:positron-notebooks

@github-actions
Copy link

github-actions bot commented Nov 20, 2025

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:positron-notebooks

readme  valid tags

@dhruvisompura dhruvisompura force-pushed the feature/notebook-multiselect-cut-delete-undo-redo branch from d50e141 to a3561dc Compare November 20, 2025 17:57
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.
@dhruvisompura dhruvisompura force-pushed the feature/notebook-multiselect-cut-delete-undo-redo branch from 973caaa to cc40353 Compare November 20, 2025 19:40
Copy link
Contributor Author

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

Comment on lines 564 to 580
/**
* 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);
Copy link
Contributor Author

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!

Comment on lines 692 to 705
@@ -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();
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 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).

Comment on lines 1083 to 1086
when: ContextKeyExpr.or(
POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED,
ContextKeyExpr.equals('activeEditor', POSITRON_NOTEBOOK_EDITOR_ID)
),
Copy link
Contributor Author

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?

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 remolve it. One possible issue to check is whether this action consumes Cmd+v when editing a cell instead of the regular paste action

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 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++) {
Copy link
Contributor Author

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

Copy link
Contributor Author

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 }) {
Copy link
Contributor Author

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

@dhruvisompura dhruvisompura marked this pull request as ready for review November 20, 2025 22:49
seeM
seeM previously approved these changes Nov 21, 2025
Copy link
Contributor

@seeM seeM left a 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.

Comment on lines 1083 to 1086
when: ContextKeyExpr.or(
POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED,
ContextKeyExpr.equals('activeEditor', POSITRON_NOTEBOOK_EDITOR_ID)
),
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 remolve it. One possible issue to check is whether this action consumes Cmd+v when editing a cell instead of the regular paste action

@dhruvisompura
Copy link
Contributor Author

@seeM

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.

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.

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.

@dhruvisompura dhruvisompura requested a review from seeM November 21, 2025 20:09
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.
@dhruvisompura
Copy link
Contributor Author

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 POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED in almost all of our actions to determine if the user is actively focused on a notebook. This context key was being set to false when the notebook was empty, but it should have been true since the user is still focused on the notebook.

When a user removed the last cell in a notebook the onBlur event for the container was firing because the last focusable element in the notebook container was removed (focus moved out of the notebook up to another parent element). In ContextKeyManager.ts we listened for that blur event and set POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED to false.

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 onBlur event from firing and will NOT set POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED to false.

Side note: I also removed the ContextKeyExpr.equals('activeEditor', POSITRON_NOTEBOOK_EDITOR_ID) check from the actions since this caused the paste action to run anytime a user typed v anywhere in the IDE 🙈 since this tracks the (last) active editor and not the focused editor. All notebook actions should only be allowed to run when the notebook is focused. Fixing the POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED also made it so the paste cell action only works in a notebook!

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.

Positron Notebooks: "Run all below" should also run the selected cell

4 participants