Skip to content

feat: conditionallyExecuteCommand API proposal #249742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/vscode-api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"authSession",
"chatParticipantPrivate",
"chatProvider",
"conditionallyExecuteCommand",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter is throwing an error here, but it was not obvious how I should fix it

"contribStatusBarItems",
"contribViewsRemote",
"customEditorMove",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,63 @@ suite('vscode API - commands', () => {

return closeAllEditors();
});

test('conditional: no condition', async function () {
let args: IArguments;
const registration = commands.registerCommand('t1', function () {
args = arguments;
});

const result = await commands.conditionallyExecuteCommand('', 't1', 'start');
assert.ok(result.executed);
registration.dispose();
assert.ok(args!);
assert.strictEqual(args!.length, 1);
assert.strictEqual(args![0], 'start');
});

test('conditional: true condition', async function () {
let args: IArguments;
const registration = commands.registerCommand('t1', function () {
args = arguments;
});

const result = await commands.conditionallyExecuteCommand('true', 't1', 'start');
assert.ok(result.executed);
registration.dispose();
assert.ok(args!);
assert.strictEqual(args!.length, 1);
assert.strictEqual(args![0], 'start');
});

test('conditional: false condition', async function () {
let args: IArguments;
const registration = commands.registerCommand('t1', function () {
args = arguments;
});

const result = await commands.conditionallyExecuteCommand('false', 't1', 'start');
assert.strictEqual(result.executed, false);
registration.dispose();
assert.strictEqual(args!, undefined);
});

test('conditional: sidebarVisible', async function () {
const registration = commands.registerCommand('t1', function () {
return true;
});

let result = await commands.conditionallyExecuteCommand('sideBarVisible', 't1');
assert.ok(result.executed);
assert.ok(result.result);

commands.executeCommand('workbench.action.closeSidebar');

result = await commands.conditionallyExecuteCommand('sideBarVisible', 't1');
assert.strictEqual(result.executed, false);
assert.strictEqual(result.result, undefined);

registration.dispose();

});
});
3 changes: 3 additions & 0 deletions src/vs/platform/extensions/common/extensionsApiProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const _allApiProposals = {
commentsDraftState: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts',
},
conditionallyExecuteCommand: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.conditionallyExecuteCommand.d.ts',
},
contribAccessibilityHelpContent: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribAccessibilityHelpContent.d.ts',
},
Expand Down
25 changes: 25 additions & 0 deletions src/vs/workbench/api/browser/mainThreadCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { IExtensionService } from '../../services/extensions/common/extensions.j
import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js';
import { ExtHostCommandsShape, ExtHostContext, MainContext, MainThreadCommandsShape } from '../common/extHost.protocol.js';
import { isString } from '../../../base/common/types.js';
import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
import { getActiveElement } from '../../../base/browser/dom.js';


@extHostNamedCustomer(MainContext.MainThreadCommands)
Expand All @@ -24,6 +26,7 @@ export class MainThreadCommands implements MainThreadCommandsShape {
extHostContext: IExtHostContext,
@ICommandService private readonly _commandService: ICommandService,
@IExtensionService private readonly _extensionService: IExtensionService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCommands);

Expand Down Expand Up @@ -92,6 +95,28 @@ export class MainThreadCommands implements MainThreadCommandsShape {
return this._commandService.executeCommand<T>(id, ...args);
}

async $checkCondition(when: string): Promise<boolean> {
if (!when) { return true; }

const context = this._contextKeyService.getContext(getActiveElement());
const whenExpr = ContextKeyExpr.deserialize(when);

if (whenExpr) {
return whenExpr.evaluate(context);
}
return true;
}

async $conditionallyExecuteCommand<T>(when: string, id: string, args: any[] | SerializableObjectWithBuffers<any[]>): Promise<{ executed: boolean; result: T | undefined }> {
if (await this.$checkCondition(when)) {
return { executed: true, result: await this.$executeCommand<T>(id, args, false) };

}

return { executed: false, result: undefined };

}

