Skip to content

Commit 2d75d95

Browse files
authored
Merge pull request #14048 from octref/treeExplorerAPI
Declarative contribution of custom Tree Explorer
2 parents a28dc21 + a585a33 commit 2d75d95

29 files changed

+1214
-37
lines changed

src/vs/base/browser/ui/actionbar/actionbar.ts

+7
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,13 @@ export class ActionBar extends EventEmitter implements IActionRunner {
529529
});
530530
}
531531

532+
public pull(index: number): void {
533+
if (index >= 0 && index < this.items.length) {
534+
this.items.splice(index, 1);
535+
this.actionsList.removeChild(this.actionsList.childNodes[index]);
536+
}
537+
}
538+
532539
public clear(): void {
533540
// Do not dispose action items if they were provided from outside
534541
this.items = this.options.actionItemProvider ? [] : lifecycle.dispose(this.items);

src/vs/platform/extensionManagement/common/extensionManagement.ts

+7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ export interface ITheme {
7575
label: string;
7676
}
7777

78+
export interface ITreeExplorer {
79+
treeExplorerNodeProviderId: string;
80+
treeLabel: string;
81+
icon: string;
82+
}
83+
7884
export interface IExtensionContributions {
7985
commands?: ICommand[];
8086
configuration?: IConfiguration;
@@ -86,6 +92,7 @@ export interface IExtensionContributions {
8692
menus?: { [context: string]: IMenu[] };
8793
snippets?: ISnippet[];
8894
themes?: ITheme[];
95+
explorer?: ITreeExplorer;
8996
}
9097

9198
export interface IExtensionManifest {

src/vs/vscode.proposed.d.ts

+75-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,78 @@ declare module 'vscode' {
1111

1212
export function sampleFunction(): Thenable<any>;
1313
}
14-
}
14+
15+
export namespace workspace {
16+
17+
/**
18+
* Register a [TreeExplorerNodeProvider](#TreeExplorerNodeProvider).
19+
*
20+
* @param providerId A unique id that identifies the provider.
21+
* @param provider A [TreeExplorerNodeProvider](#TreeExplorerNodeProvider).
22+
* @return A [disposable](#Disposable) that unregisters this provider when being disposed.
23+
*/
24+
export function registerTreeExplorerNodeProvider(providerId: string, provider: TreeExplorerNodeProvider<any>): Disposable;
25+
}
26+
27+
/**
28+
* A node provider for a tree explorer contribution.
29+
*
30+
* Providers are registered through (#workspace.registerTreeExplorerNodeProvider) with a
31+
* `providerId` that corresponds to the `treeExplorerNodeProviderId` in the extension's
32+
* `contributes.explorer` section.
33+
*
34+
* The contributed tree explorer will ask the corresponding provider to provide the root
35+
* node and resolve children for each node. In addition, the provider could **optionally**
36+
* provide the following information for each node:
37+
* - label: A human-readable label used for rendering the node.
38+
* - hasChildren: Whether the node has children and is expandable.
39+
* - clickCommand: A command to execute when the node is clicked.
40+
*/
41+
export interface TreeExplorerNodeProvider<T> {
42+
43+
/**
44+
* Provide the root node. This function will be called when the tree explorer is activated
45+
* for the first time. The root node is hidden and its direct children will be displayed on the first level of
46+
* the tree explorer.
47+
*
48+
* @return The root node.
49+
*/
50+
provideRootNode(): T | Thenable<T>;
51+
52+
/**
53+
* Resolve the children of `node`.
54+
*
55+
* @param node The node from which the provider resolves children.
56+
* @return Children of `node`.
57+
*/
58+
resolveChildren(node: T): T[] | Thenable<T[]>;
59+
60+
/**
61+
* Provide a human-readable string that will be used for rendering the node. Default to use
62+
* `node.toString()` if not provided.
63+
*
64+
* @param node The node from which the provider computes label.
65+
* @return A human-readable label.
66+
*/
67+
getLabel?(node: T): string;
68+
69+
/**
70+
* Determine if `node` has children and is expandable. Default to `true` if not provided.
71+
*
72+
* @param node The node to determine if it has children and is expandable.
73+
* @return A boolean that determines if `node` has children and is expandable.
74+
*/
75+
getHasChildren?(node: T): boolean;
76+
77+
/**
78+
* Get the command to execute when `node` is clicked.
79+
*
80+
* Commands can be registered through [registerCommand](#commands.registerCommand). `node` will be provided
81+
* as the first argument to the command's callback function.
82+
*
83+
* @param node The node that the command is associated with.
84+
* @return The command to execute when `node` is clicked.
85+
*/
86+
getClickCommand?(node: T): string;
87+
}
88+
}

src/vs/workbench/api/node/extHost.api.impl.ts

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments';
1818
import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant';
1919
import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration';
2020
import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics';
21+
import { ExtHostTreeExplorers } from 'vs/workbench/api/node/extHostTreeExplorers';
2122
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
2223
import { ExtHostQuickOpen } from 'vs/workbench/api/node/extHostQuickOpen';
2324
import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService';
@@ -73,6 +74,7 @@ export function createApiFactory(initDataConfiguration: WorkspaceConfigurationNo
7374
const extHostDocumentSaveParticipant = col.define(ExtHostContext.ExtHostDocumentSaveParticipant).set<ExtHostDocumentSaveParticipant>(new ExtHostDocumentSaveParticipant(extHostDocuments, threadService.get(MainContext.MainThreadWorkspace)));
7475
const extHostEditors = col.define(ExtHostContext.ExtHostEditors).set<ExtHostEditors>(new ExtHostEditors(threadService, extHostDocuments));
7576
const extHostCommands = col.define(ExtHostContext.ExtHostCommands).set<ExtHostCommands>(new ExtHostCommands(threadService, extHostEditors, extHostHeapService));
77+
const extHostExplorers = col.define(ExtHostContext.ExtHostExplorers).set<ExtHostTreeExplorers>(new ExtHostTreeExplorers(threadService, extHostCommands));
7678
const extHostConfiguration = col.define(ExtHostContext.ExtHostConfiguration).set<ExtHostConfiguration>(new ExtHostConfiguration(threadService.get(MainContext.MainThreadConfiguration), initDataConfiguration));
7779
const extHostDiagnostics = col.define(ExtHostContext.ExtHostDiagnostics).set<ExtHostDiagnostics>(new ExtHostDiagnostics(threadService));
7880
const languageFeatures = col.define(ExtHostContext.ExtHostLanguageFeatures).set<ExtHostLanguageFeatures>(new ExtHostLanguageFeatures(threadService, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics));
@@ -341,6 +343,9 @@ export function createApiFactory(initDataConfiguration: WorkspaceConfigurationNo
341343
onWillSaveTextDocument: (listener, thisArgs?, disposables?) => {
342344
return extHostDocumentSaveParticipant.onWillSaveTextDocumentEvent(listener, thisArgs, disposables);
343345
},
346+
registerTreeExplorerNodeProvider: proposedApiFunction(extension, (providerId: string, provider: vscode.TreeExplorerNodeProvider<any>) => {
347+
return extHostExplorers.registerTreeExplorerNodeProvider(providerId, provider);
348+
}),
344349
onDidChangeConfiguration: (listener: () => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => {
345350
return extHostConfiguration.onDidChangeConfiguration(listener, thisArgs, disposables);
346351
},

src/vs/workbench/api/node/extHost.contribution.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { MainThreadDiagnostics } from './mainThreadDiagnostics';
2020
import { MainThreadDocuments } from './mainThreadDocuments';
2121
import { MainThreadEditors } from './mainThreadEditors';
2222
import { MainThreadErrors } from './mainThreadErrors';
23+
import { MainThreadTreeExplorers } from './mainThreadTreeExplorers';
2324
import { MainThreadLanguageFeatures } from './mainThreadLanguageFeatures';
2425
import { MainThreadLanguages } from './mainThreadLanguages';
2526
import { MainThreadMessageService } from './mainThreadMessageService';
@@ -70,6 +71,7 @@ export class ExtHostContribution implements IWorkbenchContribution {
7071
col.define(MainContext.MainThreadDocuments).set(create(MainThreadDocuments));
7172
col.define(MainContext.MainThreadEditors).set(create(MainThreadEditors));
7273
col.define(MainContext.MainThreadErrors).set(create(MainThreadErrors));
74+
col.define(MainContext.MainThreadExplorers).set(create(MainThreadTreeExplorers));
7375
col.define(MainContext.MainThreadLanguageFeatures).set(create(MainThreadLanguageFeatures));
7476
col.define(MainContext.MainThreadLanguages).set(create(MainThreadLanguages));
7577
col.define(MainContext.MainThreadMessageService).set(create(MainThreadMessageService));

src/vs/workbench/api/node/extHost.protocol.ts

+14
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
3636
import { IWorkspaceSymbol } from 'vs/workbench/parts/search/common/search';
3737
import { IApplyEditsOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditorsTracker';
3838

39+
import { InternalTreeExplorerNodeContent } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel';
40+
3941
export interface IEnvironment {
4042
appSettingsHome: string;
4143
disableExtensions: boolean;
@@ -136,6 +138,10 @@ export abstract class MainThreadEditorsShape {
136138
$tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise<boolean> { throw ni(); }
137139
}
138140

141+
export abstract class MainThreadTreeExplorersShape {
142+
$registerTreeExplorerNodeProvider(providerId: string): void { throw ni(); }
143+
}
144+
139145
export abstract class MainThreadErrorsShape {
140146
onUnexpectedExtHostError(err: any): void { throw ni(); }
141147
}
@@ -277,6 +283,12 @@ export abstract class ExtHostEditorsShape {
277283
$acceptTextEditorRemove(id: string): void { throw ni(); }
278284
}
279285

286+
export abstract class ExtHostTreeExplorersShape {
287+
$provideRootNode(providerId: string): TPromise<InternalTreeExplorerNodeContent> { throw ni(); };
288+
$resolveChildren(providerId: string, node: InternalTreeExplorerNodeContent): TPromise<InternalTreeExplorerNodeContent[]> { throw ni(); }
289+
$getInternalCommand(providerId: string, node: InternalTreeExplorerNodeContent): TPromise<modes.Command> { throw ni(); }
290+
}
291+
280292
export abstract class ExtHostExtensionServiceShape {
281293
$localShowMessage(severity: Severity, msg: string): void { throw ni(); }
282294
$activateExtension(extensionDescription: IExtensionDescription): TPromise<void> { throw ni(); }
@@ -351,6 +363,7 @@ export const MainContext = {
351363
MainThreadDocuments: createMainId<MainThreadDocumentsShape>('MainThreadDocuments', MainThreadDocumentsShape),
352364
MainThreadEditors: createMainId<MainThreadEditorsShape>('MainThreadEditors', MainThreadEditorsShape),
353365
MainThreadErrors: createMainId<MainThreadErrorsShape>('MainThreadErrors', MainThreadErrorsShape),
366+
MainThreadExplorers: createMainId<MainThreadTreeExplorersShape>('MainThreadExplorers', MainThreadTreeExplorersShape),
354367
MainThreadLanguageFeatures: createMainId<MainThreadLanguageFeaturesShape>('MainThreadLanguageFeatures', MainThreadLanguageFeaturesShape),
355368
MainThreadLanguages: createMainId<MainThreadLanguagesShape>('MainThreadLanguages', MainThreadLanguagesShape),
356369
MainThreadMessageService: createMainId<MainThreadMessageServiceShape>('MainThreadMessageService', MainThreadMessageServiceShape),
@@ -371,6 +384,7 @@ export const ExtHostContext = {
371384
ExtHostDocuments: createExtId<ExtHostDocumentsShape>('ExtHostDocuments', ExtHostDocumentsShape),
372385
ExtHostDocumentSaveParticipant: createExtId<ExtHostDocumentSaveParticipantShape>('ExtHostDocumentSaveParticipant', ExtHostDocumentSaveParticipantShape),
373386
ExtHostEditors: createExtId<ExtHostEditorsShape>('ExtHostEditors', ExtHostEditorsShape),
387+
ExtHostExplorers: createExtId<ExtHostTreeExplorersShape>('ExtHostExplorers', ExtHostTreeExplorersShape),
374388
ExtHostFileSystemEventService: createExtId<ExtHostFileSystemEventServiceShape>('ExtHostFileSystemEventService', ExtHostFileSystemEventServiceShape),
375389
ExtHostHeapService: createExtId<ExtHostHeapServiceShape>('ExtHostHeapMonitor', ExtHostHeapServiceShape),
376390
ExtHostLanguageFeatures: createExtId<ExtHostLanguageFeaturesShape>('ExtHostLanguageFeatures', ExtHostLanguageFeaturesShape),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import { localize } from 'vs/nls';
8+
import { TreeExplorerNodeProvider } from 'vscode';
9+
import { TPromise } from 'vs/base/common/winjs.base';
10+
import { Disposable } from 'vs/workbench/api/node/extHostTypes';
11+
import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
12+
import { MainContext, ExtHostTreeExplorersShape, MainThreadTreeExplorersShape } from './extHost.protocol';
13+
import { InternalTreeExplorerNode } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel';
14+
import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands';
15+
import { asWinJsPromise } from 'vs/base/common/async';
16+
import * as modes from 'vs/editor/common/modes';
17+
18+
export class ExtHostTreeExplorers extends ExtHostTreeExplorersShape {
19+
private _proxy: MainThreadTreeExplorersShape;
20+
21+
private _extNodeProviders: { [providerId: string]: TreeExplorerNodeProvider<any> };
22+
private _extNodeMaps: { [providerId: string]: { [id: number]: any } };
23+
24+
constructor(
25+
threadService: IThreadService,
26+
private commands: ExtHostCommands
27+
) {
28+
super();
29+
30+
this._proxy = threadService.get(MainContext.MainThreadExplorers);
31+
32+
this._extNodeProviders = Object.create(null);
33+
this._extNodeMaps = Object.create(null);
34+
}
35+
36+
registerTreeExplorerNodeProvider(providerId: string, provider: TreeExplorerNodeProvider<any>): Disposable {
37+
this._proxy.$registerTreeExplorerNodeProvider(providerId);
38+
this._extNodeProviders[providerId] = provider;
39+
40+
return new Disposable(() => {
41+
delete this._extNodeProviders[providerId];
42+
delete this._extNodeProviders[providerId];
43+
});
44+
}
45+
46+
$provideRootNode(providerId: string): TPromise<InternalTreeExplorerNode> {
47+
const provider = this._extNodeProviders[providerId];
48+
if (!provider) {
49+
const errMessage = localize('treeExplorer.notRegistered', 'No TreeExplorerNodeProvider with id \'{0}\' registered.', providerId);
50+
return TPromise.wrapError(errMessage);
51+
}
52+
53+
return asWinJsPromise(() => provider.provideRootNode()).then(extRootNode => {
54+
const extNodeMap = Object.create(null);
55+
const internalRootNode = new InternalTreeExplorerNode(extRootNode, provider);
56+
57+
extNodeMap[internalRootNode.id] = extRootNode;
58+
this._extNodeMaps[providerId] = extNodeMap;
59+
60+
return internalRootNode;
61+
}, err => {
62+
const errMessage = localize('treeExplorer.failedToProvideRootNode', 'TreeExplorerNodeProvider \'{0}\' failed to provide root node.', providerId);
63+
return TPromise.wrapError(errMessage);
64+
});
65+
}
66+
67+
$resolveChildren(providerId: string, mainThreadNode: InternalTreeExplorerNode): TPromise<InternalTreeExplorerNode[]> {
68+
const provider = this._extNodeProviders[providerId];
69+
if (!provider) {
70+
const errMessage = localize('treeExplorer.notRegistered', 'No TreeExplorerNodeProvider with id \'{0}\' registered.', providerId);
71+
return TPromise.wrapError(errMessage);
72+
}
73+
74+
const extNodeMap = this._extNodeMaps[providerId];
75+
const extNode = extNodeMap[mainThreadNode.id];
76+
77+
return asWinJsPromise(() => provider.resolveChildren(extNode)).then(children => {
78+
return children.map(extChild => {
79+
const internalChild = new InternalTreeExplorerNode(extChild, provider);
80+
extNodeMap[internalChild.id] = extChild;
81+
return internalChild;
82+
});
83+
}, err => {
84+
const errMessage = localize('treeExplorer.failedToResolveChildren', 'TreeExplorerNodeProvider \'{0}\' failed to resolveChildren.', providerId);
85+
return TPromise.wrapError(errMessage);
86+
});
87+
}
88+
89+
// Convert the command on the ExtHost side so we can pass the original externalNode to the registered handler
90+
$getInternalCommand(providerId: string, mainThreadNode: InternalTreeExplorerNode): TPromise<modes.Command> {
91+
const commandConverter = this.commands.converter;
92+
93+
if (mainThreadNode.clickCommand) {
94+
const extNode = this._extNodeMaps[providerId][mainThreadNode.id];
95+
96+
const internalCommand = commandConverter.toInternal({
97+
title: '',
98+
command: mainThreadNode.clickCommand,
99+
arguments: [extNode]
100+
});
101+
102+
return TPromise.wrap(internalCommand);
103+
}
104+
105+
return TPromise.as(null);
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import { TPromise } from 'vs/base/common/winjs.base';
8+
import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
9+
import { ExtHostContext, MainThreadTreeExplorersShape, ExtHostTreeExplorersShape } from './extHost.protocol';
10+
import { ICustomTreeExplorerService } from 'vs/workbench/parts/explorers/browser/customTreeExplorerService';
11+
import { InternalTreeExplorerNodeContent } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel';
12+
import { IMessageService, Severity } from 'vs/platform/message/common/message';
13+
import { ICommandService } from 'vs/platform/commands/common/commands';
14+
15+
export class MainThreadTreeExplorers extends MainThreadTreeExplorersShape {
16+
private _proxy: ExtHostTreeExplorersShape;
17+
18+
constructor(
19+
@IThreadService private threadService: IThreadService,
20+
@ICustomTreeExplorerService private treeExplorerService: ICustomTreeExplorerService,
21+
@IMessageService private messageService: IMessageService,
22+
@ICommandService private commandService: ICommandService
23+
) {
24+
super();
25+
26+
this._proxy = threadService.get(ExtHostContext.ExtHostExplorers);
27+
}
28+
29+
$registerTreeExplorerNodeProvider(providerId: string): void {
30+
const onError = err => { this.messageService.show(Severity.Error, err); };
31+
32+
this.treeExplorerService.registerTreeExplorerNodeProvider(providerId, {
33+
provideRootNode: (): TPromise<InternalTreeExplorerNodeContent> => {
34+
return this._proxy.$provideRootNode(providerId).then(rootNode => rootNode, onError);
35+
},
36+
resolveChildren: (node: InternalTreeExplorerNodeContent): TPromise<InternalTreeExplorerNodeContent[]> => {
37+
return this._proxy.$resolveChildren(providerId, node).then(children => children, onError);
38+
},
39+
executeCommand: (node: InternalTreeExplorerNodeContent): TPromise<any> => {
40+
return this._proxy.$getInternalCommand(providerId, node).then(command => {
41+
return this.commandService.executeCommand(command.id, ...command.arguments);
42+
});
43+
}
44+
});
45+
}
46+
}

0 commit comments

Comments
 (0)