Skip to content

Commit 24a7eea

Browse files
committed
[plugin] Cache command arguments to safely pass the command over JSON-RPC
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
1 parent c1d367f commit 24a7eea

File tree

5 files changed

+81
-16
lines changed

5 files changed

+81
-16
lines changed

packages/plugin-ext/src/common/plugin-api-rpc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export interface CommandRegistryMain {
184184
}
185185

186186
export interface CommandRegistryExt {
187-
$executeCommand<T>(id: string, ...ars: any[]): PromiseLike<T>;
187+
$executeCommand<T>(id: string, ...ars: any[]): PromiseLike<T | undefined>;
188188
registerArgumentProcessor(processor: ArgumentProcessor): void;
189189
}
190190

packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class PluginTree extends TreeImpl {
127127
contextValue: item.contextValue
128128
};
129129
const node = this.getNode(item.id);
130-
if (item.collapsibleState !== TreeViewItemCollapsibleState.None) {
130+
if (item.collapsibleState !== undefined && item.collapsibleState !== TreeViewItemCollapsibleState.None) {
131131
if (CompositeTreeViewNode.is(node)) {
132132
return Object.assign(node, update);
133133
}
@@ -141,13 +141,14 @@ export class PluginTree extends TreeImpl {
141141
}, update);
142142
}
143143
if (TreeViewNode.is(node)) {
144-
return Object.assign(node, update);
144+
return Object.assign(node, update, { command: item.command });
145145
}
146146
return Object.assign({
147147
id: item.id,
148148
parent,
149149
visible: true,
150-
selected: false
150+
selected: false,
151+
command: item.command
151152
}, update);
152153
}
153154

packages/plugin-ext/src/plugin/command-registry.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import { CommandRegistryExt, PLUGIN_RPC_CONTEXT as Ext, CommandRegistryMain } fr
1919
import { RPCProtocol } from '../common/rpc-protocol';
2020
import { Disposable } from './types-impl';
2121
import { KnownCommands } from './type-converters';
22+
import { DisposableCollection } from '@theia/core';
2223

2324
// tslint:disable-next-line:no-any
24-
export type Handler = <T>(...args: any[]) => T | PromiseLike<T>;
25+
export type Handler = <T>(...args: any[]) => T | PromiseLike<T | undefined>;
2526

