diff --git a/src/vs/workbench/api/browser/mainThreadCommands.ts b/src/vs/workbench/api/browser/mainThreadCommands.ts index 2f4d043f048..f6a63722a34 100644 --- a/src/vs/workbench/api/browser/mainThreadCommands.ts +++ b/src/vs/workbench/api/browser/mainThreadCommands.ts @@ -8,6 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadCommandsShape, ExtHostCommandsShape, MainContext, IExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { revive } from 'vs/base/common/marshalling'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @extHostNamedCustomer(MainContext.MainThreadCommands) export class MainThreadCommands implements MainThreadCommandsShape { @@ -19,6 +20,7 @@ export class MainThreadCommands implements MainThreadCommandsShape { constructor( extHostContext: IExtHostContext, @ICommandService private readonly _commandService: ICommandService, + @IExtensionService private readonly _extensionService: IExtensionService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCommands); @@ -70,10 +72,14 @@ export class MainThreadCommands implements MainThreadCommandsShape { } } - $executeCommand(id: string, args: any[]): Promise { + async $executeCommand(id: string, args: any[], retry: boolean): Promise { for (let i = 0; i < args.length; i++) { args[i] = revive(args[i], 0); } + if (retry && args.length > 0 && !CommandsRegistry.getCommand(id)) { + await this._extensionService.activateByEvent(`onCommand:${id}`); + throw new Error('$executeCommand:retry'); + } return this._commandService.executeCommand(id, ...args); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f41147802a7..2067cef3de7 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -115,7 +115,7 @@ export interface MainThreadClipboardShape extends IDisposable { export interface MainThreadCommandsShape extends IDisposable { $registerCommand(id: string): void; $unregisterCommand(id: string): void; - $executeCommand(id: string, args: any[]): Promise; + $executeCommand(id: string, args: any[], retry: boolean): Promise; $getCommands(): Promise; } diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 819119a0f72..6df4dc8a417 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -112,6 +112,10 @@ export class ExtHostCommands implements ExtHostCommandsShape { executeCommand(id: string, ...args: any[]): Promise { this._logService.trace('ExtHostCommands#executeCommand', id); + return this._doExecuteCommand(id, args, true); + } + + private async _doExecuteCommand(id: string, args: any[], retry: boolean): Promise { if (this._commands.has(id)) { // we stay inside the extension host and support @@ -120,8 +124,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { } else { // automagically convert some argument types - - args = cloneAndChange(args, function (value) { + const toArgs = cloneAndChange(args, function (value) { if (value instanceof extHostTypes.Position) { return extHostTypeConverter.Position.from(value); } @@ -136,7 +139,19 @@ export class ExtHostCommands implements ExtHostCommandsShape { } }); - return this._proxy.$executeCommand(id, args).then(result => revive(result, 0)); + try { + const result = await this._proxy.$executeCommand(id, toArgs, retry); + return revive(result, 0); + } catch (e) { + // Rerun the command when it wasn't known, had arguments, and when retry + // is enabled. We do this because the command might be registered inside + // the extension host now and can therfore accept the arguments as-is. + if (e instanceof Error && e.message === '$executeCommand:retry') { + return this._doExecuteCommand(id, args, false); + } else { + throw e; + } + } } } diff --git a/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts index cbf97988142..94dee4ef4d3 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts @@ -59,4 +59,35 @@ suite('ExtHostCommands', function () { reg.dispose(); assert.equal(unregisterCounter, 1); }); + + test('execute with retry', async function () { + + let count = 0; + + const shape = new class extends mock() { + $registerCommand(id: string): void { + // + } + async $executeCommand(id: string, args: any[], retry: boolean): Promise { + count++; + assert.equal(retry, count === 1); + if (count === 1) { + assert.equal(retry, true); + throw new Error('$executeCommand:retry'); + } else { + assert.equal(retry, false); + return 17; + } + } + }; + + const commands = new ExtHostCommands( + SingleProxyRPCProtocol(shape), + new NullLogService() + ); + + const result = await commands.executeCommand('fooo', [this, true]); + assert.equal(result, 17); + assert.equal(count, 2); + }); }); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadCommands.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadCommands.test.ts index 5f820b2087a..8e1f8ac0fcb 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadCommands.test.ts @@ -5,14 +5,16 @@ import * as assert from 'assert'; import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { mock } from 'vs/workbench/test/electron-browser/api/mock'; suite('MainThreadCommands', function () { test('dispose on unregister', function () { - const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!); + const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!, new class extends mock() { }); assert.equal(CommandsRegistry.getCommand('foo'), undefined); // register @@ -26,7 +28,7 @@ suite('MainThreadCommands', function () { test('unregister all on dispose', function () { - const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!); + const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!, new class extends mock() { }); assert.equal(CommandsRegistry.getCommand('foo'), undefined); commands.$registerCommand('foo'); @@ -40,4 +42,46 @@ suite('MainThreadCommands', function () { assert.equal(CommandsRegistry.getCommand('foo'), undefined); assert.equal(CommandsRegistry.getCommand('bar'), undefined); }); + + test('activate and throw when needed', async function () { + + const activations: string[] = []; + const runs: string[] = []; + + const commands = new MainThreadCommands( + SingleProxyRPCProtocol(null), + new class extends mock() { + executeCommand(id: string): Promise { + runs.push(id); + return Promise.resolve(undefined); + } + }, + new class extends mock() { + activateByEvent(id: string) { + activations.push(id); + return Promise.resolve(); + } + } + ); + + // case 1: arguments and retry + try { + activations.length = 0; + await commands.$executeCommand('bazz', [1, 2, { n: 3 }], true); + assert.ok(false); + } catch (e) { + assert.deepEqual(activations, ['onCommand:bazz']); + assert.equal((e).message, '$executeCommand:retry'); + } + + // case 2: no arguments and retry + runs.length = 0; + await commands.$executeCommand('bazz', [], true); + assert.deepEqual(runs, ['bazz']); + + // case 3: arguments and no retry + runs.length = 0; + await commands.$executeCommand('bazz', [1, 2, true], false); + assert.deepEqual(runs, ['bazz']); + }); });