Path: blob/main/src/vs/platform/commands/common/commands.ts
5263 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../base/common/event.js';6import { Iterable } from '../../../base/common/iterator.js';7import { IJSONSchema } from '../../../base/common/jsonSchema.js';8import { IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js';9import { LinkedList } from '../../../base/common/linkedList.js';10import { TypeConstraint, validateConstraints } from '../../../base/common/types.js';11import { ILocalizedString } from '../../action/common/action.js';12import { createDecorator, ServicesAccessor } from '../../instantiation/common/instantiation.js';1314export const ICommandService = createDecorator<ICommandService>('commandService');1516export interface ICommandEvent {17readonly commandId: string;18readonly args: unknown[];19}2021export interface ICommandService {22readonly _serviceBrand: undefined;23readonly onWillExecuteCommand: Event<ICommandEvent>;24readonly onDidExecuteCommand: Event<ICommandEvent>;25executeCommand<R = unknown>(commandId: string, ...args: unknown[]): Promise<R | undefined>;26}2728export type ICommandsMap = Map<string, ICommand>;2930export type ICommandHandler<Args extends unknown[] = unknown[], R = void> = (accessor: ServicesAccessor, ...args: Args) => R;3132export interface ICommand<Args extends unknown[] = unknown[], R = void> {33id: string;34handler: ICommandHandler<Args, R>;35metadata?: ICommandMetadata | null;36}3738export interface ICommandMetadata {39/**40* NOTE: Please use an ILocalizedString. string is in the type for backcompat for now.41* A short summary of what the command does. This will be used in:42* - API commands43* - when showing keybindings that have no other UX44* - when searching for commands in the Command Palette45*/46readonly description: ILocalizedString | string;47readonly args?: ReadonlyArray<{48readonly name: string;49readonly isOptional?: boolean;50readonly description?: string;51readonly constraint?: TypeConstraint;52readonly schema?: IJSONSchema;53}>;54readonly returns?: string;55}5657export interface ICommandRegistry {58readonly onDidRegisterCommand: Event<string>;59registerCommand<Args extends unknown[]>(id: string, command: ICommandHandler<Args>): IDisposable;60registerCommand<Args extends unknown[]>(command: ICommand<Args>): IDisposable;61registerCommandAlias(oldId: string, newId: string): IDisposable;62getCommand(id: string): ICommand | undefined;63getCommands(): ICommandsMap;64}6566export const CommandsRegistry: ICommandRegistry = new class implements ICommandRegistry {6768private readonly _commands = new Map<string, LinkedList<ICommand>>();6970private readonly _onDidRegisterCommand = new Emitter<string>();71readonly onDidRegisterCommand: Event<string> = this._onDidRegisterCommand.event;7273registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): IDisposable {7475if (!idOrCommand) {76throw new Error(`invalid command`);77}7879if (typeof idOrCommand === 'string') {80if (!handler) {81throw new Error(`invalid command`);82}83return this.registerCommand({ id: idOrCommand, handler });84}8586// add argument validation if rich command metadata is provided87if (idOrCommand.metadata && Array.isArray(idOrCommand.metadata.args)) {88const constraints: Array<TypeConstraint | undefined> = [];89for (const arg of idOrCommand.metadata.args) {90constraints.push(arg.constraint);91}92const actualHandler = idOrCommand.handler;93idOrCommand.handler = function (accessor, ...args: unknown[]) {94validateConstraints(args, constraints);95return actualHandler(accessor, ...args);96};97}9899// find a place to store the command100const { id } = idOrCommand;101102let commands = this._commands.get(id);103if (!commands) {104commands = new LinkedList<ICommand>();105this._commands.set(id, commands);106}107108const removeFn = commands.unshift(idOrCommand);109110const ret = toDisposable(() => {111removeFn();112const command = this._commands.get(id);113if (command?.isEmpty()) {114this._commands.delete(id);115}116});117118// tell the world about this command119this._onDidRegisterCommand.fire(id);120121return markAsSingleton(ret);122}123124registerCommandAlias(oldId: string, newId: string): IDisposable {125return CommandsRegistry.registerCommand(oldId, (accessor, ...args) => accessor.get(ICommandService).executeCommand(newId, ...args));126}127128getCommand(id: string): ICommand | undefined {129const list = this._commands.get(id);130if (!list || list.isEmpty()) {131return undefined;132}133return Iterable.first(list);134}135136getCommands(): ICommandsMap {137const result = new Map<string, ICommand>();138for (const key of this._commands.keys()) {139const command = this.getCommand(key);140if (command) {141result.set(key, command);142}143}144return result;145}146};147148CommandsRegistry.registerCommand('noop', () => { });149150151