2627
export interface ArgumentProcessor {
2728
// tslint:disable-next-line:no-any
@@ -34,10 +35,16 @@ export class CommandRegistryImpl implements CommandRegistryExt {
3435
private readonly commands = new Set<string>();
3536
private readonly handlers = new Map<string, Handler>();
3637
private readonly argumentProcessors: ArgumentProcessor[];
38+
private readonly commandsConverter: CommandsConverter;
3739

3840
constructor(rpc: RPCProtocol) {
3941
this.proxy = rpc.getProxy(Ext.COMMAND_REGISTRY_MAIN);
4042
this.argumentProcessors = [];
43+
this.commandsConverter = new CommandsConverter(this);
44+
}
45+
46+
get converter(): CommandsConverter {
47+
return this.commandsConverter;
4148
}
4249

4350
// tslint:disable-next-line:no-any
@@ -78,7 +85,7 @@ export class CommandRegistryImpl implements CommandRegistryExt {
7885
}
7986

8087
// tslint:disable-next-line:no-any
81-
$executeCommand<T>(id: string, ...args: any[]): PromiseLike<T> {
88+
$executeCommand<T>(id: string, ...args: any[]): PromiseLike<T | undefined> {
8289
if (this.handlers.has(id)) {
8390
return this.executeLocalCommand(id, ...args);
8491
} else {
@@ -102,7 +109,7 @@ export class CommandRegistryImpl implements CommandRegistryExt {
102109
}
103110

104111
// tslint:disable-next-line:no-any
105-
private async executeLocalCommand<T>(id: string, ...args: any[]): Promise<T> {
112+
private async executeLocalCommand<T>(id: string, ...args: any[]): Promise<T | undefined> {
106113
const handler = this.handlers.get(id);
107114
if (handler) {
108115
return handler<T>(...args.map(arg => this.argumentProcessors.reduce((r, p) => p.processArgument(r), arg)));
@@ -123,3 +130,57 @@ export class CommandRegistryImpl implements CommandRegistryExt {
123130
this.argumentProcessors.push(processor);
124131
}
125132
}
133+
134+
/*---------------------------------------------------------------------------------------------
135+
* Copyright (c) Microsoft Corporation. All rights reserved.
136+
* Licensed under the MIT License. See License.txt in the project root for license information.
137+
*--------------------------------------------------------------------------------------------*/
138+
139+
// copied and modified from https://github.com/microsoft/vscode/blob/1.37.1/src/vs/workbench/api/common/extHostCommands.ts#L217-L259
140+
export class CommandsConverter {
141+
142+
private readonly safeCommandId: string;
143+
private readonly commands: CommandRegistryImpl;
144+
private readonly commandsMap = new Map<number, theia.Command>();
145+
private handle = 0;
146+
private isSafeCommandRegistered: boolean;
147+
148+
constructor(commands: CommandRegistryImpl) {
149+
this.safeCommandId = `theia_safe_cmd_${Date.now().toString()}`;
150+
this.commands = commands;
151+
this.isSafeCommandRegistered = false;
152+
}
153+
154+
/**
155+
* Convert to a command that can be safely passed over JSON-RPC.
156+
*/
157+
toSafeCommand(command: theia.Command, disposables: DisposableCollection): theia.Command {
158+
if (!this.isSafeCommandRegistered) {
159+
this.commands.registerCommand({ id: this.safeCommandId }, this.executeSafeCommand, this);
160+
this.isSafeCommandRegistered = true;
161+
}
162+
163+
const result: theia.Command = {};
164+
Object.assign(result, command);
165+
166+
if (command.command && command.arguments && command.arguments.length > 0) {
167+
const id = this.handle++;
168+
this.commandsMap.set(id, command);
169+
disposables.push(new Disposable(() => this.commandsMap.delete(id)));
170+
result.command = this.safeCommandId;
171+
result.arguments = [id];
172+
}
173+
174+
return result;
175+
}
176+
177+
// tslint:disable-next-line:no-any
178+
private executeSafeCommand<R>(...args: any[]): PromiseLike<R | undefined> {
179+
const command = this.commandsMap.get(args[0]);
180+
if (!command || !command.command) {
181+
return Promise.reject('command NOT FOUND');
182+
}
183+
return this.commands.executeCommand(command.command, ...(command.arguments || []));
184+
}
185+
186+
}

packages/plugin-ext/src/plugin/plugin-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export function createAPIFactory(
174174
return function (plugin: InternalPlugin): typeof theia {
175175
const commands: typeof theia.commands = {
176176
// tslint:disable-next-line:no-any
177-
registerCommand(command: theia.CommandDescription, handler?: <T>(...args: any[]) => T | Thenable<T>, thisArg?: any): Disposable {
177+
registerCommand(command: theia.CommandDescription, handler?: <T>(...args: any[]) => T | Thenable<T | undefined>, thisArg?: any): Disposable {
178178
return commandRegistry.registerCommand(command, handler, thisArg);
179179
},
180180
// tslint:disable-next-line:no-any

packages/plugin-ext/src/plugin/tree/tree-views.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@ import { Emitter } from '@theia/core/lib/common/event';
2323
import { Disposable, ThemeIcon } from '../types-impl';
2424
import { Plugin, PLUGIN_RPC_CONTEXT, TreeViewsExt, TreeViewsMain, TreeViewItem } from '../../common/plugin-api-rpc';
2525
import { RPCProtocol } from '../../common/rpc-protocol';
26-
import { CommandRegistryImpl } from '../command-registry';
26+
import { CommandRegistryImpl, CommandsConverter } from '../command-registry';
2727
import { TreeViewSelection } from '../../common';
2828
import { PluginPackage } from '../../common/plugin-protocol';
29+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
30+
import { toInternalCommand } from '../type-converters';
2931

3032
export class TreeViewsExtImpl implements TreeViewsExt {
3133

3234
private proxy: TreeViewsMain;
3335

3436
private treeViews: Map<string, TreeViewExtImpl<any>> = new Map<string, TreeViewExtImpl<any>>();
3537

36-
constructor(rpc: RPCProtocol, commandRegistry: CommandRegistryImpl) {
38+
constructor(rpc: RPCProtocol, readonly commandRegistry: CommandRegistryImpl) {
3739
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TREE_VIEWS_MAIN);
3840
commandRegistry.registerArgumentProcessor({
3941
processArgument: arg => {
@@ -61,7 +63,7 @@ export class TreeViewsExtImpl implements TreeViewsExt {
6163
throw new Error('Options with treeDataProvider is mandatory');
6264
}
6365

64-
const treeView = new TreeViewExtImpl(plugin, treeViewId, options.treeDataProvider, this.proxy);
66+
const treeView = new TreeViewExtImpl(plugin, treeViewId, options.treeDataProvider, this.proxy, this.commandRegistry.converter);
6567
this.treeViews.set(treeViewId, treeView);
6668

6769
return {
@@ -120,6 +122,8 @@ class TreeViewExtImpl<T> extends Disposable {
120122
private onDidCollapseElementEmitter: Emitter<TreeViewExpansionEvent<T>> = new Emitter<TreeViewExpansionEvent<T>>();
121123
public readonly onDidCollapseElement = this.onDidCollapseElementEmitter.event;
122124

125+
private disposables = new DisposableCollection();
126+
123127
private selection: T[] = [];
124128
get selectedElements(): T[] { return this.selection; }
125129

@@ -129,7 +133,8 @@ class TreeViewExtImpl<T> extends Disposable {
129133
private plugin: Plugin,
130134
private treeViewId: string,
131135
private treeDataProvider: TreeDataProvider<T>,
132-
private proxy: TreeViewsMain) {
136+
private proxy: TreeViewsMain,
137+
readonly commandsConverter: CommandsConverter) {
133138

134139
super(() => {
135140
proxy.$unregisterTreeDataProvider(treeViewId);
@@ -169,6 +174,7 @@ class TreeViewExtImpl<T> extends Disposable {
169174
console.error(`No tree item with id '${parentId}' found.`);
170175
return [];
171176
}
177+
this.disposables.dispose();
172178

173179
// ask data provider for children for cached element
174180
const result = await this.treeDataProvider.getChildren(parent);
@@ -244,9 +250,6 @@ class TreeViewExtImpl<T> extends Disposable {
244250
}
245251
}
246252

247-
if (treeItem.command) {
248-
treeItem.command.arguments = [id];
249-
}
250253
const treeViewItem = {
251254
id,
252255
label,
@@ -257,7 +260,7 @@ class TreeViewExtImpl<T> extends Disposable {
257260
tooltip: treeItem.tooltip,
258261
collapsibleState: treeItem.collapsibleState,
259262
contextValue: treeItem.contextValue,
260-
command: treeItem.command
263+
command: treeItem.command ? toInternalCommand(this.commandsConverter.toSafeCommand(treeItem.command, this.disposables)) : undefined
261264
} as TreeViewItem;
262265

263266
treeItems.push(treeViewItem);

0 commit comments

Comments
 (0)