Skip to content

Commit c3ea868

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 f718cb3 commit c3ea868

File tree

5 files changed

+72
-14
lines changed

5 files changed

+72
-14
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ 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,

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

Lines changed: 58 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,51 @@ export class CommandRegistryImpl implements CommandRegistryExt {
123130
this.argumentProcessors.push(processor);
124131
}
125132
}
133+
134+
export class CommandsConverter {
135+
136+
private readonly safeCommandId: string;
137+
private readonly commands: CommandRegistryImpl;
138+
private readonly commandsMap = new Map<number, theia.Command>();
139+
private handle = 0;
140+
private isCommandRegistered: boolean;
141+
142+
constructor(commands: CommandRegistryImpl) {
143+
this.safeCommandId = `theia_safe_cmd_${Date.now().toString()}`;
144+
this.commands = commands;
145+
this.isCommandRegistered = false;
146+
}
147+
148+
/**
149+
* Convert to a command that can be safely passed over JSON-RPC.
150+
*/
151+
toSafeCommand(command: theia.Command, disposables: DisposableCollection): theia.Command {
152+
if (!this.isCommandRegistered) {
153+
this.commands.registerCommand({ id: this.safeCommandId }, this.executeSafeCommand, this);
154+
this.isCommandRegistered = true;
155+
}
156+
157+
const result: theia.Command = {};
158+
Object.assign(result, command);
159+
160+
if (command.command && command.arguments && command.arguments.length > 0) {
161+
const id = this.handle++;
162+
this.commandsMap.set(id, command);
163+
disposables.push(new Disposable(() => this.commandsMap.delete(id)));
164+
result.command = this.safeCommandId;
165+
result.arguments = [id];
166+
}
167+
168+
return result;
169+
}
170+
171+
// tslint:disable-next-line:no-any
172+
private executeSafeCommand<R>(...args: any[]): PromiseLike<R | undefined> {
173+
const command = this.commandsMap.get(args[0]);
174+
if (!command || !command.command) {
175+
return Promise.reject('command NOT FOUND');
176+
}
177+
return this.commands.executeCommand(command.command, ...(command.arguments || []));
178+
}
179+
180+
}

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

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