-
Notifications
You must be signed in to change notification settings - Fork 30.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Declarative contribution of custom Tree Explorer #14048
Changes from 94 commits
2657a9d
aa1ffcd
c2bdd6a
d03ea81
d8c0af7
5276b19
8081e2e
10af4d7
6f4de8b
61639c0
c8bbaaa
f364538
154cdf9
ec1cc0d
e88e3f0
373829a
7f5e03f
230ac46
eced815
8e9982c
fb032eb
29c03ea
f366857
09703d2
c7f2d3d
30e3be2
b8ce9ad
668aa65
f660ca6
f659fdd
6bb8278
b474369
0cc3fec
c24338d
af5af72
6300fbf
13d5690
4eab0df
944101c
1c28702
940a7fa
2e0f8d0
1364159
88e869a
0031078
0471662
5b1ba3c
31afc03
09eebc5
857411d
0c18979
045aedd
f19446e
ec7f077
7750596
c49e7bc
7642976
b654b53
4cc76f5
16dd5f9
313df77
b141507
6fd1c54
267aab6
6e868d6
a5adf50
d579cb8
0b84a56
37398ea
bc573a7
0e9aecd
60574ba
a21ac50
39ecfd3
e2f4a6b
794ad90
8f777ab
4ed359a
a3d577c
a196d7f
70be684
f5d4ede
09c11fe
f18fa6e
c476937
31960a7
84dd43b
a52cfbb
d153d96
894c3cf
355f115
1ed2912
68284bb
a04f317
4660455
50eb471
3870173
a6f8b94
3b5e3ea
9cf01e8
b205a4c
1cdbe70
13f2dad
5342705
65f43cc
347c2e9
ff63a4d
11486b8
29346ad
307037c
15bf2ec
e2b7b52
6793e08
c57973f
85f4204
0b15cbb
b9cf15d
902137c
06b82f5
4d25517
dd19d59
d111426
25885c6
80a0cbf
ae720e9
a585a33
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,8 @@ import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; | |
import { IWorkspaceSymbol } from 'vs/workbench/parts/search/common/search'; | ||
import { IApplyEditsOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditorsTracker'; | ||
|
||
import { InternalTreeExplorerNodeContent } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; | ||
|
||
export interface IEnvironment { | ||
appSettingsHome: string; | ||
disableExtensions: boolean; | ||
|
@@ -139,6 +141,10 @@ export abstract class MainThreadEditorsShape { | |
$tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise<boolean> { throw ni(); } | ||
} | ||
|
||
export abstract class MainThreadTreeExplorersShape { | ||
$registerTreeExplorerNodeProvider(providerId: string): void { throw ni(); } | ||
} | ||
|
||
export abstract class MainThreadErrorsShape { | ||
onUnexpectedExtHostError(err: any): void { throw ni(); } | ||
} | ||
|
@@ -280,6 +286,12 @@ export abstract class ExtHostEditorsShape { | |
$acceptTextEditorRemove(id: string): void { throw ni(); } | ||
} | ||
|
||
export abstract class ExtHostTreeExplorersShape { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand that but the practice is to be explicit about API and implementation. The protocol is data-driven and therefore we are explicit about only using primitive types and plain (data) interfaces that map directly to JSON. This is more a convention thing than a problem There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops sorry. Actually I designed with this in mind and should've used |
||
$provideRootNode(providerId: string): TPromise<InternalTreeExplorerNodeContent> { throw ni(); }; | ||
$resolveChildren(providerId: string, node: InternalTreeExplorerNodeContent): TPromise<InternalTreeExplorerNodeContent[]> { throw ni(); } | ||
$getInternalCommand(providerId: string, node: InternalTreeExplorerNodeContent): TPromise<modes.Command> { throw ni(); } | ||
} | ||
|
||
export abstract class ExtHostExtensionServiceShape { | ||
$localShowMessage(severity: Severity, msg: string): void { throw ni(); } | ||
$activateExtension(extensionDescription: IExtensionDescription): TPromise<void> { throw ni(); } | ||
|
@@ -354,6 +366,7 @@ export const MainContext = { | |
MainThreadDocuments: createMainId<MainThreadDocumentsShape>('MainThreadDocuments', MainThreadDocumentsShape), | ||
MainThreadEditors: createMainId<MainThreadEditorsShape>('MainThreadEditors', MainThreadEditorsShape), | ||
MainThreadErrors: createMainId<MainThreadErrorsShape>('MainThreadErrors', MainThreadErrorsShape), | ||
MainThreadExplorers: createMainId<MainThreadTreeExplorersShape>('MainThreadExplorers', MainThreadTreeExplorersShape), | ||
MainThreadLanguageFeatures: createMainId<MainThreadLanguageFeaturesShape>('MainThreadLanguageFeatures', MainThreadLanguageFeaturesShape), | ||
MainThreadLanguages: createMainId<MainThreadLanguagesShape>('MainThreadLanguages', MainThreadLanguagesShape), | ||
MainThreadMessageService: createMainId<MainThreadMessageServiceShape>('MainThreadMessageService', MainThreadMessageServiceShape), | ||
|
@@ -374,6 +387,7 @@ export const ExtHostContext = { | |
ExtHostDocuments: createExtId<ExtHostDocumentsShape>('ExtHostDocuments', ExtHostDocumentsShape), | ||
ExtHostDocumentSaveParticipant: createExtId<ExtHostDocumentSaveParticipantShape>('ExtHostDocumentSaveParticipant', ExtHostDocumentSaveParticipantShape), | ||
ExtHostEditors: createExtId<ExtHostEditorsShape>('ExtHostEditors', ExtHostEditorsShape), | ||
ExtHostExplorers: createExtId<ExtHostTreeExplorersShape>('ExtHostExplorers', ExtHostTreeExplorersShape), | ||
ExtHostFileSystemEventService: createExtId<ExtHostFileSystemEventServiceShape>('ExtHostFileSystemEventService', ExtHostFileSystemEventServiceShape), | ||
ExtHostHeapService: createExtId<ExtHostHeapServiceShape>('ExtHostHeapMonitor', ExtHostHeapServiceShape), | ||
ExtHostLanguageFeatures: createExtId<ExtHostLanguageFeaturesShape>('ExtHostLanguageFeatures', ExtHostLanguageFeaturesShape), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
'use strict'; | ||
|
||
import { TreeExplorerNodeProvider } from 'vscode'; | ||
import { TPromise } from 'vs/base/common/winjs.base'; | ||
import { Disposable } from 'vs/workbench/api/node/extHostTypes'; | ||
import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; | ||
import { MainContext, ExtHostTreeExplorersShape, MainThreadTreeExplorersShape } from './extHost.protocol'; | ||
import { InternalTreeExplorerNode } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; | ||
import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; | ||
import { asWinJsPromise } from 'vs/base/common/async'; | ||
import * as modes from 'vs/editor/common/modes'; | ||
|
||
export class ExtHostTreeExplorers extends ExtHostTreeExplorersShape { | ||
private _proxy: MainThreadTreeExplorersShape; | ||
|
||
private _treeExplorerNodeProviders: { [providerId: string]: TreeExplorerNodeProvider<any> }; | ||
private _externalNodeMaps: { [providerId: string]: { [id: number]: any } }; | ||
|
||
constructor( | ||
threadService: IThreadService, | ||
private commands: ExtHostCommands | ||
) { | ||
super(); | ||
|
||
this._proxy = threadService.get(MainContext.MainThreadExplorers); | ||
|
||
this._treeExplorerNodeProviders = Object.create(null); | ||
this._externalNodeMaps = Object.create(null); | ||
} | ||
|
||
registerTreeExplorerNodeProvider(providerId: string, provider: TreeExplorerNodeProvider<any>): Disposable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw
So I just followed the pattern. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both types are equal - |
||
this._proxy.$registerTreeExplorerNodeProvider(providerId); | ||
this._treeExplorerNodeProviders[providerId] = provider; | ||
|
||
return new Disposable(() => { | ||
delete this._treeExplorerNodeProviders[providerId]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't you need to clean up |
||
delete this._treeExplorerNodeProviders[providerId]; | ||
}); | ||
} | ||
|
||
$provideRootNode(providerId: string): TPromise<InternalTreeExplorerNode> { | ||
const provider = this._treeExplorerNodeProviders[providerId]; | ||
if (!provider) { | ||
return TPromise.wrapError(`No TreeExplorerNodeProvider with id '${providerId}' registered.`); | ||
} | ||
|
||
return asWinJsPromise(() => provider.provideRootNode()).then(externalRootNode => { | ||
const treeNodeMap = Object.create(null); | ||
this._externalNodeMaps[providerId] = treeNodeMap; | ||
|
||
const internalRootNode = new InternalTreeExplorerNode(externalRootNode, provider); | ||
this._externalNodeMaps[providerId][internalRootNode.id] = externalRootNode; | ||
return internalRootNode; | ||
}, err => { | ||
return TPromise.wrapError(`TreeExplorerNodeProvider '${providerId}' failed to provide root node.`); | ||
}); | ||
} | ||
|
||
$resolveChildren(providerId: string, mainThreadNode: InternalTreeExplorerNode): TPromise<InternalTreeExplorerNode[]> { | ||
const provider = this._treeExplorerNodeProviders[providerId]; | ||
if (!provider) { | ||
return TPromise.wrapError(`No TreeExplorerNodeProvider with id '${providerId}' registered.`); | ||
} | ||
|
||
const externalNodeMap = this._externalNodeMaps[providerId]; | ||
const externalNode = externalNodeMap[mainThreadNode.id]; | ||
|
||
return asWinJsPromise(() => provider.resolveChildren(externalNode)).then(children => { | ||
return children.map(externalChild => { | ||
const internalChild = new InternalTreeExplorerNode(externalChild, provider); | ||
externalNodeMap[internalChild.id] = externalChild; | ||
return internalChild; | ||
}); | ||
}, err => { | ||
return TPromise.wrapError(`TreeExplorerNodeProvider '${providerId}' failed to resolve children.`); | ||
}); | ||
} | ||
|
||
// Convert the command on the ExtHost side so we can pass the original externalNode to the registered handler | ||
$getInternalCommand(providerId: string, mainThreadNode: InternalTreeExplorerNode): TPromise<modes.Command> { | ||
const commandConverter = this.commands.converter; | ||
|
||
if (mainThreadNode.clickCommand) { | ||
const externalNode = this._externalNodeMaps[providerId][mainThreadNode.id]; | ||
|
||
const internalCommand = commandConverter.toInternal({ | ||
title: '', | ||
command: mainThreadNode.clickCommand, | ||
arguments: [externalNode] | ||
}); | ||
|
||
return TPromise.wrap(internalCommand); | ||
} | ||
|
||
return TPromise.as(null); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
'use strict'; | ||
|
||
import { TPromise } from 'vs/base/common/winjs.base'; | ||
import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; | ||
import { ExtHostContext, MainThreadTreeExplorersShape, ExtHostTreeExplorersShape } from './extHost.protocol'; | ||
import { ITreeExplorerViewletService } from 'vs/workbench/parts/explorers/browser/treeExplorerViewletService'; | ||
import { InternalTreeExplorerNodeContent } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; | ||
import { IMessageService, Severity } from 'vs/platform/message/common/message'; | ||
import { ICommandService } from 'vs/platform/commands/common/commands'; | ||
|
||
export class MainThreadTreeExplorers extends MainThreadTreeExplorersShape { | ||
private _proxy: ExtHostTreeExplorersShape; | ||
|
||
constructor( | ||
@IThreadService private threadService: IThreadService, | ||
@ITreeExplorerViewletService private treeExplorerService: ITreeExplorerViewletService, | ||
@IMessageService private messageService: IMessageService, | ||
@ICommandService private commandService: ICommandService | ||
) { | ||
super(); | ||
|
||
this._proxy = threadService.get(ExtHostContext.ExtHostExplorers); | ||
} | ||
|
||
$registerTreeExplorerNodeProvider(providerId: string): void { | ||
const onError = err => { this.messageService.show(Severity.Error, err); }; | ||
|
||
this.treeExplorerService.registerTreeExplorerNodeProvider(providerId, { | ||
provideRootNode: (): TPromise<InternalTreeExplorerNodeContent> => { | ||
return this._proxy.$provideRootNode(providerId).then(rootNode => rootNode, onError); | ||
}, | ||
resolveChildren: (node: InternalTreeExplorerNodeContent): TPromise<InternalTreeExplorerNodeContent[]> => { | ||
return this._proxy.$resolveChildren(providerId, node).then(children => children, onError); | ||
}, | ||
executeCommand: (node: InternalTreeExplorerNodeContent): TPromise<any> => { | ||
return this._proxy.$getInternalCommand(providerId, node).then(command => { | ||
return this.commandService.executeCommand(command.id, ...command.arguments); | ||
}); | ||
} | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only one tree per extension?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to start with this, because I don't know which path we'll take if we were to allow more complex explorer contribution in the future.
Allow multiple sections (tree or non-tree) like debug
Allow multiple explorers per extenseion
Better to keep it minimal for first iteration I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[needs thinking] The problem is that this means no extension can ever add more than one tree-viewlet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if we change this part later, it would be an easy update from extension side. I found use cases for multiple viewlets less unlikely than a single multi-section viewlet.