Let core command such as undo/redo be overridden by webviews and notebooks (#98288)

* Add CoreCommandService

For #90110

This change introduce a new service (`ICoreCommandService`) that lets high levels parts of the editor (such as webviews) hook into core commands such as undo and redo that are fired from the editor menus.

## Why is this needed?
To implement undo/redo in custom editors / webviews, we currently register special commands (`editor.action.customEditor.undo` and `editor.action.customEditor.redo`). This is not ideal since it means that users have to update multiple commands if they wish to rebind undo/redo

These custom command also are not invoked when the user goes to the `edit` menu and select `undo` or `redo`. Instead, the edit menu always calls the core `UndoCommand`

We cannot make `UndoCommand` know about custom editors because it lives in `base`

## What this change does?
This change adds a new `ICoreCommandService` that lets higher level parts of the editor override core commands such as undo and redo. We use a similar approach in the `IOpenerService`.

Right now only `undo` and `redo` are overridable. If this approach looks ok, we could also extend it to `copy`, `paste`, `cut`, and `select all`

* Add docs and clean up types

* Rework implementation

Switch from using a command service to having each command state if is overrideable or not.

This hooks up overrides for:

- Undo/redo
- cut/copy/paste

for webviews, custom editors, and notebooks

* Add ProxyCommand so that multiple registered commands can share a single implementation

* Fix compilation & missing file being referenced

* Introduce and adopt MultiCommand for Undo

* Adopt MultiCommand for Redo and SelectAll

* Adopt MultiCommand for Cut, Copy and Paste

Co-authored-by: Alex Dima <alexdima@microsoft.com>
This commit is contained in:
Matt Bierner
2020-06-19 16:54:22 -07:00
committed by GitHub
parent 2c73c38a11
commit 97650fb8f3
12 changed files with 465 additions and 583 deletions

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IPosition } from 'vs/base/browser/ui/contextview/contextview';
import { illegalArgument } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
@@ -17,11 +18,13 @@ import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { IConstructorSignature1, ServicesAccessor as InstantiationServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindings, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IKeybindings, KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { withNullAsUndefined, assertType } from 'vs/base/common/types';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
export type ServicesAccessor = InstantiationServicesAccessor;
@@ -139,6 +142,66 @@ export abstract class Command {
//#endregion Command
//#region MultiplexingCommand
/**
* Potential override for a command.
*
* @return `true` if the command was successfully run. This stops other overrides from being executed.
*/
export type CommandImplementation = (accessor: ServicesAccessor, args: unknown) => boolean;
export class MultiCommand extends Command {
private readonly _implementations: [number, CommandImplementation][] = [];
/**
* A higher priority gets to be looked at first
*/
public addImplementation(priority: number, implementation: CommandImplementation): IDisposable {
this._implementations.push([priority, implementation]);
this._implementations.sort((a, b) => b[0] - a[0]);
return {
dispose: () => {
for (let i = 0; i < this._implementations.length; i++) {
if (this._implementations[i][1] === implementation) {
this._implementations.splice(i, 1);
return;
}
}
}
};
}
public runCommand(accessor: ServicesAccessor, args: any): void | Promise<void> {
for (const impl of this._implementations) {
if (impl[1](accessor, args)) {
return;
}
}
}
}
//#endregion
/**
* A command that delegates to another command's implementation.
*
* This lets different commands be registered but share the same implementation
*/
export class ProxyCommand extends Command {
constructor(
private readonly command: Command,
opts: ICommandOptions
) {
super(opts);
}
public runCommand(accessor: ServicesAccessor, args: any): void | Promise<void> {
return this.command.runCommand(accessor, args);
}
}
//#region EditorCommand
export interface IContributionCommandOptions<T> extends ICommandOptions {
@@ -379,8 +442,10 @@ export function registerEditorCommand<T extends EditorCommand>(editorCommand: T)
return editorCommand;
}
export function registerEditorAction(ctor: { new(): EditorAction; }): void {
EditorContributionRegistry.INSTANCE.registerEditorAction(new ctor());
export function registerEditorAction<T extends EditorAction>(ctor: { new(): T; }): T {
const action = new ctor();
EditorContributionRegistry.INSTANCE.registerEditorAction(action);
return action;
}
export function registerInstantiatedEditorAction(editorAction: EditorAction): void {
@@ -475,3 +540,75 @@ class EditorContributionRegistry {
}
Registry.add(Extensions.EditorCommonContributions, EditorContributionRegistry.INSTANCE);
function registerCommand<T extends Command>(command: T): T {
command.register();
return command;
}
export const UndoCommand = registerCommand(new MultiCommand({
id: 'undo',
precondition: undefined,
kbOpts: {
weight: KeybindingWeight.EditorCore,
primary: KeyMod.CtrlCmd | KeyCode.KEY_Z
},
menuOpts: [{
menuId: MenuId.MenubarEditMenu,
group: '1_do',
title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"),
order: 1
}, {
menuId: MenuId.CommandPalette,
group: '',
title: nls.localize('undo', "Undo"),
order: 1
}]
}));
registerCommand(new ProxyCommand(UndoCommand, { id: 'default:undo', precondition: undefined }));
export const RedoCommand = registerCommand(new MultiCommand({
id: 'redo',
precondition: undefined,
kbOpts: {
weight: KeybindingWeight.EditorCore,
primary: KeyMod.CtrlCmd | KeyCode.KEY_Y,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z],
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z }
},
menuOpts: [{
menuId: MenuId.MenubarEditMenu,
group: '1_do',
title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"),
order: 2
}, {
menuId: MenuId.CommandPalette,
group: '',
title: nls.localize('redo', "Redo"),
order: 1
}]
}));
registerCommand(new ProxyCommand(RedoCommand, { id: 'default:redo', precondition: undefined }));
export const SelectAllCommand = registerCommand(new MultiCommand({
id: 'editor.action.selectAll',
precondition: undefined,
kbOpts: {
weight: KeybindingWeight.EditorCore,
kbExpr: null,
primary: KeyMod.CtrlCmd | KeyCode.KEY_A
},
menuOpts: [{
menuId: MenuId.MenubarSelectionMenu,
group: '1_basic',
title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"),
order: 1
}, {
menuId: MenuId.CommandPalette,
group: '',
title: nls.localize('selectAll', "Select All"),
order: 1
}]
}));