mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-26 19:44:25 +01:00
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:
@@ -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
|
||||
}]
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user