diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 060d06f9fc8..fe9dd1abbc3 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1196,7 +1196,30 @@ declare module 'vscode' { } //#endregion - //#region Tree Item Label Highlights + //#region Tree View + + /** + * Options for creating a [TreeView](#TreeView] + */ + export interface TreeViewOptions { + + /** + * A data provider that provides tree data. + */ + treeDataProvider: TreeDataProvider; + + /** + * Whether to show collapse all action or not. + */ + showCollapseAll?: boolean; + } + + namespace window { + + export function createTreeView(viewId: string, options: TreeViewOptions): TreeView; + + } + /** * Label describing the [Tree item](#TreeItem) */ diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index 50a345b39bb..9ed8aff7cd0 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -26,12 +26,13 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); } - $registerTreeViewDataProvider(treeViewId: string): void { + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void { const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService); this._dataProviders.set(treeViewId, dataProvider); const viewer = this.getTreeViewer(treeViewId); if (viewer) { viewer.dataProvider = dataProvider; + viewer.showCollapseAllAction = !!options.showCollapseAll; this.registerListeners(treeViewId, viewer); this._proxy.$setVisible(treeViewId, viewer.visible); } else { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index c5b4176b5bc..7a6afa546c5 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -59,7 +59,7 @@ import { ExtHostUrls } from 'vs/workbench/api/node/extHostUrls'; import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; import { ExtHostWindow } from 'vs/workbench/api/node/extHostWindow'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; -import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionDescription, throwProposedApiError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; import * as vscode from 'vscode'; @@ -67,16 +67,6 @@ export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; } -export function checkProposedApiEnabled(extension: IExtensionDescription): void { - if (!extension.enableProposedApi) { - throwProposedApiError(extension); - } -} - -function throwProposedApiError(extension: IExtensionDescription): never { - throw new Error(`[${extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.id}`); -} - function proposedApiFunction(extension: IExtensionDescription, fn: T): T { if (extension.enableProposedApi) { return fn; @@ -461,10 +451,10 @@ export function createApiFactory( return extHostTerminalService.createTerminalRenderer(name); }), registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { - return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider); + return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension); }, createTreeView(viewId: string, options: { treeDataProvider: vscode.TreeDataProvider }): vscode.TreeView { - return extHostTreeViews.createTreeView(viewId, options); + return extHostTreeViews.createTreeView(viewId, options, extension); }, registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 395e42c6182..78d9bd8fb9f 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -210,7 +210,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string): void; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Thenable; $reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options: { select: boolean, focus: boolean }): Thenable; } diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/node/extHostTreeViews.ts index e6d4ee22a8a..cd21bb1bb31 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/node/extHostTreeViews.ts @@ -17,6 +17,7 @@ import { TreeItemCollapsibleState, ThemeIcon } from 'vs/workbench/api/node/extHo import { isUndefinedOrNull, isString } from 'vs/base/common/types'; import { equals } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; +import { IExtensionDescription, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; type TreeItemHandle = string; @@ -59,16 +60,20 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { }); } - registerTreeDataProvider(id: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { - const treeView = this.createTreeView(id, { treeDataProvider }); + registerTreeDataProvider(id: string, treeDataProvider: vscode.TreeDataProvider, extension: IExtensionDescription): vscode.Disposable { + const treeView = this.createTreeView(id, { treeDataProvider }, extension); return { dispose: () => treeView.dispose() }; } - createTreeView(viewId: string, options: { treeDataProvider: vscode.TreeDataProvider }): vscode.TreeView { + createTreeView(viewId: string, options: vscode.TreeViewOptions, extension: IExtensionDescription): vscode.TreeView { if (!options || !options.treeDataProvider) { throw new Error('Options with treeDataProvider is mandatory'); } - const treeView = this.createExtHostTreeViewer(viewId, options.treeDataProvider); + if (options.showCollapseAll) { + checkProposedApiEnabled(extension); + } + + const treeView = this.createExtHostTreeViewer(viewId, options); return { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, get onDidExpandElement() { return treeView.onDidExpandElement; }, @@ -118,8 +123,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { treeView.setVisible(isVisible); } - private createExtHostTreeViewer(id: string, dataProvider: vscode.TreeDataProvider): ExtHostTreeView { - const treeView = new ExtHostTreeView(id, dataProvider, this._proxy, this.commands.converter, this.logService); + private createExtHostTreeViewer(id: string, options: vscode.TreeViewOptions): ExtHostTreeView { + const treeView = new ExtHostTreeView(id, options, this._proxy, this.commands.converter, this.logService); this.treeViews.set(id, treeView); return treeView; } @@ -141,6 +146,8 @@ class ExtHostTreeView extends Disposable { private static LABEL_HANDLE_PREFIX = '0'; private static ID_HANDLE_PREFIX = '1'; + private readonly dataProvider: vscode.TreeDataProvider; + private roots: TreeNode[] | null = null; private elements: Map = new Map(); private nodes: Map = new Map(); @@ -165,9 +172,10 @@ class ExtHostTreeView extends Disposable { private refreshPromise: Promise = Promise.resolve(null); - constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService) { + constructor(private viewId: string, options: vscode.TreeViewOptions, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService) { super(); - this.proxy.$registerTreeViewDataProvider(viewId); + this.dataProvider = options.treeDataProvider; + this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll }); if (this.dataProvider.onDidChangeTreeData) { let refreshingPromise, promiseCallback; this._register(debounceEvent(this.dataProvider.onDidChangeTreeData, (last, current) => { diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 95cf224b7c1..abaa1b7eb73 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions'; +import { IAction, IActionItem, ActionRunner, Action } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -36,10 +36,10 @@ import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/v import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { localize } from 'vs/nls'; import { timeout } from 'vs/base/common/async'; +import { CollapseAllAction } from 'vs/base/parts/tree/browser/treeDefaults'; export class CustomTreeViewPanel extends ViewletPanel { - private menus: TitleMenus; private treeViewer: ITreeViewer; constructor( @@ -47,15 +47,14 @@ export class CustomTreeViewPanel extends ViewletPanel { @INotificationService private notificationService: INotificationService, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, - @IInstantiationService private instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IViewsService viewsService: IViewsService, ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); - this.treeViewer = (ViewsRegistry.getView(options.id)).treeViewer; + const { treeViewer } = (ViewsRegistry.getView(options.id)); + this.treeViewer = treeViewer; + this.treeViewer.onDidChangeActions(() => this.updateActions(), this, this.disposables); this.disposables.push(toDisposable(() => this.treeViewer.setVisibility(false))); - this.menus = this.instantiationService.createInstance(TitleMenus, this.id); - this.menus.onDidChangeTitle(() => this.updateActions(), this, this.disposables); this.updateTreeVisibility(); } @@ -83,11 +82,11 @@ export class CustomTreeViewPanel extends ViewletPanel { } getActions(): IAction[] { - return [...this.menus.getTitleActions()]; + return [...this.treeViewer.getPrimaryActions()]; } getSecondaryActions(): IAction[] { - return this.menus.getTitleSecondaryActions(); + return [...this.treeViewer.getSecondaryActions()]; } getActionItem(action: IAction): IActionItem { @@ -180,6 +179,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { private activated: boolean = false; private _hasIconForParentNode = false; private _hasIconForLeafNode = false; + private _showCollapseAllAction = false; private domNode: HTMLElement; private treeContainer: HTMLElement; @@ -187,6 +187,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { private tree: FileIconThemableWorkbenchTree; private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; + private menus: TitleMenus; private _dataProvider: ITreeViewDataProvider; @@ -202,6 +203,9 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { private _onDidChangeVisibility: Emitter = this._register(new Emitter()); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + private _onDidChangeActions: Emitter = this._register(new Emitter()); + readonly onDidChangeActions: Event = this._onDidChangeActions.event; + constructor( private id: string, private container: ViewContainer, @@ -214,6 +218,8 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { ) { super(); this.root = new Root(); + this.menus = this._register(this.instantiationService.createInstance(TitleMenus, this.id)); + this._register(this.menus.onDidChangeTitle(() => this._onDidChangeActions.fire())); this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.themeService.onThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -263,6 +269,30 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { return this.isVisible; } + get showCollapseAllAction(): boolean { + return this._showCollapseAllAction; + } + + set showCollapseAllAction(showCollapseAllAction: boolean) { + if (this._showCollapseAllAction !== !!showCollapseAllAction) { + this._showCollapseAllAction = !!showCollapseAllAction; + this._onDidChangeActions.fire(); + } + } + + getPrimaryActions(): IAction[] { + if (this.showCollapseAllAction) { + const collapseAllAction = new Action('vs.tree.collapse', localize('collapse', "Collapse"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); + return [...this.menus.getTitleActions(), collapseAllAction]; + } else { + return this.menus.getTitleActions(); + } + } + + getSecondaryActions(): IAction[] { + return this.menus.getTitleSecondaryActions(); + } + setVisibility(isVisible: boolean): void { isVisible = !!isVisible; if (this.isVisible === isVisible) { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index ab8d9857d80..3ee072af53b 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -17,6 +17,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { values } from 'vs/base/common/map'; import { Registry } from 'vs/platform/registry/common/platform'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IAction } from 'vs/base/common/actions'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; @@ -238,6 +239,10 @@ export interface ITreeViewer extends IDisposable { dataProvider: ITreeViewDataProvider; + showCollapseAllAction: boolean; + + readonly visible: boolean; + readonly onDidExpandItem: Event; readonly onDidCollapseItem: Event; @@ -246,7 +251,7 @@ export interface ITreeViewer extends IDisposable { readonly onDidChangeVisibility: Event; - readonly visible: boolean; + readonly onDidChangeActions: Event; refresh(treeItems?: ITreeItem[]): TPromise; @@ -261,6 +266,10 @@ export interface ITreeViewer extends IDisposable { getOptimalWidth(): number; reveal(item: ITreeItem, parentChain: ITreeItem[], options: { select?: boolean }): TPromise; + + getPrimaryActions(): IAction[]; + + getSecondaryActions(): IAction[]; } export interface ICustomViewDescriptor extends IViewDescriptor { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 20357f2c9b5..375a98a423c 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -206,3 +206,13 @@ export interface IExtensionService { export interface ProfileSession { stop(): TPromise; } + +export function checkProposedApiEnabled(extension: IExtensionDescription): void { + if (!extension.enableProposedApi) { + throwProposedApiError(extension); + } +} + +export function throwProposedApiError(extension: IExtensionDescription): never { + throw new Error(`[${extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.id}`); +} diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index ee9704fd064..5c77cd8ce46 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -72,9 +72,9 @@ suite('ExtHostTreeView', function () { testObject = new ExtHostTreeViews(target, new ExtHostCommands(rpcProtocol, new ExtHostHeapService(), new NullLogService()), new NullLogService()); onDidChangeTreeNode = new Emitter<{ key: string }>(); onDidChangeTreeNodeWithId = new Emitter<{ key: string }>(); - testObject.createTreeView('testNodeTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }); - testObject.createTreeView('testNodeWithIdTreeProvider', { treeDataProvider: aNodeWithIdTreeDataProvider() }); - testObject.createTreeView('testNodeWithHighlightsTreeProvider', { treeDataProvider: aNodeWithHighlightedLabelTreeDataProvider() }); + testObject.createTreeView('testNodeTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, null); + testObject.createTreeView('testNodeWithIdTreeProvider', { treeDataProvider: aNodeWithIdTreeDataProvider() }, null); + testObject.createTreeView('testNodeWithHighlightsTreeProvider', { treeDataProvider: aNodeWithHighlightedLabelTreeDataProvider() }, null); return loadCompleteTree('testNodeTreeProvider'); }); @@ -445,14 +445,14 @@ suite('ExtHostTreeView', function () { }); test('reveal will throw an error if getParent is not implemented', () => { - const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aNodeTreeDataProvider() }); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aNodeTreeDataProvider() }, null); return treeView.reveal({ key: 'a' }) .then(() => assert.fail('Reveal should throw an error as getParent is not implemented'), () => null); }); test('reveal will return empty array for root element', () => { const revealTarget = sinon.spy(target, '$reveal'); - const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null); return treeView.reveal({ key: 'a' }) .then(() => { assert.ok(revealTarget.calledOnce); @@ -465,7 +465,7 @@ suite('ExtHostTreeView', function () { test('reveal will return parents array for an element when hierarchy is not loaded', () => { const revealTarget = sinon.spy(target, '$reveal'); - const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null); return treeView.reveal({ key: 'aa' }) .then(() => { assert.ok(revealTarget.calledOnce); @@ -478,7 +478,7 @@ suite('ExtHostTreeView', function () { test('reveal will return parents array for an element when hierarchy is loaded', () => { const revealTarget = sinon.spy(target, '$reveal'); - const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null); return testObject.$getChildren('treeDataProvider') .then(() => testObject.$getChildren('treeDataProvider', '0/0:a')) .then(() => treeView.reveal({ key: 'aa' }) @@ -500,7 +500,7 @@ suite('ExtHostTreeView', function () { } }; const revealTarget = sinon.spy(target, '$reveal'); - const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null); return treeView.reveal({ key: 'bac' }, { select: false, focus: false }) .then(() => { assert.ok(revealTarget.calledOnce); @@ -516,7 +516,7 @@ suite('ExtHostTreeView', function () { test('reveal after first udpate', () => { const revealTarget = sinon.spy(target, '$reveal'); - const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null); return loadCompleteTree('treeDataProvider') .then(() => { tree = { @@ -544,7 +544,7 @@ suite('ExtHostTreeView', function () { test('reveal after second udpate', () => { const revealTarget = sinon.spy(target, '$reveal'); - const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); + const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }, null); return loadCompleteTree('treeDataProvider') .then(() => { tree = {