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

Rework copy paste and other browser events for webviews #101958

Merged
merged 1 commit into from Jul 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts
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);
}
}
}
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
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@deepak1556 I opted against using before-input-event since this problem also effects iframe based webviews

// 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
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
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
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());