// ***************************************************************************** // Copyright (C) 2018 Red Hat, Inc. and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as theia from '@theia/plugin'; import * as model from '../common/plugin-api-rpc-model'; import { CommandRegistryExt, PLUGIN_RPC_CONTEXT as Ext, CommandRegistryMain } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; import { Disposable, URI } from './types-impl'; import { DisposableCollection } from '@theia/core'; import { KnownCommands } from './known-commands'; import { ArgumentProcessor } from '../common/commands'; import { isUriComponents } from './type-converters'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Handler = (...args: any[]) => T | PromiseLike; export class CommandRegistryImpl implements CommandRegistryExt { private proxy: CommandRegistryMain; private readonly commands = new Set(); private readonly handlers = new Map(); private readonly argumentProcessors: ArgumentProcessor[]; private readonly commandsConverter: CommandsConverter; constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(Ext.COMMAND_REGISTRY_MAIN); this.argumentProcessors = []; this.commandsConverter = new CommandsConverter(this); this.registerArgumentProcessor({ processArgument: arg => { // Revive URI-like plain objects that lost their class identity during RPC/JSON serialization // (e.g. command arguments from markdown links parsed by CommandOpenHandler) if (isUriComponents(arg) && !(arg instanceof URI)) { return URI.revive(arg); } return arg; } }); } get converter(): CommandsConverter { return this.commandsConverter; } // eslint-disable-next-line @typescript-eslint/no-explicit-any registerCommand(command: theia.CommandDescription, handler?: Handler, thisArg?: any): Disposable { if (this.commands.has(command.id)) { throw new Error(`Command ${command.id} already exist`); } this.commands.add(command.id); this.proxy.$registerCommand(command); const toDispose: Disposable[] = []; if (handler) { toDispose.push(this.registerHandler(command.id, handler, thisArg)); } toDispose.push(Disposable.create(() => { this.commands.delete(command.id); this.proxy.$unregisterCommand(command.id); })); return Disposable.from(...toDispose); } // eslint-disable-next-line @typescript-eslint/no-explicit-any registerHandler(commandId: string, handler: Handler, thisArg?: any): Disposable { if (this.handlers.has(commandId)) { throw new Error(`Command "${commandId}" already has handler`); } this.proxy.$registerHandler(commandId); // eslint-disable-next-line @typescript-eslint/no-explicit-any this.handlers.set(commandId, (...args: any[]) => handler.apply(thisArg, args)); return Disposable.create(() => { this.handlers.delete(commandId); this.proxy.$unregisterHandler(commandId); }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any $executeCommand(id: string, ...args: any[]): PromiseLike { if (this.handlers.has(id)) { return this.executeLocalCommand(id, ...args); } else { return Promise.reject(new Error(`Command: ${id} does not exist.`)); } } /* eslint-disable @typescript-eslint/no-explicit-any */ executeCommand(id: string, ...args: any[]): PromiseLike { if (this.handlers.has(id)) { return this.executeLocalCommand(id, ...args); } else if (KnownCommands.mapped(id)) { // Using the KnownCommand exclusions, convert the commands manually return KnownCommands.map(id, args, (mappedId: string, mappedArgs: any[] | undefined, mappedResult: KnownCommands.ConversionFunction) => { const mr: KnownCommands.ConversionFunction = mappedResult; return this.proxy.$executeCommand(mappedId, ...mappedArgs || []).then((result: any) => { if (!result) { return undefined; } if (!mr) { return result; } return mr(result); }); } ); } else { return this.proxy.$executeCommand(id, ...args); } } /* eslint-enable @typescript-eslint/no-explicit-any */ getKeyBinding(commandId: string): PromiseLike { return this.proxy.$getKeyBinding(commandId); } // eslint-disable-next-line @typescript-eslint/no-explicit-any private async executeLocalCommand(id: string, ...args: any[]): Promise { const handler = this.handlers.get(id); if (handler) { return handler(...args.map(arg => this.argumentProcessors.reduce((r, p) => p.processArgument(r), arg))); } else { throw new Error(`No handler exists for command '${id}'`); } } async getCommands(filterUnderscoreCommands: boolean = false): Promise { const result = await this.proxy.$getCommands(); if (filterUnderscoreCommands) { return result.filter(command => command[0] !== '_'); } return result; } registerArgumentProcessor(processor: ArgumentProcessor): void { this.argumentProcessors.push(processor); } } // copied and modified from https://github.com/microsoft/vscode/blob/1.37.1/src/vs/workbench/api/common/extHostCommands.ts#L217-L259 export class CommandsConverter { private readonly safeCommandId: string; private readonly commands: CommandRegistryImpl; private readonly commandsMap = new Map(); private handle = 0; private isSafeCommandRegistered: boolean; constructor(commands: CommandRegistryImpl) { this.safeCommandId = `theia_safe_cmd_${Date.now().toString()}`; this.commands = commands; this.isSafeCommandRegistered = false; } /** * Convert to a command that can be safely passed over JSON-RPC. */ toSafeCommand(command: undefined, disposables: DisposableCollection): undefined; toSafeCommand(command: theia.Command, disposables: DisposableCollection): model.Command; toSafeCommand(command: theia.Command | undefined, disposables: DisposableCollection): model.Command | undefined; toSafeCommand(command: theia.Command | undefined, disposables: DisposableCollection): model.Command | undefined { if (!command) { return undefined; } const result = this.toInternalCommand(command); if (KnownCommands.mapped(result.id)) { return result; } if (!this.isSafeCommandRegistered) { this.commands.registerCommand({ id: this.safeCommandId }, this.executeSafeCommand, this); this.isSafeCommandRegistered = true; } if (command.arguments && command.arguments.length > 0) { const id = this.handle++; this.commandsMap.set(id, command); disposables.push(new Disposable(() => this.commandsMap.delete(id))); result.id = this.safeCommandId; result.arguments = [id]; } return result; } protected toInternalCommand(external: theia.Command): model.Command { // we're deprecating Command.id, so it has to be optional. // Existing code will have compiled against a non - optional version of the field, so asserting it to exist is ok // eslint-disable-next-line @typescript-eslint/no-explicit-any return KnownCommands.map(external.command, external.arguments, (mappedId: string, mappedArgs: any[]) => ({ id: mappedId, title: external.title || ' ', tooltip: external.tooltip, arguments: mappedArgs })); } // eslint-disable-next-line @typescript-eslint/no-explicit-any private executeSafeCommand(...args: any[]): PromiseLike { const handle = args[0]; if (typeof handle !== 'number') { return Promise.reject(`Invalid handle ${handle}`); } const command = this.commandsMap.get(handle); if (!command || !command.command) { return Promise.reject(`Safe command with handle ${handle} not found`); } const allArgs = (command.arguments ?? []).concat(args.slice(1)); return this.commands.executeCommand(command.command, ...allArgs); } }