From a8a38595ac76996200320b972fec32dba7d75169 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 26 Mar 2024 10:13:56 +1100 Subject: [PATCH] Disable ctrl+A in notebook renderer and support Ctrl+A for inputs (#208635) * Disable ctrl+A in notebook renderer * Select All in input elements in outputs --- .../contrib/clipboard/notebookClipboard.ts | 8 ++- .../browser/diff/diffNestedCellViewModel.ts | 9 ++++ .../notebook/browser/notebookBrowser.ts | 6 +++ .../notebook/browser/notebookEditorWidget.ts | 8 ++- .../view/renderers/backLayerWebView.ts | 20 ++++++++ .../browser/view/renderers/webviewMessages.ts | 8 ++- .../browser/view/renderers/webviewPreloads.ts | 51 +++++++++++++++++-- .../browser/viewModel/codeCellViewModel.ts | 9 ++++ .../browser/viewModel/markupCellViewModel.ts | 8 +++ 9 files changed, 119 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index c945e31d356..2c4a1d43436 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -597,7 +597,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebook.cell.output.selectAll', "Select All"), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyA, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_OUTPUT_FOCUSED), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED), weight: NOTEBOOK_OUTPUT_WEBVIEW_ACTION_WEIGHT } }); @@ -615,7 +615,11 @@ registerAction2(class extends NotebookCellAction { if (!cell || !cell.outputIsFocused || !editor.hasWebviewFocus()) { return true; } - editor.selectOutputContent(cell); + if (cell.inputInOutputIsFocused) { + editor.selectInputContents(cell); + } else { + editor.selectOutputContent(cell); + } return true; }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts index 0f31bdfc6ca..9b15ad922c3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -62,6 +62,15 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe this._onDidChangeState.fire({ outputIsFocusedChanged: true }); } + private _focusInputInOutput: boolean = false; + public get inputInOutputIsFocused(): boolean { + return this._focusInputInOutput; + } + + public set inputInOutputIsFocused(v: boolean) { + this._focusInputInOutput = v; + } + private _outputViewModels: ICellOutputViewModel[]; get outputsViewModels() { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index a502b32b708..35eb6e5800e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -128,6 +128,7 @@ export interface IGenericCellViewModel { metadata: NotebookCellMetadata; outputIsHovered: boolean; outputIsFocused: boolean; + inputInOutputIsFocused: boolean; outputsViewModels: ICellOutputViewModel[]; getOutputOffset(index: number): number; updateOutputHeight(index: number, height: number, source?: string): void; @@ -587,6 +588,11 @@ export interface INotebookEditor { * Implementation of Ctrl+A for an output item. */ selectOutputContent(cell: ICellViewModel): void; + /** + * Select the active input element of the first focused output of the cell. + * Implementation of Ctrl+A for an input element in an output item. + */ + selectInputContents(cell: ICellViewModel): void; readonly onDidReceiveMessage: Event; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index ed6e78cad01..c788da84380 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1961,6 +1961,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._webview?.selectOutputContents(cell); } + selectInputContents(cell: ICellViewModel) { + this._webview?.selectInputContents(cell); + } + onWillHide() { this._isVisible = false; this._editorFocus.set(false); @@ -2402,12 +2406,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } - const focusElementId = options?.outputId ?? cell.id; + const firstOutputId = cell.outputsViewModels.find(o => o.model.alternativeOutputId)?.model.alternativeOutputId; + const focusElementId = options?.outputId ?? firstOutputId ?? cell.id; this._webview.focusOutput(focusElementId, options?.altOutputId, options?.outputWebviewFocused || this._webviewFocused); cell.updateEditState(CellEditState.Preview, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Output; cell.focusedOutputId = options?.outputId; + this._outputFocus.set(true); if (!options?.skipReveal) { this.revealInCenterIfOutsideViewport(cell); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index a005797d87e..315c68a31e0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -686,6 +686,7 @@ export class BackLayerWebView extends Themable { const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); if (latestCell) { latestCell.outputIsFocused = false; + latestCell.inputInOutputIsFocused = false; } } break; @@ -911,6 +912,13 @@ export class BackLayerWebView extends Themable { break; } case 'outputInputFocus': { + const resolvedResult = this.resolveOutputId(data.id); + if (resolvedResult) { + const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); + if (latestCell) { + latestCell.inputInOutputIsFocused = data.inputFocused; + } + } this.notebookEditor.didFocusOutputInputChange(data.inputFocused); } } @@ -1693,6 +1701,18 @@ export class BackLayerWebView extends Themable { }); } + selectInputContents(cell: ICellViewModel) { + if (this._disposed) { + return; + } + const output = cell.outputsViewModels.find(o => o.model.outputId === cell.focusedOutputId); + const outputId = output ? this.insetMapping.get(output)?.outputId : undefined; + this._sendMessageToWebview({ + type: 'select-input-contents', + cellOrOutputId: outputId || cell.id + }); + } + focusOutput(cellOrOutputId: string, alternateId: string | undefined, viewFocused: boolean) { if (this._disposed) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 12a4f4f7e01..ab8ff3a55f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -50,6 +50,7 @@ export interface IOutputBlurMessage extends BaseToWebviewMessage { export interface IOutputInputFocusMessage extends BaseToWebviewMessage { readonly type: 'outputInputFocus'; readonly inputFocused: boolean; + readonly id: string; } export interface IScrollToRevealMessage extends BaseToWebviewMessage { @@ -463,6 +464,10 @@ export interface ISelectOutputItemMessage { readonly type: 'select-output-contents'; readonly cellOrOutputId: string; } +export interface ISelectInputOutputItemMessage { + readonly type: 'select-input-contents'; + readonly cellOrOutputId: string; +} export interface ILogRendererDebugMessage extends BaseToWebviewMessage { readonly type: 'logRendererDebugMessage'; @@ -544,7 +549,8 @@ export type ToWebviewMessage = IClearMessage | IFindUnHighlightCurrentMessage | IFindStopMessage | IReturnOutputItemMessage | - ISelectOutputItemMessage; + ISelectOutputItemMessage | + ISelectInputOutputItemMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 96e05009b44..19f242d53dc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -194,11 +194,12 @@ async function webviewPreloads(ctx: PreloadContext) { return; } - if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') { - postNotebookMessage('outputInputFocus', { inputFocused: true }); + const id = lastFocusedOutput?.id; + if (id && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { + postNotebookMessage('outputInputFocus', { inputFocused: true, id }); activeElement.addEventListener('blur', () => { - postNotebookMessage('outputInputFocus', { inputFocused: false }); + postNotebookMessage('outputInputFocus', { inputFocused: false, id }); }, { once: true }); } }; @@ -286,6 +287,17 @@ async function webviewPreloads(ctx: PreloadContext) { }; + const selectInputContents = (cellOrOutputId: string) => { + const cellOutputContainer = window.document.getElementById(cellOrOutputId); + if (!cellOutputContainer) { + return; + } + const activeElement = window.document.activeElement; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') { + (activeElement as HTMLInputElement).select(); + } + }; + const onPageUpDownSelectionHandler = (e: KeyboardEvent) => { if (!lastFocusedOutput?.id || !e.shiftKey) { return; @@ -299,6 +311,11 @@ async function webviewPreloads(ctx: PreloadContext) { if (!outputContainer || !selection?.anchorNode) { return; } + const activeElement = window.document.activeElement; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') { + // Leave for default behavior. + return; + } // These should change the scroll position, not adjust the selected cell in the notebook e.stopPropagation(); // We don't want the notebook to handle this. @@ -318,6 +335,22 @@ async function webviewPreloads(ctx: PreloadContext) { selection.addRange(range); }; + const disableNativeSelectAll = (e: KeyboardEvent) => { + if (!lastFocusedOutput?.id) { + return; + } + const activeElement = window.document.activeElement; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') { + e.preventDefault(); // We will handle selection in editor code. + return; + } + + if ((e.key === 'a' && e.ctrlKey) || (e.metaKey && e.key === 'a')) { + e.preventDefault(); // We will handle selection in editor code. + return; + } + }; + const handleDataUrl = async (data: string | ArrayBuffer | null, downloadName: string) => { postNotebookMessage('clicked-data-url', { data, @@ -343,6 +376,7 @@ async function webviewPreloads(ctx: PreloadContext) { window.document.body.addEventListener('focusin', checkOutputInputFocus); window.document.body.addEventListener('focusout', handleOutputFocusOut); window.document.body.addEventListener('keydown', onPageUpDownSelectionHandler); + window.document.body.addEventListener('keydown', disableNativeSelectAll); interface RendererContext extends rendererApi.RendererContext { readonly onDidChangeSettings: Event; @@ -633,13 +667,19 @@ async function webviewPreloads(ctx: PreloadContext) { if (cellOutputContainer.contains(window.document.activeElement)) { return; } - + const id = cellOutputContainer.id; let focusableElement = cellOutputContainer.querySelector('[tabindex="0"], [href], button, input, option, select, textarea') as HTMLElement | null; if (!focusableElement) { focusableElement = cellOutputContainer; focusableElement.tabIndex = -1; + postNotebookMessage('outputInputFocus', { inputFocused: false, id }); + } else { + const inputFocused = focusableElement.tagName === 'INPUT' || focusableElement.tagName === 'TEXTAREA'; + postNotebookMessage('outputInputFocus', { inputFocused, id }); } + lastFocusedOutput = cellOutputContainer; + postNotebookMessage('outputFocus', { id: cellOutputContainer.id }); focusableElement.focus(); } } @@ -1695,6 +1735,9 @@ async function webviewPreloads(ctx: PreloadContext) { case 'select-output-contents': selectOutputContents(event.data.cellOrOutputId); break; + case 'select-input-contents': + selectInputContents(event.data.cellOrOutputId); + break; case 'decorations': { let outputContainer = window.document.getElementById(event.data.cellId); if (!outputContainer) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 51dc7be583f..da9b53fff5f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -115,6 +115,15 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._onDidChangeState.fire({ outputIsFocusedChanged: true }); } + private _focusInputInOutput: boolean = false; + public get inputInOutputIsFocused(): boolean { + return this._focusInputInOutput; + } + + public set inputInOutputIsFocused(v: boolean) { + this._focusInputInOutput = v; + } + private _outputMinHeight: number = 0; private get outputMinHeight() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 41fbef9a007..0f255cc20db 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -93,6 +93,14 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM this._focusOnOutput = v; } + public get inputInOutputIsFocused(): boolean { + return false; + } + + public set inputInOutputIsFocused(_: boolean) { + // + } + private _hoveringCell = false; public get cellIsHovered(): boolean { return this._hoveringCell;