$getCommands(): Promise<string[]> {
return Promise.resolve([...CommandsRegistry.getCommands().keys()]);
}
Expand Down
6 changes: 5 additions & 1 deletion src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
},
getCommands(filterInternal: boolean = false): Thenable<string[]> {
return extHostCommands.getCommands(filterInternal);
}
},
conditionallyExecuteCommand<T>(when: string, id: string, ...args: any[]): Thenable<{ executed: boolean; result: T | undefined }> {
checkProposedApiEnabled(extension, 'conditionallyExecuteCommand');
return extHostCommands.conditionallyExecuteCommand<T>(when, id, ...args);
},
};

// namespace: env
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export interface MainThreadCommandsShape extends IDisposable {
$unregisterCommand(id: string): void;
$fireCommandActivationEvent(id: string): void;
$executeCommand(id: string, args: any[] | SerializableObjectWithBuffers<any[]>, retry: boolean): Promise<unknown | undefined>;
$conditionallyExecuteCommand(when: string, id: string, args: any[] | SerializableObjectWithBuffers<any[]>, retry: boolean): Promise<unknown | undefined>;
$checkCondition(when: string): Promise<boolean>;
$getCommands(): Promise<string[]>;
}

Expand Down
10 changes: 9 additions & 1 deletion src/vs/workbench/api/common/extHostCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,15 @@ export class ExtHostCommands implements ExtHostCommandsShape {
return this._doExecuteCommand(id, args, true);
}

private async _doExecuteCommand<T>(id: string, args: any[], retry: boolean): Promise<T> {
async conditionallyExecuteCommand<T>(when: string, id: string, ...args: any[]): Promise<{ executed: boolean; result: T | undefined }> {
this._logService.trace('ExtHostCommands#conditionallyExecuteCommand', id);
if (!when || await this.#proxy.$checkCondition(when)) {
return { executed: true, result: await this._doExecuteCommand(id, args, true, when) };
}
return { executed: false, result: undefined };
}

private async _doExecuteCommand<T>(id: string, args: any[], retry: boolean, when?: string): Promise<T> {

if (this._commands.has(id)) {
// - We stay inside the extension host and support
Expand Down
8 changes: 5 additions & 3 deletions src/vs/workbench/api/test/browser/mainThreadCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { mock } from '../../../../base/test/common/mock.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';

suite('MainThreadCommands', function () {

ensureNoDisposablesAreLeakedInTestSuite();

test('dispose on unregister', function () {

const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!, new class extends mock<IExtensionService>() { });
const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!, new class extends mock<IExtensionService>() { }, new class extends mock<IContextKeyService>() { });
assert.strictEqual(CommandsRegistry.getCommand('foo'), undefined);

// register
Expand All @@ -34,7 +35,7 @@ suite('MainThreadCommands', function () {

test('unregister all on dispose', function () {

const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!, new class extends mock<IExtensionService>() { });
const commands = new MainThreadCommands(SingleProxyRPCProtocol(null), undefined!, new class extends mock<IExtensionService>() { }, new class extends mock<IContextKeyService>() { });
assert.strictEqual(CommandsRegistry.getCommand('foo'), undefined);

commands.$registerCommand('foo');
Expand Down Expand Up @@ -67,7 +68,8 @@ suite('MainThreadCommands', function () {
activations.push(id);
return Promise.resolve();
}
}
},
new class extends mock<IContextKeyService>() { }
);

// case 1: arguments and retry
Expand Down
14 changes: 14 additions & 0 deletions src/vscode-dts/vscode.proposed.conditionallyExecuteCommand.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

declare module 'vscode' {
export namespace commands {

/**
* TODO
*/
export function conditionallyExecuteCommand<T>(when: string, id: string, ...args: string[]): Thenable<{ executed: boolean; result: T | undefined }>;
}
}