From c5b598e7a586b6db8f0192be21526f046c00b96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Mon, 4 Feb 2019 10:03:50 +0100 Subject: [PATCH] Map well known commands to theia cmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- CHANGELOG.md | 3 + packages/plugin-ext/src/api/plugin-api.ts | 2 +- .../src/main/browser/command-registry-main.ts | 2 +- .../plugin-ext/src/plugin/command-registry.ts | 117 ++---------------- .../src/plugin/languages/code-action.ts | 4 +- .../plugin-ext/src/plugin/languages/lens.ts | 3 +- .../plugin-ext/src/plugin/plugin-context.ts | 2 +- .../plugin-ext/src/plugin/tree/tree-views.ts | 2 +- .../plugin-ext/src/plugin/type-converters.ts | 72 +++++++++-- packages/plugin/src/theia.d.ts | 44 ++++--- 10 files changed, 112 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db5e0128c3492..1f703cdc24779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - [plugin] added `tasks.onDidEndTask` Plug-in API - [cpp] fixed `CPP_CLANGD_COMMAND` and `CPP_CLANGD_ARGS` environment variables - [electron] open markdown links in the OS default browser +- [plugin] the "Command" interface has been split into two: "CommandDescription" and "Command". "Command" has been +made compatible with the "Command" interface in vscode. This is not a breaking change, currently, but fields in those interfaces +have been deprecated and will be removed in the future. Breaking changes: - menus aligned with built-in VS Code menus [#4173](https://github.com/theia-ide/theia/pull/4173) diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index 236e5cad28706..f63bb9bc432e4 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -153,7 +153,7 @@ export interface PluginManagerExt { } export interface CommandRegistryMain { - $registerCommand(command: theia.Command): void; + $registerCommand(command: theia.CommandDescription): void; $unregisterCommand(id: string): void; $registerHandler(id: string): void; diff --git a/packages/plugin-ext/src/main/browser/command-registry-main.ts b/packages/plugin-ext/src/main/browser/command-registry-main.ts index c1fb56d0538ac..077b631d68528 100644 --- a/packages/plugin-ext/src/main/browser/command-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/command-registry-main.ts @@ -35,7 +35,7 @@ export class CommandRegistryMainImpl implements CommandRegistryMain { this.keyBinding = container.get(KeybindingRegistry); } - $registerCommand(command: theia.Command): void { + $registerCommand(command: theia.CommandDescription): void { this.commands.set(command.id, this.delegate.registerCommand(command)); } $unregisterCommand(id: string): void { diff --git a/packages/plugin-ext/src/plugin/command-registry.ts b/packages/plugin-ext/src/plugin/command-registry.ts index 373e3cecbb38a..47194b77d7e29 100644 --- a/packages/plugin-ext/src/plugin/command-registry.ts +++ b/packages/plugin-ext/src/plugin/command-registry.ts @@ -18,8 +18,7 @@ import * as theia from '@theia/plugin'; import { CommandRegistryExt, PLUGIN_RPC_CONTEXT as Ext, CommandRegistryMain } from '../api/plugin-api'; import { RPCProtocol } from '../api/rpc-protocol'; import { Disposable } from './types-impl'; -import { Command } from '../api/model'; -import { ObjectIdentifier } from '../common/object-identifier'; +import { KnownCommands } from './type-converters'; // tslint:disable-next-line:no-any export type Handler = (...args: any[]) => T | PromiseLike; @@ -29,9 +28,6 @@ export class CommandRegistryImpl implements CommandRegistryExt { private proxy: CommandRegistryMain; private readonly commands = new Set(); private readonly handlers = new Map(); - private converter: CommandsConverter; - private cache = new Map(); - private delegatingCommandId: string; // tslint:disable-next-line:no-any private static EMPTY_HANDLER(...args: any[]): Promise { return Promise.resolve(undefined); } @@ -43,22 +39,8 @@ export class CommandRegistryImpl implements CommandRegistryExt { this.registerCommand({ id: 'vscode.previewHtml' }, CommandRegistryImpl.EMPTY_HANDLER); } - getConverter(): CommandsConverter { - if (this.converter) { - return this.converter; - } else { - this.delegatingCommandId = `_internal_command_delegation_${Date.now()}`; - const command: theia.Command = { - id: this.delegatingCommandId - }; - this.registerCommand(command, this.executeConvertedCommand); - this.converter = new CommandsConverter(this.delegatingCommandId, this.cache); - return this.converter; - } - } - // tslint:disable-next-line:no-any - registerCommand(command: theia.Command, handler?: Handler, thisArg?: any): Disposable { + registerCommand(command: theia.CommandDescription, handler?: Handler, thisArg?: any): Disposable { if (this.commands.has(command.id)) { throw new Error(`Command ${command.id} already exist`); } @@ -103,14 +85,16 @@ export class CommandRegistryImpl implements CommandRegistryExt { } } - // tslint:disable-next-line:no-any + // tslint:disable:no-any executeCommand(id: string, ...args: any[]): PromiseLike { if (this.handlers.has(id)) { return this.executeLocalCommand(id, ...args); } else { - return this.proxy.$executeCommand(id, ...args); + return KnownCommands.map(id, args, (mappedId: string, mappedArgs: any[] | undefined) => + this.proxy.$executeCommand(mappedId, ...mappedArgs)); } } + // tslint:enable:no-any getKeyBinding(commandId: string): PromiseLike { return this.proxy.$getKeyBinding(commandId); @@ -120,24 +104,12 @@ export class CommandRegistryImpl implements CommandRegistryExt { private executeLocalCommand(id: string, ...args: any[]): PromiseLike { const handler = this.handlers.get(id); if (handler) { - const result = id === this.delegatingCommandId ? - handler(this, ...args) - : handler.apply(undefined, args); - return Promise.resolve(result); + return Promise.resolve(handler(...args)); } else { return Promise.reject(new Error(`Command ${id} doesn't exist`)); } } - // tslint:disable-next-line:no-any - executeConvertedCommand(commands: CommandRegistryImpl, ...args: any[]): PromiseLike { - const actualCmd = commands.cache.get(args[0]); - if (!actualCmd) { - return Promise.resolve(undefined); - } - return commands.executeCommand(actualCmd.command ? actualCmd.command : actualCmd.id, ...(actualCmd.arguments || [])); - } - async getCommands(filterUnderscoreCommands: boolean = false): Promise { const result = await this.proxy.$getCommands(); if (filterUnderscoreCommands) { @@ -146,78 +118,3 @@ export class CommandRegistryImpl implements CommandRegistryExt { return result; } } - -/** Converter between internal and api commands. */ -export class CommandsConverter { - private readonly delegatingCommandId: string; - private cacheId = 0; - private cache: Map; - - constructor(id: string, cache: Map) { - this.cache = cache; - this.delegatingCommandId = id; - } - - toInternal(command: theia.Command | undefined): Command | undefined { - if (!command) { - return undefined; - } - - let title; - if (command.title) { - title = command.title; - } else if (command.label) { - title = command.label; - } else { - return undefined; - } - - const result: Command = { - id: command.command ? command.command : command.id, - title: title - }; - - if (command.command && !CommandsConverter.isFalsyOrEmpty(command.arguments)) { - const id = this.cacheId++; - ObjectIdentifier.mixin(result, id); - this.cache.set(id, command); - - result.id = this.delegatingCommandId; - result.arguments = [id]; - } - - if (command.tooltip) { - result.tooltip = command.tooltip; - } - - return result; - } - - fromInternal(command: Command | undefined): theia.Command | undefined { - if (!command) { - return undefined; - } - - const id = ObjectIdentifier.of(command); - if (typeof id === 'number') { - return this.cache.get(id); - } else { - return { - id: command.id, - label: command.title, - command: command.id, - title: command.title, - arguments: command.arguments - }; - } - } - - /** - * @returns `false` if the provided object is an array and not empty. - */ - // tslint:disable-next-line:no-any - private static isFalsyOrEmpty(obj: any): boolean { - // tslint:disable-next-line:no-any - return !Array.isArray(obj) || (>obj).length === 0; - } -} diff --git a/packages/plugin-ext/src/plugin/languages/code-action.ts b/packages/plugin-ext/src/plugin/languages/code-action.ts index a9e3a56a5279a..32236a214d0cb 100644 --- a/packages/plugin-ext/src/plugin/languages/code-action.ts +++ b/packages/plugin-ext/src/plugin/languages/code-action.ts @@ -67,7 +67,7 @@ export class CodeActionAdapter { } if (CodeActionAdapter._isCommand(candidate)) { result.push({ - title: candidate.label || '', + title: candidate.title || '', command: Converter.toInternalCommand(candidate) }); } else { @@ -98,7 +98,7 @@ export class CodeActionAdapter { // tslint:disable-next-line:no-any private static _isCommand(smth: any): smth is theia.Command { - return typeof (smth).id === 'string'; + return typeof (smth).command === 'string' || typeof (smth).id === 'string'; } // tslint:disable-next-line:no-any diff --git a/packages/plugin-ext/src/plugin/languages/lens.ts b/packages/plugin-ext/src/plugin/languages/lens.ts index 2bbe07742acb2..1c4d024c93079 100644 --- a/packages/plugin-ext/src/plugin/languages/lens.ts +++ b/packages/plugin-ext/src/plugin/languages/lens.ts @@ -25,7 +25,7 @@ import { createToken } from '../token-provider'; /** Adapts the calls from main to extension thread for providing/resolving the code lenses. */ export class CodeLensAdapter { - private static readonly BAD_CMD: theia.Command = { id: 'missing', label: '<>' }; + private static readonly BAD_CMD: theia.Command = { command: 'missing', title: '<>' }; private cacheId = 0; private cache = new Map(); @@ -47,7 +47,6 @@ export class CodeLensAdapter { if (Array.isArray(lenses)) { return lenses.map(lens => { const id = this.cacheId++; - console.log(lens); const lensSymbol = ObjectIdentifier.mixin({ range: Converter.fromRange(lens.range)!, command: lens.command ? Converter.toInternalCommand(lens.command) : undefined diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index e1f57afb682a0..931a3eb129385 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -154,7 +154,7 @@ export function createAPIFactory( return function (plugin: InternalPlugin): typeof theia { const commands: typeof theia.commands = { // tslint:disable-next-line:no-any - registerCommand(command: theia.Command, handler?: (...args: any[]) => T | Thenable, thisArg?: any): Disposable { + registerCommand(command: theia.CommandDescription, handler?: (...args: any[]) => T | Thenable, thisArg?: any): Disposable { return commandRegistry.registerCommand(command, handler, thisArg); }, // tslint:disable-next-line:no-any diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts index 96aca5b9b4c00..fb539aa0314f8 100644 --- a/packages/plugin-ext/src/plugin/tree/tree-views.ts +++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts @@ -263,7 +263,7 @@ class TreeViewExtImpl extends Disposable { const treeItem = await this.treeDataProvider.getTreeItem(cachedElement); if (treeItem.command) { - this.commandRegistry.executeCommand(treeItem.command.id, ...(treeItem.command.arguments || [])); + this.commandRegistry.executeCommand((treeItem.command.command || treeItem.command.id)!, ...(treeItem.command.arguments || [])); } } } diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 28db9e60551a4..5f2ce87d49b9b 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -24,7 +24,7 @@ import URI from 'vscode-uri'; const SIDE_GROUP = -2; const ACTIVE_GROUP = -1; -import { SymbolInformation, Range as R, Position as P, SymbolKind as S } from 'vscode-languageserver-types'; +import { SymbolInformation, Range as R, Position as P, SymbolKind as S, Location as L } from 'vscode-languageserver-types'; export function toViewColumn(ep?: EditorPosition): theia.ViewColumn | undefined { if (typeof ep !== 'number') { @@ -401,12 +401,70 @@ export function fromDocumentHighlight(documentHighlight: theia.DocumentHighlight }; } -export function toInternalCommand(command: theia.Command): model.Command { - return { - id: command.command ? command.command : command.id, - title: command.title ? command.title : command.label || ' ', - tooltip: command.tooltip, - arguments: command.arguments +export function 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 + // tslint:disable-next-line: no-any + return KnownCommands.map((external.command || external.id)!, external.arguments, (mappedId: string, mappedArgs: any[]) => + ({ + id: mappedId, + title: external.title || external.label || ' ', + tooltip: external.tooltip, + arguments: mappedArgs + })); +} + +export namespace KnownCommands { + // tslint:disable: no-any + const mappings: { [id: string]: [string, (args: any[] | undefined) => any[] | undefined] } = {}; + mappings['editor.action.showReferences'] = ['textEditor.commands.showReferences', createConversionFunction( + (uri: URI) => uri.toString(), + fromPositionToP, + toArrayConversion(fromLocationToL))]; + + export function map(id: string, args: any[] | undefined, toDo: (mappedId: string, mappedArgs: any[] | undefined) => T): T { + if (mappings[id]) { + return toDo(mappings[id][0], mappings[id][1](args)); + } else { + return toDo(id, args); + } + } + + type conversionFunction = ((parameter: any) => any) | undefined; + function createConversionFunction(...conversions: conversionFunction[]): (args: any[] | undefined) => any[] | undefined { + return function (args: any[] | undefined): any[] | undefined { + if (!args) { + return args; + } + return args.map(function (arg: any, index: number): any { + if (index < conversions.length) { + const conversion = conversions[index]; + if (conversion) { + return conversion(arg); + } + } + return arg; + }); + }; + } + // tslint:enable: no-any + function fromPositionToP(p: theia.Position): P { + return P.create(p.line, p.character); + } + + function fromRangeToR(r: theia.Range): R { + return R.create(fromPositionToP(r.start), fromPositionToP(r.end)); + } + + function fromLocationToL(l: theia.Location): L { + return L.create(l.uri.toString(), fromRangeToR(l.range)); + } + +} + +function toArrayConversion(f: (a: T) => U): (a: T[]) => U[] { + return function (a: T[]) { + return a.map(f); }; } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 2f74df8512b33..4d9b8378c21a5 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -157,12 +157,13 @@ declare module '@theia/plugin' { export let all: Plugin[]; } + /** - * A command is a unique identifier of a function - * which can be executed by a user via a keyboard shortcut, - * a menu action or directly. - */ - export interface Command { + * A command is a unique identifier of a function + * which can be executed by a user via a keyboard shortcut, + * a menu action or directly. + */ + export interface CommandDescription { /** * A unique identifier of this command. */ @@ -172,29 +173,44 @@ declare module '@theia/plugin' { */ label?: string; /** - * A tooltip for for command, when represented in the UI. - */ + * A tooltip for for command, when represented in the UI. + */ tooltip?: string; /** * An icon class of this command. */ iconClass?: string; + } + /** + * Command represents a particular invocation of a registered command. + */ + export interface Command { + /** + * The identifier of the actual command handler. + */ + command?: string; + /** + * Title of the command invocation, like "Add local varible 'foo'". + */ + title?: string; + /** + * A tooltip for for command, when represented in the UI. + */ + tooltip?: string; /** * Arguments that the command handler should be * invoked with. */ arguments?: any[]; - // Title and command fields are needed to make Command object similar to Command from vscode API - /** - * Title of the command, like "save". + * @deprecated use command instead */ - title?: string; + id?: string; /** - * The identifier of the actual command handler. + * @deprecated use title instead */ - command?: string; + label?: string; } /** @@ -1996,7 +2012,7 @@ declare module '@theia/plugin' { * * Throw if a command is already registered for the given command identifier. */ - export function registerCommand(command: Command, handler?: (...args: any[]) => any, thisArg?: any): Disposable; + export function registerCommand(command: CommandDescription, handler?: (...args: any[]) => any, thisArg?: any): Disposable; /** * Register the given handler for the given command identifier.