From 230c2edb9c7b680fc86743e4adcf7f96d71e409e Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Sat, 24 Aug 2019 20:54:14 +0000 Subject: [PATCH 01/10] =?UTF-8?q?[vscode]=C2=A0stub=20an=20extension=20kin?= =?UTF-8?q?d=20as=20a=20local=20VS=20Code=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i.e. not running on a remote workspace Signed-off-by: Anton Kosyakov --- .../src/node/plugin-vscode-init.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts index 9b074d03eee07..58479f309c28f 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts @@ -31,8 +31,13 @@ let defaultApi: typeof theia; let isLoadOverride = false; let pluginApiFactory: PluginAPIFactory; +export enum ExtensionKind { + UI = 1, + Workspace = 2 +} + export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIFactory, plugin: Plugin) => { - const vscode = apiFactory(plugin); + const vscode = Object.assign(apiFactory(plugin), { ExtensionKind }); // replace command API as it will send only the ID as a string parameter const registerCommand = vscode.commands.registerCommand; @@ -78,10 +83,10 @@ export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIF // use Theia plugin api instead vscode extensions (vscode).extensions = { get all(): any[] { - return vscode.plugins.all.map(p => withExtensionPath(p)); + return vscode.plugins.all.map(p => asExtension(p)); }, getExtension(pluginId: string): any | undefined { - return withExtensionPath(vscode.plugins.getPlugin(pluginId)); + return asExtension(vscode.plugins.getPlugin(pluginId)); }, get onDidChange(): theia.Event { return vscode.plugins.onDidChange; @@ -133,10 +138,14 @@ function findPlugin(filePath: string): Plugin | undefined { return plugins.find(plugin => filePath.startsWith(plugin.pluginFolder)); } -function withExtensionPath(plugin: any | undefined): any | undefined { - if (plugin && plugin.pluginPath) { +function asExtension(plugin: any | undefined): any | undefined { + if (!plugin) { + return plugin; + } + if (plugin.pluginPath) { plugin.extensionPath = plugin.pluginPath; } - + // stub as a local VS Code extension (not running on a remote workspace) + plugin.extensionKind = ExtensionKind.UI; return plugin; } From 73cca79b408acfbc5f3b40aa9e4d05e01e28a283 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Sun, 25 Aug 2019 13:51:05 +0000 Subject: [PATCH 02/10] [debug] ignore additional breakpoints returned by `setBreakpoints` request otherwise it fails `undefined` and swallows updates for other breakpoints Signed-off-by: Anton Kosyakov --- packages/debug/src/browser/debug-session.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index c6c3a4336bbfe..af819ab88b0e4 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -542,7 +542,12 @@ export class DebugSession implements CompositeTreeElement { sourceModified, breakpoints: enabled.map(({ origin }) => origin.raw) }); - response.body.breakpoints.map((raw, index) => enabled[index].update({ raw })); + response.body.breakpoints.map((raw, index) => { + // node debug adapter returns more breakpoints sometimes + if (enabled[index]) { + enabled[index].update({ raw }); + } + }); } catch (error) { // could be error or promise rejection of DebugProtocol.SetBreakpointsResponse if (error instanceof Error) { From b71fad24c74261a94a293bdade95e957991fdaff Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Sun, 25 Aug 2019 14:27:36 +0000 Subject: [PATCH 03/10] =?UTF-8?q?[plugin]=C2=A0implement=20`Plugin.isActiv?= =?UTF-8?q?e`=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit before it was stub with `true` always Signed-off-by: Anton Kosyakov --- packages/plugin-ext/src/common/plugin-api-rpc.ts | 1 + packages/plugin-ext/src/plugin/plugin-context.ts | 6 ++++-- packages/plugin-ext/src/plugin/plugin-manager.ts | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 06b54b55a2e6d..10d3a405d7bc9 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -117,6 +117,7 @@ export interface PluginManager { getPluginById(pluginId: string): Plugin | undefined; getPluginExport(pluginId: string): PluginAPI | undefined; isRunning(pluginId: string): boolean; + isActive(pluginId: string): boolean; activatePlugin(pluginId: string): PromiseLike; onDidChange: theia.Event; } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 3bd27bb8ddca5..c5ef60a8e15d1 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -831,7 +831,6 @@ export function createAPIFactory( class Plugin implements theia.Plugin { id: string; pluginPath: string; - isActive: boolean; // tslint:disable-next-line:no-any packageJSON: any; pluginType: theia.PluginType; @@ -839,10 +838,13 @@ class Plugin implements theia.Plugin { this.id = plugin.model.id; this.pluginPath = plugin.pluginFolder; this.packageJSON = plugin.rawModel; - this.isActive = true; this.pluginType = plugin.model.entryPoint.frontend ? 'frontend' : 'backend'; } + get isActive(): boolean { + return this.pluginManager.isActive(this.id); + } + get exports(): T { return this.pluginManager.getPluginExport(this.id); } diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 1088e684edd97..0deca4cb93df2 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -286,6 +286,10 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { return this.registry.has(pluginId); } + isActive(pluginId: string): boolean { + return this.activatedPlugins.has(pluginId); + } + activatePlugin(pluginId: string): PromiseLike { if (this.pluginActivationPromises.has(pluginId)) { return this.pluginActivationPromises.get(pluginId)!.promise; From 99e1273447f0cfc75e31913ea03c55e92d699934 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Sun, 25 Aug 2019 15:53:01 +0000 Subject: [PATCH 04/10] [plugin] implement selection and visible tree view APIs Signed-off-by: Anton Kosyakov --- .../plugin-ext/src/common/plugin-api-rpc.ts | 2 + .../main/browser/view/tree-view-widget.tsx | 20 ++ .../src/main/browser/view/tree-views-main.ts | 6 + .../plugin-ext/src/plugin/tree/tree-views.ts | 183 +++++++++++++--- packages/plugin/src/theia.d.ts | 207 +++++++++++------- 5 files changed, 307 insertions(+), 111 deletions(-) diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 10d3a405d7bc9..5063bef9392f7 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -477,6 +477,8 @@ export interface TreeViewsMain { export interface TreeViewsExt { $getChildren(treeViewId: string, treeItemId: string | undefined): Promise; $setExpanded(treeViewId: string, treeItemId: string, expanded: boolean): Promise; + $setSelection(treeViewId: string, treeItemIds: string[]): Promise; + $setVisible(treeViewId: string, visible: boolean): Promise; } export class TreeViewItem { diff --git a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx index d818b0c4ed9fa..44608623961a1 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx +++ b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx @@ -37,7 +37,9 @@ import { MenuPath, MenuModelRegistry, ActionMenuNode } from '@theia/core/lib/com import * as React from 'react'; import { PluginSharedStyle } from '../plugin-shared-style'; import { ViewContextKeyService } from './view-context-key-service'; +import { Widget } from '@theia/core/lib/browser/widgets/widget'; import { CommandRegistry } from '@theia/core/lib/common/command'; +import { Emitter } from '@theia/core/lib/common/event'; import { MessageService } from '@theia/core/lib/common/message-service'; import { View } from '../../../common/plugin-protocol'; @@ -206,12 +208,16 @@ export class TreeViewWidget extends TreeWidget { @inject(PluginTreeModel) readonly model: PluginTreeModel; + protected readonly onDidChangeVisibilityEmitter = new Emitter(); + readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event; + @postConstruct() protected init(): void { super.init(); this.id = this.identifier.id; this.addClass('theia-tree-view'); this.node.style.height = '100%'; + this.toDispose.push(this.onDidChangeVisibilityEmitter); } protected renderIcon(node: TreeNode, props: NodeProps): React.ReactNode { @@ -318,4 +324,18 @@ export class TreeViewWidget extends TreeWidget { return [this.toTreeViewSelection(node)]; } + setFlag(flag: Widget.Flag): void { + super.setFlag(flag); + if (flag === Widget.Flag.IsVisible) { + this.onDidChangeVisibilityEmitter.fire(this.isVisible); + } + } + + clearFlag(flag: Widget.Flag): void { + super.clearFlag(flag); + if (flag === Widget.Flag.IsVisible) { + this.onDidChangeVisibilityEmitter.fire(this.isVisible); + } + } + } diff --git a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts index 6c2738078208f..f0398a1356e55 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts +++ b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts @@ -115,12 +115,18 @@ export class TreeViewsMainImpl implements TreeViewsMain { } this.contextKeys.view.set(treeViewId); + this.proxy.$setSelection(treeViewId, event.map((node: TreeViewNode) => node.id)); + // execute TreeItem.command if present const treeNode = event[0] as TreeViewNode; if (treeNode && treeNode.command) { this.commands.executeCommand(treeNode.command.id, ...(treeNode.command.arguments || [])); } }); + + const updateVisible = () => this.proxy.$setVisible(treeViewId, treeViewWidget.isVisible); + updateVisible(); + treeViewWidget.onDidChangeVisibility(() => updateVisible()); } } diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts index d900172e77c55..d3e50160bd88e 100644 --- a/packages/plugin-ext/src/plugin/tree/tree-views.ts +++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts @@ -18,15 +18,20 @@ import * as path from 'path'; import URI from 'vscode-uri'; -import { TreeDataProvider, TreeView, TreeViewExpansionEvent, TreeItem2, TreeItemLabel } from '@theia/plugin'; +import { + TreeDataProvider, TreeView, TreeViewExpansionEvent, TreeItem2, TreeItemLabel, + TreeViewSelectionChangeEvent, TreeViewVisibilityChangeEvent +} from '@theia/plugin'; +// TODO: extract `@theia/util` for event, disposable, cancellation and common types +// don't use @theia/core directly from plugin host import { Emitter } from '@theia/core/lib/common/event'; -import { Disposable, ThemeIcon } from '../types-impl'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Disposable as PluginDisposable, ThemeIcon } from '../types-impl'; import { Plugin, PLUGIN_RPC_CONTEXT, TreeViewsExt, TreeViewsMain, TreeViewItem } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { CommandRegistryImpl, CommandsConverter } from '../command-registry'; import { TreeViewSelection } from '../../common'; import { PluginPackage } from '../../common/plugin-protocol'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { toInternalCommand } from '../type-converters'; export class TreeViewsExtImpl implements TreeViewsExt { @@ -49,10 +54,10 @@ export class TreeViewsExtImpl implements TreeViewsExt { }); } - registerTreeDataProvider(plugin: Plugin, treeViewId: string, treeDataProvider: TreeDataProvider): Disposable { + registerTreeDataProvider(plugin: Plugin, treeViewId: string, treeDataProvider: TreeDataProvider): PluginDisposable { const treeView = this.createTreeView(plugin, treeViewId, { treeDataProvider }); - return Disposable.create(() => { + return PluginDisposable.create(() => { this.treeViews.delete(treeViewId); treeView.dispose(); }); @@ -79,7 +84,18 @@ export class TreeViewsExtImpl implements TreeViewsExt { get selection() { return treeView.selectedElements; }, - + // tslint:disable-next-line:typedef + get onDidChangeSelection() { + return treeView.onDidChangeSelection; + }, + // tslint:disable-next-line:typedef + get visible() { + return treeView.visible; + }, + // tslint:disable-next-line:typedef + get onDidChangeVisibility() { + return treeView.onDidChangeVisibility; + }, reveal: (element: T, selectionOptions: { select?: boolean }): Thenable => treeView.reveal(element, selectionOptions), @@ -112,22 +128,53 @@ export class TreeViewsExtImpl implements TreeViewsExt { } } + async $setSelection(treeViewId: string, treeItemIds: string[]): Promise { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + throw new Error('No tree view with id' + treeViewId); + } + treeView.setSelection(treeItemIds); + } + + async $setVisible(treeViewId: string, isVisible: boolean): Promise { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + throw new Error('No tree view with id' + treeViewId); + } + treeView.setVisible(isVisible); + } + +} + +interface TreeExtNode extends Disposable { + id: string + value?: T + children?: TreeExtNode[] } -class TreeViewExtImpl extends Disposable { +class TreeViewExtImpl implements Disposable { - private onDidExpandElementEmitter: Emitter> = new Emitter>(); - public readonly onDidExpandElement = this.onDidExpandElementEmitter.event; + private readonly onDidExpandElementEmitter = new Emitter>(); + readonly onDidExpandElement = this.onDidExpandElementEmitter.event; - private onDidCollapseElementEmitter: Emitter> = new Emitter>(); - public readonly onDidCollapseElement = this.onDidCollapseElementEmitter.event; + private readonly onDidCollapseElementEmitter = new Emitter>(); + readonly onDidCollapseElement = this.onDidCollapseElementEmitter.event; - private disposables = new DisposableCollection(); + private readonly onDidChangeSelectionEmitter = new Emitter>(); + readonly onDidChangeSelection = this.onDidChangeSelectionEmitter.event; - private selection: T[] = []; - get selectedElements(): T[] { return this.selection; } + private readonly onDidChangeVisibilityEmitter = new Emitter(); + readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event; - private cache: Map = new Map(); + private readonly nodes = new Map>(); + + private readonly toDispose = new DisposableCollection( + Disposable.create(() => this.clearAll()), + this.onDidExpandElementEmitter, + this.onDidCollapseElementEmitter, + this.onDidChangeSelectionEmitter, + this.onDidChangeVisibilityEmitter + ); constructor( private plugin: Plugin, @@ -136,11 +183,8 @@ class TreeViewExtImpl extends Disposable { private proxy: TreeViewsMain, readonly commandsConverter: CommandsConverter) { - super(() => { - proxy.$unregisterTreeDataProvider(treeViewId); - }); - proxy.$registerTreeDataProvider(treeViewId); + this.toDispose.push(Disposable.create(() => this.proxy.$unregisterTreeDataProvider(treeViewId))); if (treeDataProvider.onDidChangeTreeData) { treeDataProvider.onDidChangeTreeData((e: T) => { @@ -149,10 +193,14 @@ class TreeViewExtImpl extends Disposable { } } + dispose(): void { + this.toDispose.dispose(); + } + async reveal(element: T, selectionOptions?: { select?: boolean }): Promise { // find element id in a cache let elementId; - this.cache.forEach((el, id) => { + this.nodes.forEach((el, id) => { if (Object.is(el, element)) { elementId = id; } @@ -164,17 +212,18 @@ class TreeViewExtImpl extends Disposable { } getTreeItem(treeItemId: string): T | undefined { - return this.cache.get(treeItemId); + const element = this.nodes.get(treeItemId); + return element && element.value; } async getChildren(parentId: string): Promise { - // get element from a cache - const parent = this.getTreeItem(parentId); + const parentNode = this.nodes.get(parentId); + const parent = parentNode && parentNode.value; if (parentId && !parent) { console.error(`No tree item with id '${parentId}' found.`); return []; } - this.disposables.dispose(); + this.clearChildren(parentNode); // ask data provider for children for cached element const result = await this.treeDataProvider.getChildren(parent); @@ -216,8 +265,18 @@ class TreeViewExtImpl extends Disposable { label = treeItem.id; } - // Add element to the cache - this.cache.set(id, value); + const toDisposeElement = new DisposableCollection(); + const node: TreeExtNode = { + id, + value, + dispose: () => toDisposeElement.dispose() + }; + if (parentNode) { + const children = parentNode.children || []; + children.push(node); + parentNode.children = children; + } + this.nodes.set(id, node); let icon; let iconUrl; @@ -260,7 +319,7 @@ class TreeViewExtImpl extends Disposable { tooltip: treeItem.tooltip, collapsibleState: treeItem.collapsibleState, contextValue: treeItem.contextValue, - command: treeItem.command ? toInternalCommand(this.commandsConverter.toSafeCommand(treeItem.command, this.disposables)) : undefined + command: treeItem.command ? toInternalCommand(this.commandsConverter.toSafeCommand(treeItem.command, toDisposeElement)) : undefined } as TreeViewItem; treeItems.push(treeViewItem); @@ -273,6 +332,34 @@ class TreeViewExtImpl extends Disposable { } } + private clearChildren(parentNode?: TreeExtNode): void { + if (parentNode) { + if (parentNode.children) { + for (const child of parentNode.children) { + this.clear(child); + } + } + delete parentNode['children']; + } else { + this.clearAll(); + } + } + + private clear(node: TreeExtNode): void { + if (node.children) { + for (const child of node.children) { + this.clear(child); + } + } + this.nodes.delete(node.id); + node.dispose(); + } + + private clearAll(): void { + this.nodes.forEach(node => node.dispose()); + this.nodes.clear(); + } + async onExpanded(treeItemId: string): Promise { // get element from a cache const cachedElement = this.getTreeItem(treeItemId); @@ -297,4 +384,46 @@ class TreeViewExtImpl extends Disposable { } } + private selectedItemIds = new Set(); + get selectedElements(): T[] { + const items: T[] = []; + for (const id of this.selectedItemIds) { + const item = this.getTreeItem(id); + if (item) { + items.push(item); + } + } + return items; + } + + setSelection(selectedItemIds: string[]): void { + const toDelete = new Set(this.selectedItemIds); + for (const id of this.selectedItemIds) { + toDelete.delete(id); + if (!this.selectedItemIds.has(id)) { + this.doSetSelection(selectedItemIds); + return; + } + } + if (toDelete.size) { + this.doSetSelection(selectedItemIds); + } + } + protected doSetSelection(selectedItemIts: string[]): void { + this.selectedItemIds = new Set(selectedItemIts); + this.onDidChangeSelectionEmitter.fire(Object.freeze({ selection: this.selectedElements })); + } + + private _visible = false; + get visible(): boolean { + return this._visible; + } + + setVisible(visible: boolean): void { + if (visible !== this._visible) { + this._visible = visible; + this.onDidChangeVisibilityEmitter.fire(Object.freeze({ visible: this._visible })); + } + } + } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index d6037cfebaa5e..559efd0570986 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -3713,6 +3713,30 @@ declare module '@theia/plugin' { } + /** + * The event that is fired when there is a change in [tree view's selection](#TreeView.selection) + */ + export interface TreeViewSelectionChangeEvent { + + /** + * Selected elements. + */ + readonly selection: T[]; + + } + + /** + * The event that is fired when there is a change in [tree view's visibility](#TreeView.visible) + */ + export interface TreeViewVisibilityChangeEvent { + + /** + * `true` if the [tree view](#TreeView) is visible otherwise `false`. + */ + readonly visible: boolean; + + } + /** * Represents a Tree view */ @@ -3733,6 +3757,21 @@ declare module '@theia/plugin' { */ readonly selection: ReadonlyArray; + /** + * Event that is fired when the [selection](#TreeView.selection) has changed + */ + readonly onDidChangeSelection: Event>; + + /** + * `true` if the [tree view](#TreeView) is visible otherwise `false`. + */ + readonly visible: boolean; + + /** + * Event that is fired when [visibility](#TreeView.visible) has changed + */ + readonly onDidChangeVisibility: Event; + /** * Reveal an element. By default revealed element is selected. * @@ -4113,11 +4152,11 @@ declare module '@theia/plugin' { } /** - * A type that filesystem providers should use to signal errors. - * - * This class has factory methods for common error-cases, like `EntryNotFound` when - * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.EntryNotFound(someUri);` - */ + * A type that filesystem providers should use to signal errors. + * + * This class has factory methods for common error-cases, like `EntryNotFound` when + * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.EntryNotFound(someUri);` + */ export class FileSystemError extends Error { /** @@ -7374,9 +7413,9 @@ declare module '@theia/plugin' { resolveDebugConfiguration?(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult; } - /** - * A Debug Adapter Tracker is a means to track the communication between VS Code and a Debug Adapter. - */ + /** + * A Debug Adapter Tracker is a means to track the communication between VS Code and a Debug Adapter. + */ export interface DebugAdapterTracker { /** * A session with the debug adapter is about to be started. @@ -7448,9 +7487,9 @@ declare module '@theia/plugin' { readonly options?: DebugAdapterExecutableOptions; } - /** - * Options for a debug adapter executable. - */ + /** + * Options for a debug adapter executable. + */ export interface DebugAdapterExecutableOptions { /** @@ -8327,20 +8366,20 @@ declare module '@theia/plugin' { Expanded = 1 } - /** - * Comment mode of a [comment](#Comment) - */ - export enum CommentMode { - /** - * Displays the comment editor - */ - Editing = 0, + /** + * Comment mode of a [comment](#Comment) + */ + export enum CommentMode { + /** + * Displays the comment editor + */ + Editing = 0, - /** - * Displays the preview of the comment - */ - Preview = 1 - } + /** + * Displays the preview of the comment + */ + Preview = 1 + } /** * A collection of [comments](#Comment) representing a conversation at a particular range in a document. @@ -8531,81 +8570,81 @@ declare module '@theia/plugin' { dispose(): void; } - /** - * Author information of a [comment](#Comment) - */ - export interface CommentAuthorInformation { - /** - * The display name of the author of the comment - */ - name: string; - - /** - * The optional icon path for the author - */ - iconPath?: Uri; - } - - /** - * Reactions of a [comment](#Comment) - */ - export interface CommentReaction { - /** - * The human-readable label for the reaction - */ - readonly label: string; - - /** - * Icon for the reaction shown in UI. - */ - readonly iconPath: string | Uri; - - /** - * The number of users who have reacted to this reaction - */ - readonly count: number; - - /** - * Whether the [author](CommentAuthorInformation) of the comment has reacted to this reaction - */ - readonly authorHasReacted: boolean; - } + /** + * Author information of a [comment](#Comment) + */ + export interface CommentAuthorInformation { + /** + * The display name of the author of the comment + */ + name: string; + + /** + * The optional icon path for the author + */ + iconPath?: Uri; + } + + /** + * Reactions of a [comment](#Comment) + */ + export interface CommentReaction { + /** + * The human-readable label for the reaction + */ + readonly label: string; + + /** + * Icon for the reaction shown in UI. + */ + readonly iconPath: string | Uri; + + /** + * The number of users who have reacted to this reaction + */ + readonly count: number; + + /** + * Whether the [author](CommentAuthorInformation) of the comment has reacted to this reaction + */ + readonly authorHasReacted: boolean; + } /** * A comment is displayed within the editor or the Comments Panel, depending on how it is provided. */ export interface Comment { - /** - * The human-readable comment body - */ + /** + * The human-readable comment body + */ body: string | MarkdownString; - /** - * [Comment mode](#CommentMode) of the comment - */ + /** + * [Comment mode](#CommentMode) of the comment + */ mode: CommentMode; - /** - * The [author information](#CommentAuthorInformation) of the comment - */ + /** + * The [author information](#CommentAuthorInformation) of the comment + */ author: CommentAuthorInformation; - /** - * Context value of the comment. This can be used to contribute comment specific actions. - * For example, a comment is given a context value as `editable`. When contributing actions to `comments/comment/title` - * using `menus` extension point, you can specify context value for key `comment` in `when` expression like `comment == editable`. - */ + /** + * Context value of the comment. This can be used to contribute comment specific actions. + * For example, a comment is given a context value as `editable`. When contributing actions to `comments/comment/title` + * using `menus` extension point, you can specify context value for key `comment` in `when` expression like `comment == editable`. + */ contextValue?: string; - /** - * Optional reactions of the [comment](#Comment) - */ + /** + * Optional reactions of the [comment](#Comment) + */ reactions?: CommentReaction[]; - /** - * Optional label describing the [Comment](#Comment) - * Label will be rendered next to authorName if exists. - */ + /** + * Optional label describing the [Comment](#Comment) + * Label will be rendered next to authorName if exists. + */ label?: string; } From cfda1b4bba7f482b88081a9e4f050d3ac8ee9b0a Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 26 Aug 2019 08:20:36 +0000 Subject: [PATCH 05/10] fix #6039: implement vscode.env.openExternal API Signed-off-by: Anton Kosyakov --- .../plugin-ext/src/common/plugin-api-rpc.ts | 7 ++++- .../src/main/browser/main-context.ts | 4 ++- .../src/main/browser/window-state-main.ts | 26 ++++++++++++++++--- .../plugin-ext/src/plugin/plugin-context.ts | 6 +++-- .../plugin-ext/src/plugin/window-state.ts | 13 ++++++++-- packages/plugin/src/theia.d.ts | 12 +++++++++ 6 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 5063bef9392f7..b71e6f295676d 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -533,6 +533,10 @@ export enum TreeViewItemCollapsibleState { Expanded = 2 } +export interface WindowMain { + $openUri(uri: UriComponents): Promise; +} + export interface WindowStateExt { $onWindowStateChanged(focus: boolean): void; } @@ -1269,7 +1273,8 @@ export const PLUGIN_RPC_CONTEXT = { DEBUG_MAIN: createProxyIdentifier('DebugMain'), FILE_SYSTEM_MAIN: createProxyIdentifier('FileSystemMain'), SCM_MAIN: createProxyIdentifier('ScmMain'), - DECORATIONS_MAIN: createProxyIdentifier('DecorationsMain') + DECORATIONS_MAIN: createProxyIdentifier('DecorationsMain'), + WINDOW_MAIN: createProxyIdentifier('WindowMain') }; export const MAIN_RPC_CONTEXT = { diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index ad5746e27fd69..72e606cf3ca57 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -61,7 +61,6 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container rpc.set(PLUGIN_RPC_CONTEXT.PREFERENCE_REGISTRY_MAIN, preferenceRegistryMain); /* tslint:disable */ - new WindowStateMain(rpc); new EditorsAndDocumentsMain(rpc, container); /* tslint:enable */ @@ -111,4 +110,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const decorationsMain = new DecorationsMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.DECORATIONS_MAIN, decorationsMain); + + const windowMain = new WindowStateMain(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.WINDOW_MAIN, windowMain); } diff --git a/packages/plugin-ext/src/main/browser/window-state-main.ts b/packages/plugin-ext/src/main/browser/window-state-main.ts index 02701eef60614..36e7307f1f1aa 100644 --- a/packages/plugin-ext/src/main/browser/window-state-main.ts +++ b/packages/plugin-ext/src/main/browser/window-state-main.ts @@ -14,15 +14,22 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { WindowStateExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; +import URI from 'vscode-uri'; +import { interfaces } from 'inversify'; +import { WindowStateExt, MAIN_RPC_CONTEXT, WindowMain } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; +import { UriComponents } from '../../common/uri-components'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; -export class WindowStateMain { +export class WindowStateMain implements WindowMain { - private proxy: WindowStateExt; + private readonly proxy: WindowStateExt; - constructor(rpc: RPCProtocol) { + private readonly windowService: WindowService; + + constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WINDOW_STATE_EXT); + this.windowService = container.get(WindowService); window.addEventListener('focus', () => this.onFocusChanged(true)); window.addEventListener('blur', () => this.onFocusChanged(false)); @@ -32,4 +39,15 @@ export class WindowStateMain { this.proxy.$onWindowStateChanged(focused); } + async $openUri(uriComponent: UriComponents): Promise { + const uri = URI.revive(uriComponent); + const url = encodeURI(uri.toString(true)); + try { + this.windowService.openNewWindow(url, { external: true }); + return true; + } catch (e) { + return false; + } + } + } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index c5ef60a8e15d1..39832fb99e0d1 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -153,7 +153,7 @@ export function createAPIFactory( const commandRegistry = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); const quickOpenExt = rpc.set(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT, new QuickOpenExtImpl(rpc)); const dialogsExt = new DialogsExtImpl(rpc); - const windowStateExt = rpc.set(MAIN_RPC_CONTEXT.WINDOW_STATE_EXT, new WindowStateExtImpl()); + const windowStateExt = rpc.set(MAIN_RPC_CONTEXT.WINDOW_STATE_EXT, new WindowStateExtImpl(rpc)); const notificationExt = rpc.set(MAIN_RPC_CONTEXT.NOTIFICATION_EXT, new NotificationExtImpl(rpc)); const editors = rpc.set(MAIN_RPC_CONTEXT.TEXT_EDITORS_EXT, new TextEditorsExtImpl(rpc, editorsAndDocumentsExt)); const documents = rpc.set(MAIN_RPC_CONTEXT.DOCUMENTS_EXT, new DocumentsExtImpl(rpc, editorsAndDocumentsExt)); @@ -501,8 +501,10 @@ export function createAPIFactory( }, getClientOperatingSystem(): PromiseLike { return envExt.getClientOperatingSystem(); + }, + openExternal(uri: theia.Uri): PromiseLike { + return windowStateExt.openUri(uri); } - }); const languageServer: typeof theia.languageServer = { diff --git a/packages/plugin-ext/src/plugin/window-state.ts b/packages/plugin-ext/src/plugin/window-state.ts index 3578377754926..a75a34cb3bca5 100644 --- a/packages/plugin-ext/src/plugin/window-state.ts +++ b/packages/plugin-ext/src/plugin/window-state.ts @@ -14,9 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import URI from 'vscode-uri'; import { WindowState } from '@theia/plugin'; -import { WindowStateExt } from '../common/plugin-api-rpc'; +import { WindowStateExt, WindowMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; import { Event, Emitter } from '@theia/core/lib/common/event'; +import { RPCProtocol } from '../common/rpc-protocol'; export class WindowStateExtImpl implements WindowStateExt { @@ -25,7 +27,10 @@ export class WindowStateExtImpl implements WindowStateExt { private windowStateChangedEmitter = new Emitter(); public readonly onDidChangeWindowState: Event = this.windowStateChangedEmitter.event; - constructor() { + private readonly proxy: WindowMain; + + constructor(rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.WINDOW_MAIN); this.windowStateCached = { focused: true }; // supposed tab is active on start } @@ -43,4 +48,8 @@ export class WindowStateExtImpl implements WindowStateExt { this.windowStateChangedEmitter.fire(state); } + openUri(uri: URI): Promise { + return this.proxy.$openUri(uri); + } + } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 559efd0570986..9d562b5d25259 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -4744,6 +4744,18 @@ declare module '@theia/plugin' { */ export const sessionId: string; + /** + * Opens an *external* item, e.g. a http(s) or mailto-link, using the + * default application. + * + * *Note* that [`showTextDocument`](#window.showTextDocument) is the right + * way to open a text document inside the editor, not this function. + * + * @param target The uri that should be opened. + * @returns A promise indicating if open was successful. + */ + export function openExternal(target: Uri): PromiseLike; + } /** From f34d20ce0ccb3dcd0722d66846552606505c21dc Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 26 Aug 2019 12:12:22 +0000 Subject: [PATCH 06/10] =?UTF-8?q?[plugin]=C2=A0list=20`onUri`=20as=20a=20s?= =?UTF-8?q?upported=20activation=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It should be triggered when a os-wide URI is opened, for cloud case it is not necessary, so stubbing for now. As soon as os-wide opening of URIs is implemented for Electron this event should be fired as well. Signed-off-by: Anton Kosyakov --- packages/plugin-ext/src/plugin/plugin-context.ts | 1 + packages/plugin-ext/src/plugin/plugin-manager.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 39832fb99e0d1..802a07696463e 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -371,6 +371,7 @@ export function createAPIFactory( return decorationsExt.registerDecorationProvider(provider); }, registerUriHandler(handler: theia.UriHandler): theia.Disposable { + // TODO ? return new Disposable(() => { }); }, createInputBox(): theia.InputBox { diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 0deca4cb93df2..a834edd77c9e1 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -67,7 +67,8 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { 'onCommand', 'onDebug', 'onDebugInitialConfigurations', 'onDebugResolve', 'onDebugAdapterProtocolTracker', 'workspaceContains', - 'onView' + 'onView', + 'onUri' ]); private readonly registry = new Map(); From 67a04cba04454b3c77399a5483f2544e204b38af Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 2 Sep 2019 12:27:33 +0000 Subject: [PATCH 07/10] fix #6083: optimize tab bar decorations rendering - cache decorations - debounce rendering Signed-off-by: Anton Kosyakov --- .../src/browser/shell/tab-bar-decorator.ts | 38 +++---- packages/core/src/browser/shell/tab-bars.ts | 71 +++++++++---- .../problem/problem-tabbar-decorator.ts | 100 +++--------------- 3 files changed, 83 insertions(+), 126 deletions(-) diff --git a/packages/core/src/browser/shell/tab-bar-decorator.ts b/packages/core/src/browser/shell/tab-bar-decorator.ts index 96c660c8f0644..41be55578f7f4 100644 --- a/packages/core/src/browser/shell/tab-bar-decorator.ts +++ b/packages/core/src/browser/shell/tab-bar-decorator.ts @@ -14,9 +14,10 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import debounce = require('lodash.debounce'); +import { Title, Widget } from '@phosphor/widgets'; import { inject, injectable, named, postConstruct } from 'inversify'; import { Event, Emitter, Disposable, DisposableCollection, ContributionProvider } from '../../common'; -import { Title, Widget } from '@phosphor/widgets'; import { WidgetDecoration } from '../widget-decoration'; export const TabBarDecorator = Symbol('TabBarDecorator'); @@ -34,11 +35,11 @@ export interface TabBarDecorator { readonly onDidChangeDecorations: Event; /** - * Decorate tabs by the underlying URI. - * @param {Title[]} titles An array of the titles of the tabs. - * @returns A map from the URI of the tab to its decoration data. + * Decorate title. + * @param {Title} title the title + * @returns decoration data. */ - decorate(titles: Title[]): Map; + decorate(title: Title): WidgetDecoration.Data[]; } @injectable() @@ -57,33 +58,28 @@ export class TabBarDecoratorService implements Disposable { protected init(): void { const decorators = this.contributions.getContributions(); this.toDispose.pushAll(decorators.map(decorator => - decorator.onDidChangeDecorations(data => - this.onDidChangeDecorationsEmitter.fire(undefined) - )) - ); + decorator.onDidChangeDecorations(this.fireDidChangeDecorations) + )); } dispose(): void { this.toDispose.dispose(); } + protected fireDidChangeDecorations = debounce(() => this.onDidChangeDecorationsEmitter.fire(undefined), 150); + /** * Assign tabs the decorators provided by all the contributions. - * @param {Title[]} titles An array of the titles of the tabs. - * @returns A map from the URI of the tab to an array of its decoration data. + * @param {Title} title the title + * @returns an array of its decoration data. */ - getDecorations(titles: Title[]): Map { + getDecorations(title: Title): WidgetDecoration.Data[] { const decorators = this.contributions.getContributions(); - const changes: Map = new Map(); + let all: WidgetDecoration.Data[] = []; for (const decorator of decorators) { - for (const [id, data] of (decorator.decorate(titles)).entries()) { - if (changes.has(id)) { - changes.get(id)!.push(data); - } else { - changes.set(id, [data]); - } - } + const decorations = decorator.decorate(title); + all = all.concat(decorations); } - return changes; + return all; } } diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 7390a9ea74372..69a12deef6411 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -17,9 +17,9 @@ import PerfectScrollbar from 'perfect-scrollbar'; import { TabBar, Title, Widget } from '@phosphor/widgets'; import { VirtualElement, h, VirtualDOM, ElementInlineStyle } from '@phosphor/virtualdom'; -import { DisposableCollection, MenuPath, notEmpty } from '../../common'; +import { Disposable, DisposableCollection, MenuPath, notEmpty } from '../../common'; import { ContextMenuRenderer } from '../context-menu-renderer'; -import { Signal } from '@phosphor/signaling'; +import { Signal, Slot } from '@phosphor/signaling'; import { Message } from '@phosphor/messaging'; import { ArrayExt } from '@phosphor/algorithm'; import { ElementExt } from '@phosphor/domutils'; @@ -63,11 +63,6 @@ export interface SideBarRenderData extends TabBar.IRenderData { */ export class TabBarRenderer extends TabBar.Renderer { - /** - * A reference to the tab bar is required in order to activate it when a context menu - * is requested. - */ - tabBar?: TabBar; /** * The menu path used to render the context menu. */ @@ -84,9 +79,34 @@ export class TabBarRenderer extends TabBar.Renderer { ) { super(); if (this.decoratorService) { + this.toDispose.push(Disposable.create(() => this.resetDecorations())); this.toDispose.push(this.decoratorService); - this.toDispose.push(this.decoratorService.onDidChangeDecorations(() => this.tabBar && this.tabBar.update())); + this.toDispose.push(this.decoratorService.onDidChangeDecorations(() => this.resetDecorations())); + } + } + + protected _tabBar?: TabBar; + protected readonly toDisposeOnTabBar = new DisposableCollection(); + /** + * A reference to the tab bar is required in order to activate it when a context menu + * is requested. + */ + set tabBar(tabBar: TabBar | undefined) { + if (this._tabBar === tabBar) { + return; + } + this.toDisposeOnTabBar.dispose(); + this.toDispose.push(this.toDisposeOnTabBar); + this._tabBar = tabBar; + if (tabBar) { + const listener: Slot> = (_, { title }) => this.resetDecorations(title); + tabBar.tabCloseRequested.connect(listener); + this.toDisposeOnTabBar.push(Disposable.create(() => tabBar.tabCloseRequested.disconnect(listener))); } + this.resetDecorations(); + } + get tabBar(): TabBar | undefined { + return this._tabBar; } /** @@ -183,28 +203,39 @@ export class TabBarRenderer extends TabBar.Renderer { return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label); } + protected readonly decorations = new Map, WidgetDecoration.Data[]>(); + + protected resetDecorations(title?: Title): void { + if (title) { + this.decorations.delete(title); + } else { + this.decorations.clear(); + } + if (this.tabBar) { + this.tabBar.update(); + } + } + /** * Get all available decorations of a given tab. - * @param {string} tab The URI of the tab. + * @param {string} title The widget title. */ - protected getDecorations(tab: string): WidgetDecoration.Data[] { - const tabDecorations = []; + protected getDecorations(title: Title): WidgetDecoration.Data[] { if (this.tabBar && this.decoratorService) { - const allDecorations = this.decoratorService.getDecorations([...this.tabBar.titles]); - if (allDecorations.has(tab)) { - tabDecorations.push(...allDecorations.get(tab)); - } + const decorations = this.decorations.get(title) || this.decoratorService.getDecorations(title); + this.decorations.set(title, decorations); + return decorations; } - return tabDecorations; + return []; } /** * Get the decoration data given the tab URI and the decoration data type. - * @param {string} tab The URI of the tab. + * @param {string} title The title. * @param {K} key The type of the decoration data. */ - protected getDecorationData(tab: string, key: K): WidgetDecoration.Data[K][] { - return this.getDecorations(tab).filter(data => data[key] !== undefined).map(data => data[key]); + protected getDecorationData(title: Title, key: K): WidgetDecoration.Data[K][] { + return this.getDecorations(title).filter(data => data[key] !== undefined).map(data => data[key]); } @@ -318,7 +349,7 @@ export class TabBarRenderer extends TabBar.Renderer { const baseClassName = this.createIconClass(data); const overlayIcons: VirtualElement[] = []; - const decorationData = this.getDecorationData(data.title.caption, 'iconOverlay'); + const decorationData = this.getDecorationData(data.title, 'iconOverlay'); // Check if the tab has decoration markers to be rendered on top. if (decorationData.length > 0) { diff --git a/packages/markers/src/browser/problem/problem-tabbar-decorator.ts b/packages/markers/src/browser/problem/problem-tabbar-decorator.ts index e0adedbfdd3dc..56a13347fb36d 100644 --- a/packages/markers/src/browser/problem/problem-tabbar-decorator.ts +++ b/packages/markers/src/browser/problem/problem-tabbar-decorator.ts @@ -15,24 +15,22 @@ ********************************************************************************/ import { inject, injectable, postConstruct } from 'inversify'; -import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; -import URI from '@theia/core/lib/common/uri'; -import { notEmpty } from '@theia/core/lib/common/objects'; +import { Diagnostic } from 'vscode-languageserver-types'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { Title, Widget } from '@phosphor/widgets'; import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration'; import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator'; import { Marker } from '../../common/marker'; import { ProblemManager } from './problem-manager'; -import { WorkspaceService } from '@theia/workspace/lib/browser'; import { ProblemPreferences, ProblemConfiguration } from './problem-preferences'; -import { PreferenceChangeEvent } from '@theia/core/lib/browser'; +import { PreferenceChangeEvent, Navigatable } from '@theia/core/lib/browser'; + @injectable() export class ProblemTabBarDecorator implements TabBarDecorator { readonly id = 'theia-problem-tabbar-decorator'; - protected emitter: Emitter; + protected readonly emitter = new Emitter(); @inject(ProblemPreferences) protected readonly preferences: ProblemPreferences; @@ -40,18 +38,23 @@ export class ProblemTabBarDecorator implements TabBarDecorator { @inject(ProblemManager) protected readonly problemManager: ProblemManager; - @inject(WorkspaceService) - protected readonly workspaceService: WorkspaceService; - @postConstruct() protected init(): void { - this.emitter = new Emitter(); this.problemManager.onDidChangeMarkers(() => this.fireDidChangeDecorations()); this.preferences.onPreferenceChanged(event => this.handlePreferenceChange(event)); } - decorate(titles: Title[]): Map { - return this.collectDecorators(titles); + decorate(title: Title): WidgetDecoration.Data[] { + const widget = title.owner; + if (Navigatable.is(widget)) { + const resourceUri = widget.getResourceUri(); + if (resourceUri) { + return this.problemManager.findMarkers({ + uri: resourceUri + }).map(marker => this.toDecorator(marker)); + } + } + return []; } get onDidChangeDecorations(): Event { @@ -73,58 +76,6 @@ export class ProblemTabBarDecorator implements TabBarDecorator { } } - /** - * Collect decorators for the tabs. - * @returns {Map} A map from the tab URI to the tab decoration data. - */ - protected collectDecorators(titles: Title[]): Map { - const result: Map> = new Map(); - if (this.preferences['problems.decorations.tabbar.enabled']) { - const markers = this.groupMarkersByURI(this.collectMarkers()); - for (const title of titles) { - // Ensure `title.caption` does not contain illegal characters for URI. - try { - const fileUri: URI = new URI(title.caption); - const marker = markers.get(fileUri.withScheme('file').toString()); - if (marker) { - result.set(title.caption, marker); - } - } catch (e) { - } - } - } - const urlDecoratorMap = new Map(Array.from(result.entries()) - .map(entry => [entry[0], this.toDecorator(entry[1])] as [string, WidgetDecoration.Data])); - return urlDecoratorMap; - } - - /** - * Group markers by the URI of the editor they decorate. - * @param {Marker[]} markers All the diagnostic markers collected. - * @returns {Map>} A map from URI of the editor to its diagnostic markers. - */ - protected groupMarkersByURI(markers: Marker[]): Map> { - const result: Map> = new Map(); - for (const [uri, marker] of new Map(markers.map(m => [new URI(m.uri), m] as [URI, Marker])).entries()) { - const uriString = uri.toString(); - result.set(uriString, marker); - } - return result; - } - - /** - * Collect all diagnostic markers from the problem manager. - * @returns {Marker[]} An array of diagnostic markers. - */ - protected collectMarkers(): Marker[] { - return Array.from(this.problemManager.getUris()) - .map(str => new URI(str)) - .map(uri => this.problemManager.findMarkers({ uri })) - .map(markers => markers.sort(this.compare.bind(this)).shift()) - .filter(notEmpty) - .filter(this.filterMarker.bind(this)); - } - /** * Convert a diagnostic marker to a decorator. * @param {Marker} marker A diagnostic marker. @@ -177,25 +128,4 @@ export class ProblemTabBarDecorator implements TabBarDecorator { } } - /** - * Filter the diagnostic marker by its severity. - * @param {Marker} marker A diagnostic marker. - * @returns {boolean} Whether the diagnostic marker is of `Error`, `Warning`, or `Information` severity. - */ - protected filterMarker(marker: Marker): boolean { - const { severity } = marker.data; - return severity === DiagnosticSeverity.Error - || severity === DiagnosticSeverity.Warning - || severity === DiagnosticSeverity.Information; - } - - /** - * Compare the severity of two diagnostic markers. - * @param {Marker} left A diagnostic marker to be compared. - * @param {Marker} right A diagnostic marker to be compared. - * @returns {number} Number indicating which marker takes priority (`left` if negative, `right` if positive). - */ - protected compare(left: Marker, right: Marker): number { - return (left.data.severity || Number.MAX_SAFE_INTEGER) - (right.data.severity || Number.MAX_SAFE_INTEGER); - } } From 737d348b84dd66fc8a18940b86b9f0ef4ea72559 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 2 Sep 2019 12:28:47 +0000 Subject: [PATCH 08/10] fix #6084: optimize problem status rendering - debounce status update - travers all markers only once, not for each Uri and then for each severity Signed-off-by: Anton Kosyakov --- .../browser/problem/problem-contribution.ts | 8 +++---- .../src/browser/problem/problem-manager.ts | 21 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/markers/src/browser/problem/problem-contribution.ts b/packages/markers/src/browser/problem/problem-contribution.ts index adf972d4e8128..3ff29d52dddb3 100644 --- a/packages/markers/src/browser/problem/problem-contribution.ts +++ b/packages/markers/src/browser/problem/problem-contribution.ts @@ -14,6 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import debounce = require('lodash.debounce'); import { injectable, inject } from 'inversify'; import { FrontendApplication, FrontendApplicationContribution, CompositeTreeNode, SelectableTreeNode, Widget } from '@theia/core/lib/browser'; import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar'; @@ -70,16 +71,15 @@ export class ProblemContribution extends AbstractViewContribution } onStart(app: FrontendApplication): void { - this.setStatusBarElement(this.problemManager.getProblemStat()); - this.problemManager.onDidChangeMarkers(() => { - this.setStatusBarElement(this.problemManager.getProblemStat()); - }); + this.updateStatusBarElement(); + this.problemManager.onDidChangeMarkers(this.updateStatusBarElement); } async initializeLayout(app: FrontendApplication): Promise { await this.openView(); } + protected updateStatusBarElement = debounce(() => this.setStatusBarElement(this.problemManager.getProblemStat()), 10); protected setStatusBarElement(problemStat: ProblemStat): void { this.statusBar.setElement('problem-marker-status', { text: problemStat.infos <= 0 diff --git a/packages/markers/src/browser/problem/problem-manager.ts b/packages/markers/src/browser/problem/problem-manager.ts index de0d2c195cc3c..074b0c830f4d8 100644 --- a/packages/markers/src/browser/problem/problem-manager.ts +++ b/packages/markers/src/browser/problem/problem-manager.ts @@ -17,8 +17,6 @@ import { injectable } from 'inversify'; import { MarkerManager } from '../marker-manager'; import { PROBLEM_KIND } from '../../common/problem-marker'; -import { Marker } from '../../common/marker'; -import URI from '@theia/core/lib/common/uri'; import { Diagnostic } from 'vscode-languageserver-types'; export interface ProblemStat { @@ -35,15 +33,18 @@ export class ProblemManager extends MarkerManager { } getProblemStat(): ProblemStat { - const allMarkers: Marker[] = []; - for (const uri of this.getUris()) { - allMarkers.push(...this.findMarkers({ uri: new URI(uri) })); + let errors = 0; + let warnings = 0; + let infos = 0; + for (const marker of this.findMarkers()) { + if (marker.data.severity === 1) { + errors++; + } else if (marker.data.severity === 2) { + warnings++; + } else if (marker.data.severity === 3) { + infos++; + } } - - const errors = allMarkers.filter(m => m.data.severity === 1).length; - const warnings = allMarkers.filter(m => m.data.severity === 2).length; - const infos = allMarkers.filter(m => m.data.severity === 3).length; - return { errors, warnings, infos }; } From a2cc0ee433f87791fa14ad0c75913ded864613b6 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Tue, 3 Sep 2019 09:21:29 +0000 Subject: [PATCH 09/10] [plugin] fix #6091: pass proper document change rangeOffset Signed-off-by: Anton Kosyakov --- packages/plugin-ext/src/main/browser/documents-main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-ext/src/main/browser/documents-main.ts b/packages/plugin-ext/src/main/browser/documents-main.ts index 858a275a1095a..df808e632c406 100644 --- a/packages/plugin-ext/src/main/browser/documents-main.ts +++ b/packages/plugin-ext/src/main/browser/documents-main.ts @@ -153,7 +153,7 @@ export class DocumentsMainImpl implements DocumentsMain { text: c.text, range: c.range, rangeLength: c.rangeLength, - rangeOffset: 0 + rangeOffset: c.rangeOffset })) }, model.dirty); })); From 6b80981d9319ea19f7536359d0d20de6c8bf8956 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Tue, 3 Sep 2019 11:58:59 +0000 Subject: [PATCH 10/10] [plugin] fix #6092: make sure command arguments are passed safely via jsonrpc Signed-off-by: Anton Kosyakov --- .../src/common/plugin-api-rpc-model.ts | 1 - .../plugin-ext/src/plugin/command-registry.ts | 32 ++++++++++-- packages/plugin-ext/src/plugin/languages.ts | 12 +++-- .../src/plugin/languages/code-action.ts | 11 ++-- .../src/plugin/languages/completion.ts | 33 +++++++++--- .../plugin-ext/src/plugin/languages/lens.ts | 48 +++++++++++------- .../plugin-ext/src/plugin/plugin-context.ts | 2 +- packages/plugin-ext/src/plugin/scm.ts | 50 ++++++++++++------- .../plugin-ext/src/plugin/tree/tree-views.ts | 3 +- .../plugin-ext/src/plugin/type-converters.ts | 17 ++----- packages/scm/src/browser/scm-provider.ts | 2 + 11 files changed, 138 insertions(+), 73 deletions(-) diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 4938d87486706..fa219971e5d4f 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -322,7 +322,6 @@ export interface DocumentLinkProvider { export interface CodeLensSymbol { range: Range; - id?: string; command?: Command; } diff --git a/packages/plugin-ext/src/plugin/command-registry.ts b/packages/plugin-ext/src/plugin/command-registry.ts index 87352948df467..60b801dbe05f5 100644 --- a/packages/plugin-ext/src/plugin/command-registry.ts +++ b/packages/plugin-ext/src/plugin/command-registry.ts @@ -19,6 +19,7 @@ *--------------------------------------------------------------------------------------------*/ import * as theia from '@theia/plugin'; +import * as model from '../common/plugin-api-rpc-model'; import { CommandRegistryExt, PLUGIN_RPC_CONTEXT as Ext, CommandRegistryMain } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; import { Disposable } from './types-impl'; @@ -153,26 +154,47 @@ export class CommandsConverter { /** * Convert to a command that can be safely passed over JSON-RPC. */ - toSafeCommand(command: theia.Command, disposables: DisposableCollection): theia.Command { + toSafeCommand(command: undefined, disposables: DisposableCollection): undefined; + toSafeCommand(command: theia.Command, disposables: DisposableCollection): model.Command; + toSafeCommand(command: theia.Command | undefined, disposables: DisposableCollection): model.Command | undefined; + toSafeCommand(command: theia.Command | undefined, disposables: DisposableCollection): model.Command | undefined { + if (!command) { + return undefined; + } + const result = this.toInternalCommand(command); + if (KnownCommands.mapped(result.id)) { + return result; + } + if (!this.isSafeCommandRegistered) { this.commands.registerCommand({ id: this.safeCommandId }, this.executeSafeCommand, this); this.isSafeCommandRegistered = true; } - const result: theia.Command = {}; - Object.assign(result, command); - if (command.command && command.arguments && command.arguments.length > 0) { const id = this.handle++; this.commandsMap.set(id, command); disposables.push(new Disposable(() => this.commandsMap.delete(id))); - result.command = this.safeCommandId; + result.id = this.safeCommandId; result.arguments = [id]; } return result; } + protected 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 + })); + } + // tslint:disable-next-line:no-any private executeSafeCommand(...args: any[]): PromiseLike { const command = this.commandsMap.get(args[0]); diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 42bc2b79487a5..a60325e72acea 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -78,6 +78,7 @@ import { FoldingProviderAdapter } from './languages/folding'; import { ColorProviderAdapter } from './languages/color'; import { RenameAdapter } from './languages/rename'; import { Event } from '@theia/core/lib/common/event'; +import { CommandRegistryImpl } from './command-registry'; type Adapter = CompletionAdapter | SignatureHelpAdapter | @@ -109,7 +110,10 @@ export class LanguagesExtImpl implements LanguagesExt { private callId = 0; private adaptersMap = new Map(); - constructor(rpc: RPCProtocol, private readonly documents: DocumentsExtImpl) { + constructor( + rpc: RPCProtocol, + private readonly documents: DocumentsExtImpl, + private readonly commands: CommandRegistryImpl) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.LANGUAGES_MAIN); this.diagnostics = new Diagnostics(rpc); } @@ -225,7 +229,7 @@ export class LanguagesExtImpl implements LanguagesExt { } registerCompletionItemProvider(selector: theia.DocumentSelector, provider: theia.CompletionItemProvider, triggerCharacters: string[]): theia.Disposable { - const callId = this.addNewAdapter(new CompletionAdapter(provider, this.documents)); + const callId = this.addNewAdapter(new CompletionAdapter(provider, this.documents, this.commands)); this.proxy.$registerCompletionSupport(callId, this.transformDocumentSelector(selector), triggerCharacters, CompletionAdapter.hasResolveSupport(provider)); return this.createDisposable(callId); } @@ -395,7 +399,7 @@ export class LanguagesExtImpl implements LanguagesExt { pluginModel: PluginModel, metadata?: theia.CodeActionProviderMetadata ): theia.Disposable { - const callId = this.addNewAdapter(new CodeActionAdapter(provider, this.documents, this.diagnostics, pluginModel ? pluginModel.id : '')); + const callId = this.addNewAdapter(new CodeActionAdapter(provider, this.documents, this.diagnostics, pluginModel ? pluginModel.id : '', this.commands)); this.proxy.$registerQuickFixProvider( callId, this.transformDocumentSelector(selector), @@ -416,7 +420,7 @@ export class LanguagesExtImpl implements LanguagesExt { // ### Code Lens Provider begin registerCodeLensProvider(selector: theia.DocumentSelector, provider: theia.CodeLensProvider): theia.Disposable { - const callId = this.addNewAdapter(new CodeLensAdapter(provider, this.documents)); + const callId = this.addNewAdapter(new CodeLensAdapter(provider, this.documents, this.commands)); const eventHandle = typeof provider.onDidChangeCodeLenses === 'function' ? this.nextCallId() : undefined; this.proxy.$registerCodeLensSupport(callId, this.transformDocumentSelector(selector), eventHandle); let result = this.createDisposable(callId); diff --git a/packages/plugin-ext/src/plugin/languages/code-action.ts b/packages/plugin-ext/src/plugin/languages/code-action.ts index 6e7722fa6d1ca..04c2646ac976e 100644 --- a/packages/plugin-ext/src/plugin/languages/code-action.ts +++ b/packages/plugin-ext/src/plugin/languages/code-action.ts @@ -22,6 +22,8 @@ import * as Converter from '../type-converters'; import { DocumentsExtImpl } from '../documents'; import { Diagnostics } from './diagnostics'; import { CodeActionKind } from '../types-impl'; +import { CommandRegistryImpl } from '../command-registry'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; export class CodeActionAdapter { @@ -29,7 +31,8 @@ export class CodeActionAdapter { private readonly provider: theia.CodeActionProvider, private readonly document: DocumentsExtImpl, private readonly diagnostics: Diagnostics, - private readonly pluginId: string + private readonly pluginId: string, + private readonly commands: CommandRegistryImpl ) { } provideCodeAction(resource: URI, rangeOrSelection: Range | Selection, @@ -60,6 +63,8 @@ export class CodeActionAdapter { if (!Array.isArray(commandsOrActions) || commandsOrActions.length === 0) { return undefined!; } + // TODO cache toDispose and dispose it + const toDispose = new DisposableCollection(); const result: monaco.languages.CodeAction[] = []; for (const candidate of commandsOrActions) { if (!candidate) { @@ -68,7 +73,7 @@ export class CodeActionAdapter { if (CodeActionAdapter._isCommand(candidate)) { result.push({ title: candidate.title || '', - command: Converter.toInternalCommand(candidate) + command: this.commands.converter.toSafeCommand(candidate, toDispose) }); } else { if (codeActionContext.only) { @@ -83,7 +88,7 @@ export class CodeActionAdapter { result.push({ title: candidate.title, - command: candidate.command && Converter.toInternalCommand(candidate.command), + command: this.commands.converter.toSafeCommand(candidate.command, toDispose), diagnostics: candidate.diagnostics && candidate.diagnostics.map(Converter.convertDiagnosticToMarkerData) as monaco.editor.IMarker[], edit: candidate.edit && Converter.fromWorkspaceEdit(candidate.edit) as monaco.languages.WorkspaceEdit, kind: candidate.kind && candidate.kind.value diff --git a/packages/plugin-ext/src/plugin/languages/completion.ts b/packages/plugin-ext/src/plugin/languages/completion.ts index 6f83655dfb250..4a3e893d2c0af 100644 --- a/packages/plugin-ext/src/plugin/languages/completion.ts +++ b/packages/plugin-ext/src/plugin/languages/completion.ts @@ -22,15 +22,19 @@ import * as Converter from '../type-converters'; import { mixin } from '../../common/types'; import { Position } from '../../common/plugin-api-rpc'; import { CompletionContext, CompletionResultDto, Completion, CompletionDto } from '../../common/plugin-api-rpc-model'; +import { CommandRegistryImpl } from '../command-registry'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; export class CompletionAdapter { private cacheId = 0; - private cache = new Map(); + private readonly cache = new Map(); + private readonly disposables = new Map(); - constructor(private readonly delegate: theia.CompletionItemProvider, - private readonly documents: DocumentsExtImpl) { - - } + constructor( + private readonly delegate: theia.CompletionItemProvider, + private readonly documents: DocumentsExtImpl, + private readonly commands: CommandRegistryImpl + ) { } provideCompletionItems(resource: URI, position: Position, context: CompletionContext, token: theia.CancellationToken): Promise { const document = this.documents.getDocumentData(resource); @@ -43,6 +47,10 @@ export class CompletionAdapter { const pos = Converter.toPosition(position); return Promise.resolve(this.delegate.provideCompletionItems(doc, pos, token, context)).then(value => { const id = this.cacheId++; + + const toDispose = new DisposableCollection(); + this.disposables.set(id, toDispose); + const result: CompletionResultDto = { id, completions: [], @@ -102,9 +110,13 @@ export class CompletionAdapter { }); } - releaseCompletionItems(id: number): Promise { + async releaseCompletionItems(id: number): Promise { this.cache.delete(id); - return Promise.resolve(); + const toDispose = this.disposables.get(id); + if (toDispose) { + toDispose.dispose(); + this.disposables.delete(id); + } } private convertCompletionItem(item: theia.CompletionItem, position: theia.Position, defaultRange: theia.Range, id: number, parentId: number): CompletionDto | undefined { @@ -113,6 +125,11 @@ export class CompletionAdapter { return undefined; } + const toDispose = this.disposables.get(parentId); + if (!toDispose) { + throw Error('DisposableCollection is missing...'); + } + const result: CompletionDto = { id, parentId, @@ -125,7 +142,7 @@ export class CompletionAdapter { preselect: item.preselect, insertText: '', additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(Converter.fromTextEdit), - command: undefined, // TODO: implement this: this.commands.toInternal(item.command), + command: this.commands.converter.toSafeCommand(item.command, toDispose), commitCharacters: item.commitCharacters }; diff --git a/packages/plugin-ext/src/plugin/languages/lens.ts b/packages/plugin-ext/src/plugin/languages/lens.ts index 0cc942c695674..1900f1dc166fb 100644 --- a/packages/plugin-ext/src/plugin/languages/lens.ts +++ b/packages/plugin-ext/src/plugin/languages/lens.ts @@ -20,6 +20,8 @@ import { DocumentsExtImpl } from '../documents'; import { CodeLensSymbol } from '../../common/plugin-api-rpc-model'; import * as Converter from '../type-converters'; import { ObjectIdentifier } from '../../common/object-identifier'; +import { CommandRegistryImpl } from '../command-registry'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; /** Adapts the calls from main to extension thread for providing/resolving the code lenses. */ export class CodeLensAdapter { @@ -27,11 +29,13 @@ export class CodeLensAdapter { private static readonly BAD_CMD: theia.Command = { command: 'missing', title: '<>' }; private cacheId = 0; - private cache = new Map(); + private readonly cache = new Map(); + private readonly disposables = new Map(); constructor( private readonly provider: theia.CodeLensProvider, private readonly documents: DocumentsExtImpl, + private readonly commands: CommandRegistryImpl ) { } provideCodeLenses(resource: URI, token: theia.CancellationToken): Promise { @@ -45,12 +49,15 @@ export class CodeLensAdapter { return Promise.resolve(this.provider.provideCodeLenses(doc, token)).then(lenses => { if (Array.isArray(lenses)) { return lenses.map(lens => { - const id = this.cacheId++; + const cacheId = this.cacheId++; + const toDispose = new DisposableCollection(); const lensSymbol = ObjectIdentifier.mixin({ range: Converter.fromRange(lens.range)!, - command: lens.command ? Converter.toInternalCommand(lens.command) : undefined - }, id); - this.cache.set(id, lens); + command: this.commands.converter.toSafeCommand(lens.command, toDispose) + }, cacheId); + // TODO: invalidate caches and dispose command handlers + this.cache.set(cacheId, lens); + this.disposables.set(cacheId, toDispose); return lensSymbol; }); } @@ -58,23 +65,28 @@ export class CodeLensAdapter { }); } - resolveCodeLens(resource: URI, symbol: CodeLensSymbol, token: theia.CancellationToken): Promise { - const lens = this.cache.get(ObjectIdentifier.of(symbol)); + async resolveCodeLens(resource: URI, symbol: CodeLensSymbol, token: theia.CancellationToken): Promise { + const cacheId = ObjectIdentifier.of(symbol); + const lens = this.cache.get(cacheId); if (!lens) { - return Promise.resolve(undefined); + return undefined; } - let resolve: Promise; - if (typeof this.provider.resolveCodeLens !== 'function' || lens.isResolved) { - resolve = Promise.resolve(lens); - } else { - resolve = Promise.resolve(this.provider.resolveCodeLens(lens, token)); + let newLens: theia.CodeLens | undefined; + if (typeof this.provider.resolveCodeLens === 'function' && !lens.isResolved) { + newLens = await this.provider.resolveCodeLens(lens, token); + if (token.isCancellationRequested) { + return undefined; + } } + newLens = newLens || lens; - return resolve.then(newLens => { - newLens = newLens || lens; - symbol.command = Converter.toInternalCommand(newLens.command ? newLens.command : CodeLensAdapter.BAD_CMD); - return symbol; - }); + const disposables = this.disposables.get(cacheId); + if (!disposables) { + // already been disposed of + return undefined; + } + symbol.command = this.commands.converter.toSafeCommand(newLens.command ? newLens.command : CodeLensAdapter.BAD_CMD, disposables); + return symbol; } } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 802a07696463e..6935baca1928e 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -160,7 +160,7 @@ export function createAPIFactory( const statusBarMessageRegistryExt = new StatusBarMessageRegistryExt(rpc); const terminalExt = rpc.set(MAIN_RPC_CONTEXT.TERMINAL_EXT, new TerminalServiceExtImpl(rpc)); const outputChannelRegistryExt = new OutputChannelRegistryExt(rpc); - const languagesExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_EXT, new LanguagesExtImpl(rpc, documents)); + const languagesExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_EXT, new LanguagesExtImpl(rpc, documents, commandRegistry)); const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry)); const webviewExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, new WebviewsExtImpl(rpc)); const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc)); diff --git a/packages/plugin-ext/src/plugin/scm.ts b/packages/plugin-ext/src/plugin/scm.ts index 209e9a09fcf43..eeb2a2005451c 100644 --- a/packages/plugin-ext/src/plugin/scm.ts +++ b/packages/plugin-ext/src/plugin/scm.ts @@ -15,11 +15,14 @@ ********************************************************************************/ import * as theia from '@theia/plugin'; -import { CommandRegistryExt, Plugin as InternalPlugin, PLUGIN_RPC_CONTEXT, ScmExt, ScmMain, ScmCommandArg } from '../common/plugin-api-rpc'; +import { Plugin as InternalPlugin, PLUGIN_RPC_CONTEXT, ScmExt, ScmMain, ScmCommandArg } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; -import { CancellationToken } from '@theia/core'; +import { CancellationToken } from '@theia/core/lib/common/cancellation'; +import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { UriComponents } from '../common/uri-components'; import URI from '@theia/core/lib/common/uri'; +import { CommandRegistryImpl } from './command-registry'; +import { ScmCommand } from '@theia/scm/lib/browser/scm-provider'; export class ScmExtImpl implements ScmExt { private handle: number = 0; @@ -27,7 +30,7 @@ export class ScmExtImpl implements ScmExt { private readonly sourceControlMap = new Map(); private readonly sourceControlsByPluginMap: Map = new Map(); - constructor(readonly rpc: RPCProtocol, private readonly commands: CommandRegistryExt) { + constructor(readonly rpc: RPCProtocol, private readonly commands: CommandRegistryImpl) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.SCM_MAIN); commands.registerArgumentProcessor({ // tslint:disable-next-line:no-any @@ -140,15 +143,18 @@ class SourceControlImpl implements theia.SourceControl { private _acceptInputCommand: theia.Command | undefined; private _statusBarCommands: theia.Command[] | undefined; + private readonly toDispose = new DisposableCollection(); + constructor( private proxy: ScmMain, - private commands: CommandRegistryExt, + private commands: CommandRegistryImpl, private _id: string, private _label: string, private _rootUri?: theia.Uri ) { this._inputBox = new InputBoxImpl(proxy, this.handle); this.proxy.$registerSourceControl(this.handle, _id, _label, _rootUri ? _rootUri.path : undefined); + this.toDispose.push(Disposable.create(() => this.proxy.$unregisterSourceControl(this.handle))); } get id(): string { @@ -166,6 +172,7 @@ class SourceControlImpl implements theia.SourceControl { createResourceGroup(id: string, label: string): theia.SourceControlResourceGroup { const sourceControlResourceGroup = new SourceControlResourceGroupImpl(this.proxy, this.commands, this.handle, id, label); this.resourceGroupsMap.set(SourceControlImpl.resourceGroupHandle++, sourceControlResourceGroup); + this.toDispose.push(sourceControlResourceGroup); return sourceControlResourceGroup; } @@ -203,38 +210,45 @@ class SourceControlImpl implements theia.SourceControl { } dispose(): void { - this.proxy.$unregisterSourceControl(this.handle); + this.toDispose.dispose(); } + protected toDisposeOnAcceptInputCommand = new DisposableCollection(); + get acceptInputCommand(): theia.Command | undefined { return this._acceptInputCommand; } set acceptInputCommand(acceptInputCommand: theia.Command | undefined) { + this.toDisposeOnAcceptInputCommand.dispose(); + this.toDispose.push(this.toDisposeOnAcceptInputCommand); + this._acceptInputCommand = acceptInputCommand; - if (acceptInputCommand && acceptInputCommand.command) { - const command = { - id: acceptInputCommand.command, - title: acceptInputCommand.title || '' - }; - this.proxy.$updateSourceControl(this.handle, { acceptInputCommand: command }); - } + this.proxy.$updateSourceControl(this.handle, { + acceptInputCommand: this.commands.converter.toSafeCommand(acceptInputCommand, this.toDisposeOnAcceptInputCommand) + }); } + protected toDisposeOnStatusBarCommands = new DisposableCollection(); + get statusBarCommands(): theia.Command[] | undefined { return this._statusBarCommands; } set statusBarCommands(statusBarCommands: theia.Command[] | undefined) { + this.toDisposeOnStatusBarCommands.dispose(); + this.toDispose.push(this.toDisposeOnStatusBarCommands); + this._statusBarCommands = statusBarCommands; + + let safeStatusBarCommands: ScmCommand[] | undefined; if (statusBarCommands) { - const commands = statusBarCommands.map(statusBarCommand => ({ - command: statusBarCommand.command, - title: statusBarCommand.title || '' - })); - this.proxy.$updateSourceControl(this.handle, { statusBarCommands: commands }); + safeStatusBarCommands = statusBarCommands.map(statusBarCommand => this.commands.converter.toSafeCommand(statusBarCommand, this.toDisposeOnStatusBarCommands)); } + this.proxy.$updateSourceControl(this.handle, { + statusBarCommands: safeStatusBarCommands + }); } getResourceGroup(handle: number): SourceControlResourceGroupImpl | undefined { @@ -253,7 +267,7 @@ class SourceControlResourceGroupImpl implements theia.SourceControlResourceGroup constructor( private proxy: ScmMain, - private commands: CommandRegistryExt, + private commands: CommandRegistryImpl, private sourceControlHandle: number, private _id: string, private _label: string, diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts index d3e50160bd88e..500dfc4d6391c 100644 --- a/packages/plugin-ext/src/plugin/tree/tree-views.ts +++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts @@ -32,7 +32,6 @@ import { RPCProtocol } from '../../common/rpc-protocol'; import { CommandRegistryImpl, CommandsConverter } from '../command-registry'; import { TreeViewSelection } from '../../common'; import { PluginPackage } from '../../common/plugin-protocol'; -import { toInternalCommand } from '../type-converters'; export class TreeViewsExtImpl implements TreeViewsExt { @@ -319,7 +318,7 @@ class TreeViewExtImpl implements Disposable { tooltip: treeItem.tooltip, collapsibleState: treeItem.collapsibleState, contextValue: treeItem.contextValue, - command: treeItem.command ? toInternalCommand(this.commandsConverter.toSafeCommand(treeItem.command, toDisposeElement)) : undefined + command: this.commandsConverter.toSafeCommand(treeItem.command, toDisposeElement) } as TreeViewItem; treeItems.push(treeViewItem); diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 8b3b3ec1ba8c4..6c747414b1f28 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -442,19 +442,6 @@ export function fromDocumentHighlight(documentHighlight: theia.DocumentHighlight }; } -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] } = {}; @@ -463,6 +450,10 @@ export namespace KnownCommands { fromPositionToP, toArrayConversion(fromLocationToL))]; + export function mapped(id: string): boolean { + return !!mappings[id]; + } + 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)); diff --git a/packages/scm/src/browser/scm-provider.ts b/packages/scm/src/browser/scm-provider.ts index 4c27cb07ea736..d1b5c94726b0a 100644 --- a/packages/scm/src/browser/scm-provider.ts +++ b/packages/scm/src/browser/scm-provider.ts @@ -65,6 +65,8 @@ export interface ScmCommand { title: string; tooltip?: string; command?: string; + // tslint:disable-next-line:no-any + arguments?: any[]; } export interface ScmCommit {