Skip to content

Commit

Permalink
Rework copy paste and other browser events for webviews (#101958)
Browse files Browse the repository at this point in the history
Fixes #101946

Webview can currently trigger some keyboard events twice. Sequence of events:

- User presses ctrl+v with a webview focused
- Webview is listening to keyboard events for rebroadcast (so that we can handle normal VS code commands even when focused on webviews)
- We rebroadcast the keypresses back to VS Code
- The webview then triggers the standard copy behavior on its own (I believe this is either chromium or electron)
- VS Code gets the ctrl+v keypress event and resolves it to the 'paste' command
- The paste command triggers the paste method on the webview
- This calls back into the webview content to trigger a second paste

This does not happen in cases where we are using native menus, which can call `setIgnoreMenuShortcuts` to disable the browser geenrated paste event.

## The fix
To fix this, I think we want to completely block the browser generate events in all cases and instead always dispatch the events through VS Code. This should ensure more consistent behavior.

This PR does this by:

- In the webview, add a keypress listener for copy/paste/cut and undo/redo. When we see these events, call `preventDefault` to block them but still dispatch back to VS Code

- In VS Code, more the  logic for triggering undo/redo, etc. on webviews out of the electron layer and into the browser layer. iframe based webviews have the exact same problem as electron based webviews, so we need to fix this issue for both of them.
  • Loading branch information
mjbvz authored Jul 13, 2020
1 parent b6cf8e8 commit 7e5937d
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 56 deletions.
26 changes: 25 additions & 1 deletion src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,32 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
}

public selectAll() {
this.execCommand('selectAll');
}

public copy() {
this.execCommand('copy');
}

public paste() {
this.execCommand('paste');
}

public cut() {
this.execCommand('cut');
}

public undo() {
this.execCommand('undo');
}

public redo() {
this.execCommand('redo');
}

