diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index e5b8611e905..79e49832e41 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -260,10 +260,10 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit async hotExit() { } - async applyEdits(_edits: any[]) { } - async undoEdits(edits: any[]) { console.log('undo', edits); } + async applyEdits(edits: any[]) { console.log('apply', edits); } + //#endregion public test_makeEdit() { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 3865ff60cb1..29fe210a1b6 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -12,7 +12,6 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -21,7 +20,7 @@ import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } fr import { IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; -import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditorModel'; +import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; @@ -96,14 +95,13 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private readonly _webviewInputs = new WebviewInputStore(); private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); - private readonly _models = new Map(); constructor( context: extHostProtocol.IExtHostContext, @IExtensionService extensionService: IExtensionService, + @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IOpenerService private readonly _openerService: IOpenerService, @IProductService private readonly _productService: IProductService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -273,18 +271,20 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.webview.options = options; webviewInput.webview.extension = extension; - const model = this._instantiationService.createInstance(CustomEditorModel, webviewInput.getResource()); - webviewInput.setModel(model); - this._models.set(handle, model); - - webviewInput.onDispose(() => { - this._models.delete(handle); - }); + const model = await this._customEditorService.models.loadOrCreate(webviewInput.getResource(), webviewInput.viewType); model.onUndo(edit => { this._proxy.$undoEdits(handle, [edit]); }); + model.onRedo(edit => { + this._proxy.$redoEdits(handle, [edit]); + }); + + webviewInput.onDispose(() => { + this._customEditorService.models.disposeModel(model); + }); + try { await this._proxy.$resolveWebviewEditor( webviewInput.getResource(), @@ -318,7 +318,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma throw new Error('Webview is not a webview editor'); } - const model = this._models.get(handle); + const model = this._customEditorService.models.get(webview.getResource(), webview.viewType); if (!model) { throw new Error('Could not find model for webview editor'); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 60701e91b6f..88d442643ff 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -590,6 +590,7 @@ export interface ExtHostWebviewsShape { $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; $undoEdits(handle: WebviewPanelHandle, edits: string[]): void; + $redoEdits(handle: WebviewPanelHandle, edits: string[]): void; } export interface MainThreadUrlsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index e13466a070d..1fc1214a5e5 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -252,6 +252,10 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa assertIsDefined(this._capabilities).editingCapability?.undoEdits(edits); } + _redoEdits(edits: string[]): void { + assertIsDefined(this._capabilities).editingCapability?.applyEdits(edits); + } + private assertNotDisposed() { if (this._isDisposed) { throw new Error('Webview is disposed'); @@ -447,6 +451,14 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { panel._undoEdits(edits); } + $redoEdits(handle: WebviewPanelHandle, edits: string[]): void { + const panel = this.getWebviewPanel(handle); + if (!panel) { + return; + } + panel._redoEdits(edits); + } + private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { return this._webviewPanels.get(handle); } diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 5aefbd36ca8..cf1118961f2 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -120,12 +120,20 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { }); } - public runCommand(accessor: ServicesAccessor, _args: any): void { + public runCommand(accessor: ServicesAccessor): void { const customEditorService = accessor.get(ICustomEditorService); - if (!customEditorService.activeCustomEditor) { + + const activeCustomEditor = customEditorService.activeCustomEditor; + if (!activeCustomEditor) { return; } - console.log(customEditorService.activeCustomEditor); + + const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType); + if (!model) { + return; + } + + model.undo(); } }).register(); @@ -147,7 +155,19 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { }); } - public runCommand(_accessor: ServicesAccessor, args: any): void { - console.log('redo', args); + public runCommand(accessor: ServicesAccessor): void { + const customEditorService = accessor.get(ICustomEditorService); + + const activeCustomEditor = customEditorService.activeCustomEditor; + if (!activeCustomEditor) { + return; + } + + const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType); + if (!model) { + return; + } + + model.redo(); } }).register(); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index e6ee2764843..56c6b0320a9 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -3,15 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, distinct, mergeSort, find } from 'vs/base/common/arrays'; +import { coalesce, distinct, find, mergeSort } from 'vs/base/common/arrays'; import * as glob from 'vs/base/common/glob'; -import { UnownedDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable, UnownedDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename, DataUri, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -21,14 +23,14 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorInput, EditorOptions, IEditor, IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; -import { CustomEditorPriority, CustomEditorInfo, CustomEditorSelector, ICustomEditorService, CONTEXT_HAS_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, ICustomEditor } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, CustomEditorInfo, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { CustomFileEditorInput } from './customEditorInput'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { Lazy } from 'vs/base/common/lazy'; const defaultEditorId = 'default'; @@ -73,11 +75,15 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _editorInfoStore = new CustomEditorInfoStore(); + private readonly _models: CustomEditorModelManager; + private readonly _hasCustomEditor: IContextKey; private readonly _focusedCustomEditorIsEditable: IContextKey; + private readonly _webviewHasOwnEditFunctions: IContextKey; constructor( @IContextKeyService contextKeyService: IContextKeyService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -86,6 +92,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ) { super(); + this._models = new CustomEditorModelManager(workingCopyService); + webviewEditorsExtensionPoint.setHandler(extensions => { this._editorInfoStore.clear(); @@ -104,21 +112,21 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._hasCustomEditor = CONTEXT_HAS_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); + this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); this.updateContexts(); } + public get models() { return this._models; } + public get activeCustomEditor(): ICustomEditor | undefined { const activeInput = this.editorService.activeControl?.input; if (!(activeInput instanceof CustomFileEditorInput)) { return undefined; } - - return { - resource: activeInput.getResource(), - model: undefined, - }; + const resource = activeInput.getResource(); + return { resource, viewType: activeInput.viewType }; } public getContributedCustomEditors(resource: URI): readonly CustomEditorInfo[] { @@ -236,6 +244,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ if (!resource) { this._hasCustomEditor.reset(); this._focusedCustomEditorIsEditable.reset(); + this._webviewHasOwnEditFunctions.reset(); return; } @@ -245,6 +254,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ]; this._hasCustomEditor.set(possibleEditors.length > 0); this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomFileEditorInput); + this._webviewHasOwnEditFunctions.set(true); } } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 408c09a0f71..b0f65e94a60 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { EditorInput, IEditor } from 'vs/workbench/common/editor'; -import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditorModel'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const ICustomEditorService = createDecorator('customEditorService'); @@ -18,12 +19,14 @@ export const CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE = new RawContextKey; } +export type CustomEditorEdit = string; + +export interface ICustomEditorModelManager { + get(resource: URI, viewType: string): ICustomEditorModel | undefined; + + loadOrCreate(resource: URI, viewType: string): Promise; + + disposeModel(model: ICustomEditorModel): void; +} + +export interface ICustomEditorModel extends IWorkingCopy { + readonly onUndo: Event; + readonly onRedo: Event; + + undo(): void; + redo(): void; + + makeEdit(data: string): void; +} + export const enum CustomEditorPriority { default = 'default', builtin = 'builtin', diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 255c8f09f16..9a34c46ead5 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -6,22 +6,20 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IWorkingCopy, IWorkingCopyService, WorkingCopyCapabilities, ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ICustomEditorModel, CustomEditorEdit } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { IRevertOptions, ISaveOptions, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -type Edit = string; -export class CustomEditorModel extends Disposable implements IWorkingCopy { +export class CustomEditorModel extends Disposable implements ICustomEditorModel { - private _currentEditIndex: number = 0; + private _currentEditIndex: number = -1; private _savePoint: number = -1; - private _edits: Array = []; + private _edits: Array = []; constructor( private readonly _resource: URI, - @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); - this._register(this._workingCopyService.registerWorkingCopy(this)); } //#region IWorkingCopy @@ -43,8 +41,11 @@ export class CustomEditorModel extends Disposable implements IWorkingCopy { //#endregion - protected readonly _onUndo: Emitter = this._register(new Emitter()); - readonly onUndo: Event = this._onUndo.event; + protected readonly _onUndo = this._register(new Emitter()); + readonly onUndo: Event = this._onUndo.event; + + protected readonly _onRedo = this._register(new Emitter()); + readonly onRedo: Event = this._onRedo.event; public makeEdit(data: string): void { this._edits.splice(this._currentEditIndex, this._edits.length - this._currentEditIndex, data); @@ -72,11 +73,30 @@ export class CustomEditorModel extends Disposable implements IWorkingCopy { } public undo() { - if (this._currentEditIndex >= 0) { - const undoneEdit = this._edits[this._currentEditIndex]; - --this._currentEditIndex; - this._onUndo.fire(undoneEdit); + if (this._currentEditIndex < 0) { + // nothing to undo + return; } + + const undoneEdit = this._edits[this._currentEditIndex]; + --this._currentEditIndex; + this._onUndo.fire(undoneEdit); + + this.updateDirty(); + } + + public redo() { + if (this._currentEditIndex >= this._edits.length - 1) { + // nothing to redo + return; + } + + ++this._currentEditIndex; + const redoneEdit = this._edits[this._currentEditIndex]; + this._onRedo.fire(redoneEdit); + this.updateDirty(); } } + + diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts new file mode 100644 index 00000000000..b97897df8c4 --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ICustomEditorModel, ICustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditorModel'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; + +export class CustomEditorModelManager implements ICustomEditorModelManager { + private readonly _models = new Map(); + + constructor( + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, + ) { } + + + public get(resource: URI, viewType: string): ICustomEditorModel | undefined { + return this._models.get(this.key(resource, viewType))?.model; + } + + public async loadOrCreate(resource: URI, viewType: string): Promise { + const existing = this.get(resource, viewType); + if (existing) { + return existing; + } + + const model = new CustomEditorModel(resource); + const disposables = new DisposableStore(); + this._workingCopyService.registerWorkingCopy(model); + this._models.set(this.key(resource, viewType), { model, disposables }); + return model; + } + + public disposeModel(model: ICustomEditorModel): void { + let foundKey: string | undefined; + this._models.forEach((value, key) => { + if (model === value.model) { + value.disposables.dispose(); + foundKey = key; + } + }); + if (typeof foundKey === 'string') { + this._models.delete(foundKey); + } + return; + } + + private key(resource: URI, viewType: string): string { + return `${resource.toString()}@@@${viewType}`; + } +} diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index cf633fbc463..c9c0c88a08c 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -19,6 +19,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED = new RawContextKey('webviewFindWidgetFocused', false); +export const webviewHasOwnEditFunctionsContextKey = 'webviewHasOwnEditFunctions'; +export const webviewHasOwnEditFunctionsContext = new RawContextKey(webviewHasOwnEditFunctionsContextKey, false); + export const IWebviewService = createDecorator('webviewService'); /** diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts index e69143e179b..e93e2c38955 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts @@ -11,7 +11,7 @@ import * as nls from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewEditorOverlay, webviewHasOwnEditFunctionsContextKey } from 'vs/workbench/contrib/webview/browser/webview'; import { getActiveWebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewCommands'; import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; @@ -119,7 +119,7 @@ export class UndoWebviewEditorCommand extends Command { constructor(contextKeyExpr: ContextKeyExpr) { super({ id: UndoWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey), ContextKeyExpr.not(webviewHasOwnEditFunctionsContextKey)), kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, weight: KeybindingWeight.EditorContrib @@ -138,7 +138,7 @@ export class RedoWebviewEditorCommand extends Command { constructor(contextKeyExpr: ContextKeyExpr) { super({ id: RedoWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey), ContextKeyExpr.not(webviewHasOwnEditFunctionsContextKey)), kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z],