private execCommand(command: string) {
if (this.element) {
this._send('execCommand', 'selectAll');
this._send('execCommand', command);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
focus(): void { this.withWebview(webview => webview.focus()); }
reload(): void { this.withWebview(webview => webview.reload()); }
selectAll(): void { this.withWebview(webview => webview.selectAll()); }
copy(): void { this.withWebview(webview => webview.copy()); }
paste(): void { this.withWebview(webview => webview.paste()); }
cut(): void { this.withWebview(webview => webview.cut()); }
undo(): void { this.withWebview(webview => webview.undo()); }
redo(): void { this.withWebview(webview => webview.redo()); }

showFind() {
if (this._webview.value) {
Expand Down
26 changes: 26 additions & 0 deletions src/vs/workbench/contrib/webview/browser/pre/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@
* @param {KeyboardEvent} e
*/
const handleInnerKeydown = (e) => {
// If the keypress would trigger a browser event, such as copy or paste,
// make sure we block the browser from dispatching it. Instead VS Code
// handles these events and will dispatch a copy/paste back to the webview
// if needed
if (isCopyPasteOrCut(e) || isUndoRedo(e)) {
e.preventDefault();
}

host.postMessage('did-keydown', {
key: e.key,
keyCode: e.keyCode,
Expand All @@ -289,6 +297,24 @@
});
};

/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isCopyPasteOrCut(e) {
const hasMeta = e.ctrlKey || e.metaKey;
return hasMeta && ['c', 'v', 'x'].includes(e.key);
}

/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isUndoRedo(e) {
const hasMeta = e.ctrlKey || e.metaKey;
return hasMeta && ['z', 'y'].includes(e.key);
}

let isHandlingScroll = false;

const handleWheel = (event) => {
Expand Down
45 changes: 44 additions & 1 deletion src/vs/workbench/contrib/webview/browser/webview.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { MultiCommand, RedoCommand, SelectAllCommand, ServicesAccessor, UndoCommand } from 'vs/editor/browser/editorExtensions';
import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard';
import { localize } from 'vs/nls';
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import { Webview, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory';
import { HideWebViewEditorFindCommand, ReloadWebviewAction, SelectAllWebviewEditorCommand, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands';
import { getActiveWebview, HideWebViewEditorFindCommand, ReloadWebviewAction, SelectAllWebviewEditorCommand, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands';
import { WebviewEditor } from './webviewEditor';
import { WebviewInput } from './webviewEditorInput';
import { IWebviewWorkbenchService, WebviewEditorService } from './webviewWorkbenchService';
Expand All @@ -35,3 +38,43 @@ registerAction2(WebViewEditorFindPreviousCommand);
registerAction2(SelectAllWebviewEditorCommand);
registerAction2(ReloadWebviewAction);


function getActiveElectronBasedWebview(accessor: ServicesAccessor): Webview | undefined {
const webview = getActiveWebview(accessor);
if (!webview) {
return undefined;
}

// Make sure we are really focused on the webview
if (!['WEBVIEW', 'IFRAME'].includes(document.activeElement?.tagName ?? '')) {
return undefined;
}

if ('getInnerWebview' in (webview as WebviewOverlay)) {
const innerWebview = (webview as WebviewOverlay).getInnerWebview();
return innerWebview;
}

return webview;
}


const PRIORITY = 100;

function overrideCommandForWebview(command: MultiCommand | undefined, f: (webview: Webview) => void) {
command?.addImplementation(PRIORITY, accessor => {
const webview = getActiveElectronBasedWebview(accessor);
if (webview) {
f(webview);
return true;
}
return false;
});
}

overrideCommandForWebview(UndoCommand, webview => webview.undo());
overrideCommandForWebview(RedoCommand, webview => webview.redo());
overrideCommandForWebview(SelectAllCommand, webview => webview.selectAll());
overrideCommandForWebview(CopyAction, webview => webview.copy());
overrideCommandForWebview(PasteAction, webview => webview.paste());
overrideCommandForWebview(CutAction, webview => webview.cut());
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/webview/browser/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ export interface Webview extends IDisposable {
runFindAction(previous: boolean): void;

selectAll(): void;
copy(): void;
paste(): void;
cut(): void;
undo(): void;
redo(): void;

windowDidDragStart(): void;
windowDidDragEnd(): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { MultiCommand, RedoCommand, SelectAllCommand, UndoCommand } from 'vs/editor/browser/editorExtensions';
import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard';
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { getActiveWebview } from 'vs/workbench/contrib/webview/browser/webviewCommands';
import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
import * as webviewCommands from 'vs/workbench/contrib/webview/electron-browser/webviewCommands';
import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
import { ElectronWebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService';
import { isMacintosh } from 'vs/base/common/platform';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';

registerSingleton(IWebviewService, ElectronWebviewService, true);

registerAction2(webviewCommands.OpenWebviewDeveloperToolsAction);

function getActiveElectronBasedWebview(accessor: ServicesAccessor): ElectronWebviewBasedWebview | undefined {
const webview = getActiveWebview(accessor);
if (!webview) {
return undefined;
}

// Make sure we are really focused on the webview
if (!['WEBVIEW', 'IFRAME'].includes(document.activeElement?.tagName ?? '')) {
return undefined;
}

if (webview instanceof ElectronWebviewBasedWebview) {
return webview;
} else if ('getInnerWebview' in (webview as WebviewOverlay)) {
const innerWebview = (webview as WebviewOverlay).getInnerWebview();
if (innerWebview instanceof ElectronWebviewBasedWebview) {
return innerWebview;
}
}

return undefined;
}

const PRIORITY = 100;

function overrideCommandForWebview(command: MultiCommand | undefined, f: (webview: ElectronWebviewBasedWebview) => void) {
command?.addImplementation(PRIORITY, accessor => {
if (isMacintosh || accessor.get(IConfigurationService).getValue<string>('window.titleBarStyle') === 'native') {
const webview = getActiveElectronBasedWebview(accessor);
if (webview) {
f(webview);
return true;
}
}

return false;
});
}

overrideCommandForWebview(UndoCommand, webview => webview.undo());
overrideCommandForWebview(RedoCommand, webview => webview.redo());
overrideCommandForWebview(SelectAllCommand, webview => webview.selectAll());
overrideCommandForWebview(CopyAction, webview => webview.copy());
overrideCommandForWebview(PasteAction, webview => webview.paste());
overrideCommandForWebview(CutAction, webview => webview.cut());

0 comments on commit 7e5937d

Please sign in to comment.