diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index f2e40528c9c..da4f851b097 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -529,6 +529,13 @@ export class ActionBar extends EventEmitter implements IActionRunner { }); } + public pull(index: number): void { + if (index >= 0 && index < this.items.length) { + this.items.splice(index, 1); + this.actionsList.removeChild(this.actionsList.childNodes[index]); + } + } + public clear(): void { // Do not dispose action items if they were provided from outside this.items = this.options.actionItemProvider ? [] : lifecycle.dispose(this.items); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 411d199be38..414e7c597fe 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -75,6 +75,12 @@ export interface ITheme { label: string; } +export interface ITreeExplorer { + treeExplorerNodeProviderId: string; + treeLabel: string; + icon: string; +} + export interface IExtensionContributions { commands?: ICommand[]; configuration?: IConfiguration; @@ -86,6 +92,7 @@ export interface IExtensionContributions { menus?: { [context: string]: IMenu[] }; snippets?: ISnippet[]; themes?: ITheme[]; + explorer?: ITreeExplorer; } export interface IExtensionManifest { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 08e11e65f46..e881ebc4a0e 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -11,4 +11,78 @@ declare module 'vscode' { export function sampleFunction(): Thenable; } -} \ No newline at end of file + + export namespace workspace { + + /** + * Register a [TreeExplorerNodeProvider](#TreeExplorerNodeProvider). + * + * @param providerId A unique id that identifies the provider. + * @param provider A [TreeExplorerNodeProvider](#TreeExplorerNodeProvider). + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerTreeExplorerNodeProvider(providerId: string, provider: TreeExplorerNodeProvider): Disposable; + } + + /** + * A node provider for a tree explorer contribution. + * + * Providers are registered through (#workspace.registerTreeExplorerNodeProvider) with a + * `providerId` that corresponds to the `treeExplorerNodeProviderId` in the extension's + * `contributes.explorer` section. + * + * The contributed tree explorer will ask the corresponding provider to provide the root + * node and resolve children for each node. In addition, the provider could **optionally** + * provide the following information for each node: + * - label: A human-readable label used for rendering the node. + * - hasChildren: Whether the node has children and is expandable. + * - clickCommand: A command to execute when the node is clicked. + */ + export interface TreeExplorerNodeProvider { + + /** + * Provide the root node. This function will be called when the tree explorer is activated + * for the first time. The root node is hidden and its direct children will be displayed on the first level of + * the tree explorer. + * + * @return The root node. + */ + provideRootNode(): T | Thenable; + + /** + * Resolve the children of `node`. + * + * @param node The node from which the provider resolves children. + * @return Children of `node`. + */ + resolveChildren(node: T): T[] | Thenable; + + /** + * Provide a human-readable string that will be used for rendering the node. Default to use + * `node.toString()` if not provided. + * + * @param node The node from which the provider computes label. + * @return A human-readable label. + */ + getLabel?(node: T): string; + + /** + * Determine if `node` has children and is expandable. Default to `true` if not provided. + * + * @param node The node to determine if it has children and is expandable. + * @return A boolean that determines if `node` has children and is expandable. + */ + getHasChildren?(node: T): boolean; + + /** + * Get the command to execute when `node` is clicked. + * + * Commands can be registered through [registerCommand](#commands.registerCommand). `node` will be provided + * as the first argument to the command's callback function. + * + * @param node The node that the command is associated with. + * @return The command to execute when `node` is clicked. + */ + getClickCommand?(node: T): string; + } +} diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 366e8113ed1..17b0a0d32bf 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -18,6 +18,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; +import { ExtHostTreeExplorers } from 'vs/workbench/api/node/extHostTreeExplorers'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { ExtHostQuickOpen } from 'vs/workbench/api/node/extHostQuickOpen'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; @@ -73,6 +74,7 @@ export function createApiFactory(initDataConfiguration: WorkspaceConfigurationNo const extHostDocumentSaveParticipant = col.define(ExtHostContext.ExtHostDocumentSaveParticipant).set(new ExtHostDocumentSaveParticipant(extHostDocuments, threadService.get(MainContext.MainThreadWorkspace))); const extHostEditors = col.define(ExtHostContext.ExtHostEditors).set(new ExtHostEditors(threadService, extHostDocuments)); const extHostCommands = col.define(ExtHostContext.ExtHostCommands).set(new ExtHostCommands(threadService, extHostEditors, extHostHeapService)); + const extHostExplorers = col.define(ExtHostContext.ExtHostExplorers).set(new ExtHostTreeExplorers(threadService, extHostCommands)); const extHostConfiguration = col.define(ExtHostContext.ExtHostConfiguration).set(new ExtHostConfiguration(threadService.get(MainContext.MainThreadConfiguration), initDataConfiguration)); const extHostDiagnostics = col.define(ExtHostContext.ExtHostDiagnostics).set(new ExtHostDiagnostics(threadService)); const languageFeatures = col.define(ExtHostContext.ExtHostLanguageFeatures).set(new ExtHostLanguageFeatures(threadService, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics)); @@ -341,6 +343,9 @@ export function createApiFactory(initDataConfiguration: WorkspaceConfigurationNo onWillSaveTextDocument: (listener, thisArgs?, disposables?) => { return extHostDocumentSaveParticipant.onWillSaveTextDocumentEvent(listener, thisArgs, disposables); }, + registerTreeExplorerNodeProvider: proposedApiFunction(extension, (providerId: string, provider: vscode.TreeExplorerNodeProvider) => { + return extHostExplorers.registerTreeExplorerNodeProvider(providerId, provider); + }), onDidChangeConfiguration: (listener: () => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => { return extHostConfiguration.onDidChangeConfiguration(listener, thisArgs, disposables); }, diff --git a/src/vs/workbench/api/node/extHost.contribution.ts b/src/vs/workbench/api/node/extHost.contribution.ts index 809d2240679..bc1b43beebe 100644 --- a/src/vs/workbench/api/node/extHost.contribution.ts +++ b/src/vs/workbench/api/node/extHost.contribution.ts @@ -20,6 +20,7 @@ import { MainThreadDiagnostics } from './mainThreadDiagnostics'; import { MainThreadDocuments } from './mainThreadDocuments'; import { MainThreadEditors } from './mainThreadEditors'; import { MainThreadErrors } from './mainThreadErrors'; +import { MainThreadTreeExplorers } from './mainThreadTreeExplorers'; import { MainThreadLanguageFeatures } from './mainThreadLanguageFeatures'; import { MainThreadLanguages } from './mainThreadLanguages'; import { MainThreadMessageService } from './mainThreadMessageService'; @@ -70,6 +71,7 @@ export class ExtHostContribution implements IWorkbenchContribution { col.define(MainContext.MainThreadDocuments).set(create(MainThreadDocuments)); col.define(MainContext.MainThreadEditors).set(create(MainThreadEditors)); col.define(MainContext.MainThreadErrors).set(create(MainThreadErrors)); + col.define(MainContext.MainThreadExplorers).set(create(MainThreadTreeExplorers)); col.define(MainContext.MainThreadLanguageFeatures).set(create(MainThreadLanguageFeatures)); col.define(MainContext.MainThreadLanguages).set(create(MainThreadLanguages)); col.define(MainContext.MainThreadMessageService).set(create(MainThreadMessageService)); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 012d6603b64..0f94b3326a4 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -36,6 +36,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; @@ -136,6 +138,10 @@ export abstract class MainThreadEditorsShape { $tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise { throw ni(); } } +export abstract class MainThreadTreeExplorersShape { + $registerTreeExplorerNodeProvider(providerId: string): void { throw ni(); } +} + export abstract class MainThreadErrorsShape { onUnexpectedExtHostError(err: any): void { throw ni(); } } @@ -277,6 +283,12 @@ export abstract class ExtHostEditorsShape { $acceptTextEditorRemove(id: string): void { throw ni(); } } +export abstract class ExtHostTreeExplorersShape { + $provideRootNode(providerId: string): TPromise { throw ni(); }; + $resolveChildren(providerId: string, node: InternalTreeExplorerNodeContent): TPromise { throw ni(); } + $getInternalCommand(providerId: string, node: InternalTreeExplorerNodeContent): TPromise { throw ni(); } +} + export abstract class ExtHostExtensionServiceShape { $localShowMessage(severity: Severity, msg: string): void { throw ni(); } $activateExtension(extensionDescription: IExtensionDescription): TPromise { throw ni(); } @@ -351,6 +363,7 @@ export const MainContext = { MainThreadDocuments: createMainId('MainThreadDocuments', MainThreadDocumentsShape), MainThreadEditors: createMainId('MainThreadEditors', MainThreadEditorsShape), MainThreadErrors: createMainId('MainThreadErrors', MainThreadErrorsShape), + MainThreadExplorers: createMainId('MainThreadExplorers', MainThreadTreeExplorersShape), MainThreadLanguageFeatures: createMainId('MainThreadLanguageFeatures', MainThreadLanguageFeaturesShape), MainThreadLanguages: createMainId('MainThreadLanguages', MainThreadLanguagesShape), MainThreadMessageService: createMainId('MainThreadMessageService', MainThreadMessageServiceShape), @@ -371,6 +384,7 @@ export const ExtHostContext = { ExtHostDocuments: createExtId('ExtHostDocuments', ExtHostDocumentsShape), ExtHostDocumentSaveParticipant: createExtId('ExtHostDocumentSaveParticipant', ExtHostDocumentSaveParticipantShape), ExtHostEditors: createExtId('ExtHostEditors', ExtHostEditorsShape), + ExtHostExplorers: createExtId('ExtHostExplorers', ExtHostTreeExplorersShape), ExtHostFileSystemEventService: createExtId('ExtHostFileSystemEventService', ExtHostFileSystemEventServiceShape), ExtHostHeapService: createExtId('ExtHostHeapMonitor', ExtHostHeapServiceShape), ExtHostLanguageFeatures: createExtId('ExtHostLanguageFeatures', ExtHostLanguageFeaturesShape), diff --git a/src/vs/workbench/api/node/extHostTreeExplorers.ts b/src/vs/workbench/api/node/extHostTreeExplorers.ts new file mode 100644 index 00000000000..22a6af7a136 --- /dev/null +++ b/src/vs/workbench/api/node/extHostTreeExplorers.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +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 _extNodeProviders: { [providerId: string]: TreeExplorerNodeProvider }; + private _extNodeMaps: { [providerId: string]: { [id: number]: any } }; + + constructor( + threadService: IThreadService, + private commands: ExtHostCommands + ) { + super(); + + this._proxy = threadService.get(MainContext.MainThreadExplorers); + + this._extNodeProviders = Object.create(null); + this._extNodeMaps = Object.create(null); + } + + registerTreeExplorerNodeProvider(providerId: string, provider: TreeExplorerNodeProvider): Disposable { + this._proxy.$registerTreeExplorerNodeProvider(providerId); + this._extNodeProviders[providerId] = provider; + + return new Disposable(() => { + delete this._extNodeProviders[providerId]; + delete this._extNodeProviders[providerId]; + }); + } + + $provideRootNode(providerId: string): TPromise { + const provider = this._extNodeProviders[providerId]; + if (!provider) { + const errMessage = localize('treeExplorer.notRegistered', 'No TreeExplorerNodeProvider with id \'{0}\' registered.', providerId); + return TPromise.wrapError(errMessage); + } + + return asWinJsPromise(() => provider.provideRootNode()).then(extRootNode => { + const extNodeMap = Object.create(null); + const internalRootNode = new InternalTreeExplorerNode(extRootNode, provider); + + extNodeMap[internalRootNode.id] = extRootNode; + this._extNodeMaps[providerId] = extNodeMap; + + return internalRootNode; + }, err => { + const errMessage = localize('treeExplorer.failedToProvideRootNode', 'TreeExplorerNodeProvider \'{0}\' failed to provide root node.', providerId); + return TPromise.wrapError(errMessage); + }); + } + + $resolveChildren(providerId: string, mainThreadNode: InternalTreeExplorerNode): TPromise { + const provider = this._extNodeProviders[providerId]; + if (!provider) { + const errMessage = localize('treeExplorer.notRegistered', 'No TreeExplorerNodeProvider with id \'{0}\' registered.', providerId); + return TPromise.wrapError(errMessage); + } + + const extNodeMap = this._extNodeMaps[providerId]; + const extNode = extNodeMap[mainThreadNode.id]; + + return asWinJsPromise(() => provider.resolveChildren(extNode)).then(children => { + return children.map(extChild => { + const internalChild = new InternalTreeExplorerNode(extChild, provider); + extNodeMap[internalChild.id] = extChild; + return internalChild; + }); + }, err => { + const errMessage = localize('treeExplorer.failedToResolveChildren', 'TreeExplorerNodeProvider \'{0}\' failed to resolveChildren.', providerId); + return TPromise.wrapError(errMessage); + }); + } + + // Convert the command on the ExtHost side so we can pass the original externalNode to the registered handler + $getInternalCommand(providerId: string, mainThreadNode: InternalTreeExplorerNode): TPromise { + const commandConverter = this.commands.converter; + + if (mainThreadNode.clickCommand) { + const extNode = this._extNodeMaps[providerId][mainThreadNode.id]; + + const internalCommand = commandConverter.toInternal({ + title: '', + command: mainThreadNode.clickCommand, + arguments: [extNode] + }); + + return TPromise.wrap(internalCommand); + } + + return TPromise.as(null); + } +} diff --git a/src/vs/workbench/api/node/mainThreadTreeExplorers.ts b/src/vs/workbench/api/node/mainThreadTreeExplorers.ts new file mode 100644 index 00000000000..7095b535668 --- /dev/null +++ b/src/vs/workbench/api/node/mainThreadTreeExplorers.ts @@ -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 { ICustomTreeExplorerService } from 'vs/workbench/parts/explorers/browser/customTreeExplorerService'; +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, + @ICustomTreeExplorerService private treeExplorerService: ICustomTreeExplorerService, + @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 => { + return this._proxy.$provideRootNode(providerId).then(rootNode => rootNode, onError); + }, + resolveChildren: (node: InternalTreeExplorerNodeContent): TPromise => { + return this._proxy.$resolveChildren(providerId, node).then(children => children, onError); + }, + executeCommand: (node: InternalTreeExplorerNodeContent): TPromise => { + return this._proxy.$getInternalCommand(providerId, node).then(command => { + return this.commandService.executeCommand(command.id, ...command.arguments); + }); + } + }); + } +} diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 7b285f5f15e..6602620942e 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -12,9 +12,8 @@ import { Builder, $ } from 'vs/base/browser/builder'; import { Action } from 'vs/base/common/actions'; import errors = require('vs/base/common/errors'); import { ActionsOrientation, ActionBar, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Registry } from 'vs/platform/platform'; import { IComposite } from 'vs/workbench/common/composite'; -import { ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet'; +import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Part } from 'vs/workbench/browser/part'; import { ActivityAction, ActivityActionItem } from 'vs/workbench/browser/parts/activitybar/activityAction'; import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService'; @@ -25,6 +24,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class ActivitybarPart extends Part implements IActivityService { public _serviceBrand: any; + private viewletSwitcherBar: ActionBar; private activityActionItems: { [actionId: string]: IActionItem; }; private compositeIdToActions: { [compositeId: string]: ActivityAction; }; @@ -51,6 +51,12 @@ export class ActivitybarPart extends Part implements IActivityService { // Deactivate viewlet action on close this.toUnbind.push(this.viewletService.onDidViewletClose(viewlet => this.onCompositeClosed(viewlet))); + + // Update viewlet switcher when external viewlets become ready + this.toUnbind.push(this.viewletService.onDidExtViewletsLoad(() => this.refreshViewletSwitcher())); + + // Update viewlet switcher on toggling of a viewlet + this.toUnbind.push(this.viewletService.onDidViewletToggle(() => this.refreshViewletSwitcher())); } private onActiveCompositeChanged(composite: IComposite): void { @@ -90,33 +96,53 @@ export class ActivitybarPart extends Part implements IActivityService { } private createViewletSwitcher(div: Builder): void { - - // Composite switcher is on top this.viewletSwitcherBar = new ActionBar(div, { actionItemProvider: (action: Action) => this.activityActionItems[action.id], orientation: ActionsOrientation.VERTICAL, ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher") }); - // Build Viewlet Actions in correct order - const allViewlets = (Registry.as(ViewletExtensions.Viewlets)).getViewlets(); - const viewletActions = allViewlets.sort((v1, v2) => v1.order - v2.order).map(viewlet => this.toAction(viewlet)); + this.fillViewletSwitcher(this.viewletService.getAllViewletsToDisplay()); + } - this.viewletSwitcherBar.push(viewletActions, { label: true, icon: true }); + private refreshViewletSwitcher(): void { + this.fillViewletSwitcher(this.viewletService.getAllViewletsToDisplay()); + } + + private fillViewletSwitcher(viewlets: ViewletDescriptor[]) { + // Pull out viewlets no longer needed + const newViewletIds = viewlets.map(v => v.id); + const existingViewletIds = Object.keys(this.compositeIdToActions); + existingViewletIds.forEach(viewletId => { + if (newViewletIds.indexOf(viewletId) === -1) { + this.pullViewlet(viewletId); + } + }); + + const actionsToPush = viewlets + .filter(viewlet => !this.compositeIdToActions[viewlet.id]) + .map(viewlet => this.toAction(viewlet)); + + this.viewletSwitcherBar.push(actionsToPush, { label: true, icon: true }); + } + + private pullViewlet(viewletId: string): void { + const index = Object.keys(this.compositeIdToActions).indexOf(viewletId); + const action = this.compositeIdToActions[viewletId]; + const actionItem = this.activityActionItems[action.id]; + delete this.compositeIdToActions[viewletId]; + delete this.activityActionItems[action.id]; + action.dispose(); + actionItem.dispose(); + this.viewletSwitcherBar.pull(index); } private toAction(composite: ViewletDescriptor): ActivityAction { - const activeViewlet = this.viewletService.getActiveViewlet(); const action = this.instantiationService.createInstance(ViewletActivityAction, composite.id + '.activity-bar-action', composite); this.activityActionItems[action.id] = new ActivityActionItem(action, composite.name, this.getKeybindingLabel(composite.id)); this.compositeIdToActions[composite.id] = action; - // Mark active viewlet as active - if (activeViewlet && activeViewlet.getId() === composite.id) { - action.activate(); - } - return action; }; @@ -144,7 +170,8 @@ class ViewletActivityAction extends ActivityAction { private lastRun: number = 0; constructor( - id: string, private viewlet: ViewletDescriptor, + id: string, + private viewlet: ViewletDescriptor, @IViewletService private viewletService: IViewletService, @IPartService private partService: IPartService ) { diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index b429e7ed4e9..12ea2a40a1c 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -25,7 +25,16 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import Event from 'vs/base/common/event'; -export class SidebarPart extends CompositePart implements IViewletService { +export interface ISidebar { + onDidViewletOpen: Event; + onDidViewletClose: Event; + openViewlet(id: string, focus?: boolean): TPromise; + getActiveViewlet(): IViewlet; + getLastActiveViewletId(): string; + hideActiveViewlet(): TPromise; +} + +export class SidebarPart extends CompositePart implements ISidebar { public static activeViewletSettingsKey = 'workbench.sidebar.activeviewletid'; diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 52b0d77f53d..511418ee269 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -153,8 +153,12 @@ export abstract class ViewerViewlet extends Viewlet { */ export class ViewletDescriptor extends CompositeDescriptor { - constructor(moduleId: string, ctorName: string, id: string, name: string, cssClass?: string, order?: number) { + constructor(moduleId: string, ctorName: string, id: string, name: string, cssClass?: string, order?: number, public isExternal: boolean = false) { super(moduleId, ctorName, id, name, cssClass, order); + if (isExternal) { + // Pass viewletId to external viewlet, which doesn't know its id until runtime. + this.appendStaticArguments([id]); + } } } diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 9b67d4aa5de..97325470a31 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -56,6 +56,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IActivityService } from 'vs/workbench/services/activity/common/activityService'; import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService'; +import { ViewletService } from 'vs/workbench/services/viewlet/browser/viewletService'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -135,6 +136,7 @@ export class Workbench implements IPartService { private workbenchCreated: boolean; private workbenchShutdown: boolean; private editorService: WorkbenchEditorService; + private viewletService: IViewletService; private contextKeyService: IContextKeyService; private keybindingService: IKeybindingService; private configurationEditingService: IConfigurationEditingService; @@ -243,16 +245,18 @@ export class Workbench implements IPartService { // Load composits and editors in parallel const compositeAndEditorPromises: TPromise[] = []; - // Load Viewlet - const viewletRegistry = Registry.as(ViewletExtensions.Viewlets); - let viewletId = viewletRegistry.getDefaultViewletId(); - if (this.shouldRestoreSidebar()) { - viewletId = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, viewletId); // help developers and restore last view - } + // Restore last opened viewlet + if (!this.sideBarHidden) { + let viewletIdToRestore; - if (!this.sideBarHidden && !!viewletId) { - const viewletTimerEvent = timer.start(timer.Topic.STARTUP, strings.format('Opening Viewlet: {0}', viewletId)); - compositeAndEditorPromises.push(this.sidebarPart.openViewlet(viewletId, false).then(() => viewletTimerEvent.stop())); + if (this.shouldRestoreLastOpenedViewlet) { + viewletIdToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE); + } else { + viewletIdToRestore = Registry.as(ViewletExtensions.Viewlets).getDefaultViewletId(); + } + + const viewletTimerEvent = timer.start(timer.Topic.STARTUP, strings.format('Opening Viewlet: {0}', viewletIdToRestore)); + compositeAndEditorPromises.push(this.viewletService.restoreViewlet(viewletIdToRestore).then(() => viewletTimerEvent.stop())); } // Load Panel @@ -389,11 +393,14 @@ export class Workbench implements IPartService { this.toShutdown.push(this.titlebarPart); serviceCollection.set(ITitleService, this.titlebarPart); - // Viewlet service (sidebar part) + // Sidebar part this.sidebarPart = this.instantiationService.createInstance(SidebarPart, Identifiers.SIDEBAR_PART); this.toDispose.push(this.sidebarPart); this.toShutdown.push(this.sidebarPart); - serviceCollection.set(IViewletService, this.sidebarPart); + + // Viewlet service + this.viewletService = this.instantiationService.createInstance(ViewletService, this.sidebarPart); + serviceCollection.set(IViewletService, this.viewletService); // Panel service (panel part) this.panelPart = this.instantiationService.createInstance(PanelPart, Identifiers.PANEL_PART); @@ -1006,7 +1013,7 @@ export class Workbench implements IPartService { this.storageService.store(Workbench.sidebarRestoreSettingKey, 'true', StorageScope.WORKSPACE); } - private shouldRestoreSidebar(): boolean { + private shouldRestoreLastOpenedViewlet(): boolean { if (!this.environmentService.isBuilt) { return true; // always restore sidebar when we are in development mode } diff --git a/src/vs/workbench/parts/explorers/browser/customTreeExplorerService.ts b/src/vs/workbench/parts/explorers/browser/customTreeExplorerService.ts new file mode 100644 index 00000000000..37fa4646164 --- /dev/null +++ b/src/vs/workbench/parts/explorers/browser/customTreeExplorerService.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { InternalTreeExplorerNode, InternalTreeExplorerNodeProvider } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; +import { IMessageService, Severity } from 'vs/platform/message/common/message'; +import Event, { Emitter } from 'vs/base/common/event'; + +export const ICustomTreeExplorerService = createDecorator('customTreeExplorerService'); + +export interface ICustomTreeExplorerService { + _serviceBrand: any; + + onTreeExplorerNodeProviderRegistered: Event; + + registerTreeExplorerNodeProvider(providerId: string, provider: InternalTreeExplorerNodeProvider): void; + hasProvider(providerId: string): boolean; + + provideRootNode(providerId: string): TPromise; + resolveChildren(providerId: string, node: InternalTreeExplorerNode): TPromise; + executeCommand(providerId: string, node: InternalTreeExplorerNode): TPromise; +} + +export class CustomTreeExplorerService implements ICustomTreeExplorerService { + public _serviceBrand: any; + + private _onTreeExplorerNodeProviderRegistered = new Emitter(); + public get onTreeExplorerNodeProviderRegistered(): Event { return this._onTreeExplorerNodeProviderRegistered.event; }; + + private _treeExplorerNodeProviders: { [providerId: string]: InternalTreeExplorerNodeProvider }; + + constructor( + @IInstantiationService private _instantiationService: IInstantiationService, + @IMessageService private messageService: IMessageService, + ) { + this._treeExplorerNodeProviders = Object.create(null); + } + + public registerTreeExplorerNodeProvider(providerId: string, provider: InternalTreeExplorerNodeProvider): void { + this._treeExplorerNodeProviders[providerId] = provider; + this._onTreeExplorerNodeProviderRegistered.fire(providerId); + } + + public hasProvider(providerId: string): boolean { + return !!this._treeExplorerNodeProviders[providerId]; + } + + public provideRootNode(providerId: string): TPromise { + const provider = this.getProvider(providerId); + return TPromise.wrap(provider.provideRootNode()); + } + + public resolveChildren(providerId: string, node: InternalTreeExplorerNode): TPromise { + const provider = this.getProvider(providerId); + return TPromise.wrap(provider.resolveChildren(node)); + } + + public executeCommand(providerId: string, node: InternalTreeExplorerNode): TPromise { + const provider = this.getProvider(providerId); + return TPromise.wrap(provider.executeCommand(node)); + } + + private getProvider(providerId: string): InternalTreeExplorerNodeProvider { + const provider = this._treeExplorerNodeProviders[providerId]; + + if (!provider) { + this.messageService.show(Severity.Error, localize('treeExplorer.noMatchingProviderId', 'No TreeExplorerNodeProvider with id {providerId} registered.')); + } + + return provider; + } +} diff --git a/src/vs/workbench/parts/explorers/browser/treeExplorer.contribution.ts b/src/vs/workbench/parts/explorers/browser/treeExplorer.contribution.ts new file mode 100644 index 00000000000..5b999c8dd73 --- /dev/null +++ b/src/vs/workbench/parts/explorers/browser/treeExplorer.contribution.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/css!../media/treeExplorer.contribution'; + +import { localize } from 'vs/nls'; +import { join } from 'vs/base/common/paths'; +import { createCSSRule } from 'vs/base/browser/dom'; +import { Registry } from 'vs/platform/platform'; +import { ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { ICustomTreeExplorerService, CustomTreeExplorerService } from 'vs/workbench/parts/explorers/browser/customTreeExplorerService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; +import { ITreeExplorer } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { toCustomExplorerViewletId, toCustomExplorerViewletCSSClass, isValidViewletId } from 'vs/workbench/parts/explorers/common/treeExplorer'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IMessageService, Severity } from 'vs/platform/message/common/message'; + +registerSingleton(ICustomTreeExplorerService, CustomTreeExplorerService); + +const explorerSchema: IJSONSchema = { + description: localize('vscode.extension.contributes.explorer', 'Contributes custom tree explorer viewlet to the sidebar'), + type: 'object', + properties: { + treeExplorerNodeProviderId: { + description: localize('vscode.extension.contributes.explorer.treeExplorerNodeProviderId', 'Unique id used to identify provider registered through vscode.workspace.registerTreeExplorerNodeProvider'), + type: 'string' + }, + treeLabel: { + description: localize('vscode.extension.contributes.explorer.treeLabel', 'Human readable string used to render the custom tree explorer'), + type: 'string' + }, + icon: { + description: localize('vscode.extension.contributes.explorer.icon', 'Path to the viewlet icon on the activity bar'), + type: 'string' + } + } +}; + +export class ExplorerContribtion implements IWorkbenchContribution { + + constructor( + @IMessageService private messageService: IMessageService + ) { + this.init(); + } + + public getId(): string { + return 'vs.explorer'; + } + + private init() { + ExtensionsRegistry.registerExtensionPoint('explorer', [], explorerSchema).setHandler(extensions => { + for (let extension of extensions) { + const { treeExplorerNodeProviderId, treeLabel, icon } = extension.value; + + if (!isValidViewletId(treeExplorerNodeProviderId)) { + return this.messageService.show(Severity.Error, localize('treeExplorer.invalidId', 'Tree Explorer extension {0} has invalid id and failed to activate.', treeLabel)); + } + + const getIconRule = (iconPath) => { return `background-image: url('${iconPath}')`; }; + if (icon) { + const iconClass = `.monaco-workbench > .activitybar .monaco-action-bar .action-label.${toCustomExplorerViewletCSSClass(treeExplorerNodeProviderId)}`; + const iconPath = join(extension.description.extensionFolderPath, icon); + createCSSRule(iconClass, getIconRule(iconPath)); + } + + Registry.as(ViewletExtensions.Viewlets).registerViewlet(new ViewletDescriptor( + 'vs/workbench/parts/explorers/browser/treeExplorerViewlet', + 'TreeExplorerViewlet', + toCustomExplorerViewletId(treeExplorerNodeProviderId), + treeLabel, + toCustomExplorerViewletCSSClass(treeExplorerNodeProviderId), + -1, // External viewlets are ordered by enabling sequence, so order here doesn't matter. + true + )); + } + }); + } +} + +(Registry.as(WorkbenchExtensions.Workbench)).registerWorkbenchContribution(ExplorerContribtion); \ No newline at end of file diff --git a/src/vs/workbench/parts/explorers/browser/treeExplorerViewlet.ts b/src/vs/workbench/parts/explorers/browser/treeExplorerViewlet.ts new file mode 100644 index 00000000000..464065e1e0f --- /dev/null +++ b/src/vs/workbench/parts/explorers/browser/treeExplorerViewlet.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Builder, Dimension } from 'vs/base/browser/builder'; +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; +import { IAction } from 'vs/base/common/actions'; +import { IViewletView, Viewlet } from 'vs/workbench/browser/viewlet'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TreeExplorerView } from 'vs/workbench/parts/explorers/browser/views/treeExplorerView'; +import { TreeExplorerViewletState } from 'vs/workbench/parts/explorers/browser/views/treeExplorerViewer'; +import { IActivityService } from 'vs/workbench/services/activity/common/activityService'; + +export class TreeExplorerViewlet extends Viewlet { + + private viewletContainer: Builder; + private view: IViewletView; + + private viewletState: TreeExplorerViewletState; + private viewletId: string; + private treeNodeProviderId: string; + + constructor( + viewletId: string, + @ITelemetryService telemetryService: ITelemetryService, + @IInstantiationService private instantiationService: IInstantiationService, + @IActivityService private activityService: IActivityService + ) { + super(viewletId, telemetryService); + + this.viewletState = new TreeExplorerViewletState(); + this.viewletId = viewletId; + this.treeNodeProviderId = this.getTreeProviderName(viewletId); + } + + public getId(): string { + return this.viewletId; + } + + public create(parent: Builder): TPromise { + super.create(parent); + + this.viewletContainer = parent.div(); + this.addTreeView(); + + return TPromise.as(null); + } + + public layout(dimension: Dimension): void { + this.view.layout(dimension.height, Orientation.VERTICAL); + } + + public setVisible(visible: boolean): TPromise { + return super.setVisible(visible).then(() => { + this.view.setVisible(visible).done(); + }); + } + + public getActions(): IAction[] { + return this.view.getActions(); + } + + private addTreeView(): void { + // Hide header (root node) by default + const headerSize = 0; + + this.view = this.instantiationService.createInstance(TreeExplorerView, this.viewletState, this.treeNodeProviderId, this.getActionRunner(), headerSize); + this.view.render(this.viewletContainer.getHTMLElement(), Orientation.VERTICAL); + } + + private getTreeProviderName(viewletId: string): string { + const tokens = viewletId.split('.'); + return tokens[tokens.length - 1]; + } + + public dispose(): void { + this.view = null; + } +} diff --git a/src/vs/workbench/parts/explorers/browser/views/treeExplorerView.ts b/src/vs/workbench/parts/explorers/browser/views/treeExplorerView.ts new file mode 100644 index 00000000000..c01cdb5540c --- /dev/null +++ b/src/vs/workbench/parts/explorers/browser/views/treeExplorerView.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls = require('vs/nls'); +import { TPromise } from 'vs/base/common/winjs.base'; +import * as DOM from 'vs/base/browser/dom'; +import { Builder, $ } from 'vs/base/browser/builder'; +import { IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { CollapsibleViewletView } from 'vs/workbench/browser/viewlet'; +import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { IMessageService } from 'vs/platform/message/common/message'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICustomTreeExplorerService } from 'vs/workbench/parts/explorers/browser/customTreeExplorerService'; +import { ITree } from 'vs/base/parts/tree/browser/tree'; +import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; +import { TreeExplorerViewletState, TreeDataSource, TreeRenderer, TreeController } from 'vs/workbench/parts/explorers/browser/views/treeExplorerViewer'; +import { RefreshViewExplorerAction } from 'vs/workbench/parts/explorers/common/treeExplorerActions'; +import { IProgressService } from 'vs/platform/progress/common/progress'; + +export class TreeExplorerView extends CollapsibleViewletView { + private workspace: IWorkspace; + + constructor( + private viewletState: TreeExplorerViewletState, + private treeNodeProviderId: string, + actionRunner: IActionRunner, + headerSize: number, + @IMessageService messageService: IMessageService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IInstantiationService private instantiationService: IInstantiationService, + @ICustomTreeExplorerService private treeExplorerViewletService: ICustomTreeExplorerService, + @IProgressService private progressService: IProgressService + ) { + super(actionRunner, false, nls.localize('treeExplorerViewlet.tree', "Tree Explorer Section"), messageService, keybindingService, contextMenuService, headerSize); + + this.workspace = contextService.getWorkspace(); + + this.create(); + } + + public renderBody(container: HTMLElement): void { + this.treeContainer = super.renderViewTree(container); + DOM.addClass(this.treeContainer, 'tree-explorer-viewlet-tree-view'); + + this.tree = this.createViewer($(this.treeContainer)); + } + + public createViewer(container: Builder): ITree { + const dataSource = this.instantiationService.createInstance(TreeDataSource, this.treeNodeProviderId); + const renderer = this.instantiationService.createInstance(TreeRenderer, this.viewletState, this.actionRunner, container.getHTMLElement()); + const controller = this.instantiationService.createInstance(TreeController, this.treeNodeProviderId); + const sorter = null; + const filter = null; + const dnd = null; + const accessibilityProvider = null; + + return new Tree(container.getHTMLElement(), { + dataSource, + renderer, + controller, + sorter, + filter, + dnd, + accessibilityProvider + }); + } + + public getActions(): IAction[] { + const refresh = this.instantiationService.createInstance(RefreshViewExplorerAction, this); + return [refresh]; + } + + public create(): TPromise { + return this.updateInput(); + } + + public setVisible(visible: boolean): TPromise { + return super.setVisible(visible); + } + + public updateInput(): TPromise { + if (this.treeExplorerViewletService.hasProvider(this.treeNodeProviderId)) { + return this.treeExplorerViewletService.provideRootNode(this.treeNodeProviderId).then(tree => { + this.tree.setInput(tree); + }); + } + // Provider registration happens independently of the reading of extension's contribution, + // which constructs the viewlet, so it's possible the viewlet is constructed before a provider + // is registered. + // This renders the viewlet first and wait for a corresponding provider is registered. + else { + this.treeExplorerViewletService.onTreeExplorerNodeProviderRegistered(providerId => { + if (this.treeNodeProviderId === providerId) { + return this.treeExplorerViewletService.provideRootNode(this.treeNodeProviderId).then(tree => { + this.tree.setInput(tree); + }); + } + }); + + return TPromise.as(null); + } + } + + public getOptimalWidth(): number { + const parentNode = this.tree.getHTMLElement(); + const childNodes = [].slice.call(parentNode.querySelectorAll('.outline-item-label > a')); + + return DOM.getLargestChildWidth(parentNode, childNodes); + } +} diff --git a/src/vs/workbench/parts/explorers/browser/views/treeExplorerViewer.ts b/src/vs/workbench/parts/explorers/browser/views/treeExplorerViewer.ts new file mode 100644 index 00000000000..ae13a5c041b --- /dev/null +++ b/src/vs/workbench/parts/explorers/browser/views/treeExplorerViewer.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { $, Builder } from 'vs/base/browser/builder'; +import { ITree, IDataSource, IRenderer, IElementCallback } from 'vs/base/parts/tree/browser/tree'; +import { InternalTreeExplorerNode } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; +import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IActionRunner } from 'vs/base/common/actions'; +import { IActionProvider, ActionsRenderer } from 'vs/base/parts/tree/browser/actionsRenderer'; +import { ContributableActionProvider } from 'vs/workbench/browser/actionBarRegistry'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IExtensionService } from 'vs/platform/extensions/common/extensions'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ICustomTreeExplorerService } from 'vs/workbench/parts/explorers/browser/customTreeExplorerService'; +import { IProgressService } from 'vs/platform/progress/common/progress'; + +export class TreeDataSource implements IDataSource { + + constructor( + private treeNodeProviderId: string, + @ICustomTreeExplorerService private treeExplorerViewletService: ICustomTreeExplorerService, + @IProgressService private progressService: IProgressService + ) { + + } + + public getId(tree: ITree, node: InternalTreeExplorerNode): string { + return node.id.toString(); + } + + public hasChildren(tree: ITree, node: InternalTreeExplorerNode): boolean { + return node.hasChildren; + } + + public getChildren(tree: ITree, node: InternalTreeExplorerNode): TPromise { + const promise = this.treeExplorerViewletService.resolveChildren(this.treeNodeProviderId, node); + + this.progressService.showWhile(promise, 800); + + return promise; + } + + public getParent(tree: ITree, node: InternalTreeExplorerNode): TPromise { + return TPromise.as(null); + } +} + +export class TreeRenderer extends ActionsRenderer implements IRenderer { + + constructor( + state: TreeExplorerViewletState, + actionRunner: IActionRunner, + private container: HTMLElement, + @IContextViewService private contextViewService: IContextViewService, + @IExtensionService private extensionService: IExtensionService, + @IModeService private modeService: IModeService + ) { + super({ + actionProvider: state.actionProvider, + actionRunner: actionRunner + }); + } + + public getContentHeight(tree: ITree, element: any): number { + return 22; + } + + public renderContents(tree: ITree, node: InternalTreeExplorerNode, domElement: HTMLElement, previousCleanupFn: IElementCallback): IElementCallback { + const el = $(domElement).clearChildren(); + const item = $('.custom-viewlet-tree-node-item'); + item.appendTo(el); + return this.renderFileFolderLabel(item, node); + } + + private renderFileFolderLabel(container: Builder, node: InternalTreeExplorerNode): IElementCallback { + const label = $('.custom-viewlet-tree-node-item-label').appendTo(container); + $('a.plain').text(node.label).title(node.label).appendTo(label); + + return null; + } +} + +export class TreeController extends DefaultController { + + constructor( + private treeNodeProviderId: string, + @ICustomTreeExplorerService private treeExplorerViewletService: ICustomTreeExplorerService + ) { + super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }); + } + + public onLeftClick(tree: ITree, node: InternalTreeExplorerNode, event: IMouseEvent, origin: string = 'mouse'): boolean { + super.onLeftClick(tree, node, event, origin); + + if (node.clickCommand) { + this.treeExplorerViewletService.executeCommand(this.treeNodeProviderId, node); + } + + return true; + } +} + +export interface ITreeExplorerViewletState { + actionProvider: IActionProvider; +} + +export class TreeExplorerActionProvider extends ContributableActionProvider { + private state: TreeExplorerViewletState; + + constructor(state: TreeExplorerViewletState) { + super(); + + this.state = state; + } +} + +export class TreeExplorerViewletState implements ITreeExplorerViewletState { + private _actionProvider: TreeExplorerActionProvider; + + constructor() { + this._actionProvider = new TreeExplorerActionProvider(this); + } + + public get actionProvider() { return this._actionProvider; } +} diff --git a/src/vs/workbench/parts/explorers/common/treeExplorer.ts b/src/vs/workbench/parts/explorers/common/treeExplorer.ts new file mode 100644 index 00000000000..ed1ed85e5e0 --- /dev/null +++ b/src/vs/workbench/parts/explorers/common/treeExplorer.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export function toCustomExplorerViewletId(viewletId: string): string { + return 'workbench.view.customExplorer.' + viewletId; +} + +export function toCustomExplorerViewletActionId(viewletId: string): string { + return 'workbench.action.customExplorer.' + viewletId; +} + +export function toCustomExplorerViewletCSSClass(viewletId: string): string { + return 'customExplorer-' + viewletId; +} + +export function isValidViewletId(viewletId: string): boolean { + // Only allow alphanumeric letters, `_` and `-`. + return /^[a-z0-9_-]+$/i.test(viewletId); +} diff --git a/src/vs/workbench/parts/explorers/common/treeExplorerActions.contribution.ts b/src/vs/workbench/parts/explorers/common/treeExplorerActions.contribution.ts new file mode 100644 index 00000000000..70f36803b4d --- /dev/null +++ b/src/vs/workbench/parts/explorers/common/treeExplorerActions.contribution.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Registry } from 'vs/platform/platform'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry'; +import { ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { Action } from 'vs/base/common/actions'; +import { IQuickOpenService, IPickOpenEntry } from 'vs/workbench/services/quickopen/common/quickOpenService'; +import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService'; +import { toCustomExplorerViewletActionId } from 'vs/workbench/parts/explorers/common/treeExplorer'; + +const registry = Registry.as(ActionExtensions.WorkbenchActions); + +export class ToggleExternalViewletAction extends Action { + public static ID = toCustomExplorerViewletActionId('toggle'); + public static LABEL = localize('treeExplorer.toggle', 'Toggle Custom Explorer'); + + constructor( + id: string, + label: string, + @IQuickOpenService private quickOpenService: IQuickOpenService, + @IViewletService private viewletService: IViewletService + ) { + super(id, name); + } + + run(): TPromise { + const extViewlets = this.viewletService.getAllViewlets().filter(viewlet => viewlet.isExternal); + + const picks: IPickOpenEntry[] = []; + + extViewlets.forEach(viewlet => { + const isEnabled = this.viewletService.isViewletEnabled(viewlet.id); + const actionLabel = isEnabled ? localize('disable', 'Disable') : localize('enable', 'Enable'); + picks.push({ + id: viewlet.id, + label: `${actionLabel} ${viewlet.name}`, + run: () => { + this.viewletService.toggleViewlet(viewlet.id).then(() => { + if (isEnabled) { + // To disable, so open default viewlet + const defaultViewletId = (Registry.as(ViewletExtensions.Viewlets)).getDefaultViewletId(); + this.viewletService.openViewlet(defaultViewletId); + } else { + // To enable, so open the viewlet to be enabled + this.viewletService.openViewlet(viewlet.id); + } + }); + } + }); + }); + + return TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */).then(() => { + this.quickOpenService.pick(picks, { placeHolder: 'Select Custom Explorer to toggle' }).done(); + }); + } +} + +registry.registerWorkbenchAction( + new SyncActionDescriptor(ToggleExternalViewletAction, ToggleExternalViewletAction.ID, ToggleExternalViewletAction.LABEL), + 'View: Toggle Custom Explorer', + localize('view', "View") +); diff --git a/src/vs/workbench/parts/explorers/common/treeExplorerActions.ts b/src/vs/workbench/parts/explorers/common/treeExplorerActions.ts new file mode 100644 index 00000000000..226f8dc1b70 --- /dev/null +++ b/src/vs/workbench/parts/explorers/common/treeExplorerActions.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { TreeExplorerView } from 'vs/workbench/parts/explorers/browser/views/treeExplorerView'; +import { toCustomExplorerViewletActionId } from 'vs/workbench/parts/explorers/common/treeExplorer'; + +export class RefreshViewExplorerAction extends Action { + + constructor(view: TreeExplorerView) { + super(toCustomExplorerViewletActionId('refresh'), nls.localize('refresh', 'Refresh'), 'customExplorer-action toggle', true, () => { + view.updateInput(); + return TPromise.as(null); + }); + } +} diff --git a/src/vs/workbench/parts/explorers/common/treeExplorerViewModel.ts b/src/vs/workbench/parts/explorers/common/treeExplorerViewModel.ts new file mode 100644 index 00000000000..f82df34a574 --- /dev/null +++ b/src/vs/workbench/parts/explorers/common/treeExplorerViewModel.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeExplorerNodeProvider } from 'vscode'; + +export interface InternalTreeExplorerNodeContent { + label: string; + hasChildren: boolean; + clickCommand: string; +} + +export class InternalTreeExplorerNode implements InternalTreeExplorerNodeContent { + private static idCounter = 1; + + id: number; + + label: string; + hasChildren: boolean; + clickCommand: string; + + constructor(node: any, provider: TreeExplorerNodeProvider) { + this.id = InternalTreeExplorerNode.idCounter++; + + this.label = provider.getLabel ? provider.getLabel(node) : node.toString(); + this.hasChildren = provider.getHasChildren ? provider.getHasChildren(node) : true; + this.clickCommand = provider.getClickCommand ? provider.getClickCommand(node) : null; + } +} + +export interface InternalTreeExplorerNodeProvider { + provideRootNode(): Thenable; + resolveChildren(node: InternalTreeExplorerNodeContent): Thenable; + executeCommand(node: InternalTreeExplorerNodeContent): TPromise; +} diff --git a/src/vs/workbench/parts/explorers/media/Refresh.svg b/src/vs/workbench/parts/explorers/media/Refresh.svg new file mode 100644 index 00000000000..e0345748192 --- /dev/null +++ b/src/vs/workbench/parts/explorers/media/Refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/explorers/media/Refresh_inverse.svg b/src/vs/workbench/parts/explorers/media/Refresh_inverse.svg new file mode 100644 index 00000000000..d79fdaa4e8e --- /dev/null +++ b/src/vs/workbench/parts/explorers/media/Refresh_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/explorers/media/treeExplorer.contribution.css b/src/vs/workbench/parts/explorers/media/treeExplorer.contribution.css new file mode 100644 index 00000000000..b8a051acd61 --- /dev/null +++ b/src/vs/workbench/parts/explorers/media/treeExplorer.contribution.css @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .customExplorer-action.toggle { + background: url('Refresh.svg') center center no-repeat; +} + +.vs-dark .monaco-workbench .customExplorer-action.toggle, +.hc-black .monaco-workbench .customExplorer-action.toggle { + background: url('Refresh_inverse.svg') center center no-repeat; +} + +/* Coerce extension-contributed viewlet icon into a style similar to stock icons */ +.monaco-workbench > .activitybar .monaco-action-bar .action-item:nth-child(n+6) .action-label { + -webkit-filter: grayscale(1) invert(1); + filter: grayscale(1) invert(1); +} + +.custom-viewlet-tree-node-item { + height: 22px; + line-height: 22px; +} diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index fb551a60010..798dee3fb06 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -258,6 +258,9 @@ export class ActionRunner extends BaseActionRunner implements IActionRunner { // Explorer Renderer export class FileRenderer extends ActionsRenderer implements IRenderer { + + private static ITEM_HEIGHT = 22; + private state: FileViewletState; constructor( @@ -275,7 +278,7 @@ export class FileRenderer extends ActionsRenderer implements IRenderer { } public getContentHeight(tree: ITree, element: any): number { - return 22; + return FileRenderer.ITEM_HEIGHT; } public renderContents(tree: ITree, stat: FileStat, domElement: HTMLElement, previousCleanupFn: IElementCallback): IElementCallback { diff --git a/src/vs/workbench/services/viewlet/browser/viewletService.ts b/src/vs/workbench/services/viewlet/browser/viewletService.ts new file mode 100644 index 00000000000..3d0df3f9b69 --- /dev/null +++ b/src/vs/workbench/services/viewlet/browser/viewletService.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Event, { Emitter } from 'vs/base/common/event'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService'; +import { IViewlet } from 'vs/workbench/common/viewlet'; +import { ISidebar } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; +import { Registry } from 'vs/platform/platform'; +import { ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IExtensionService } from 'vs/platform/extensions/common/extensions'; + +export class ViewletService implements IViewletService { + + public static readonly ENABLED_EXT_VIEWLETS = 'workbench.viewlet.enabledExtViewlets'; + + public _serviceBrand: any; + + private sidebarPart: ISidebar; + private viewletRegistry: ViewletRegistry; + private enabledExtViewletIds: string[]; + private extViewlets: ViewletDescriptor[]; + private _onDidExtViewletsLoad = new Emitter(); + private _onDidViewletToggle = new Emitter(); + + public get onDidViewletOpen(): Event { return this.sidebarPart.onDidViewletOpen; }; + public get onDidViewletClose(): Event { return this.sidebarPart.onDidViewletClose; }; + public get onDidExtViewletsLoad(): Event { return this._onDidExtViewletsLoad.event; }; + public get onDidViewletToggle(): Event { return this._onDidViewletToggle.event; }; + + constructor( + sidebarPart: ISidebar, + @IStorageService private storageService: IStorageService, + @IExtensionService private extensionService: IExtensionService + ) { + this.sidebarPart = sidebarPart; + this.viewletRegistry = Registry.as(ViewletExtensions.Viewlets); + + const enabledExtViewletsJson = this.storageService.get(ViewletService.ENABLED_EXT_VIEWLETS); + this.enabledExtViewletIds = enabledExtViewletsJson ? JSON.parse(enabledExtViewletsJson) : []; + this.extViewlets = []; + + this.extensionService.onReady().then(() => { + this.onExtensionServiceReady(); + }); + } + + private onExtensionServiceReady(): void { + const viewlets = this.viewletRegistry.getViewlets(); + viewlets.forEach(v => { + if (v.isExternal) { + this.extViewlets.push(v); + } + }); + + this._onDidExtViewletsLoad.fire(); + } + + public openViewlet(id: string, focus?: boolean): TPromise { + return this.sidebarPart.openViewlet(id, focus); + } + + public restoreViewlet(id: string): TPromise { + const shouldFocus = false; + + const stockViewletIds = this.getStockViewlets().map(v => v.id); + const isStockViewlet = stockViewletIds.indexOf(id) !== -1; + if (isStockViewlet) { + return this.sidebarPart.openViewlet(id, shouldFocus); + } else { + return new TPromise(c => { + this.onDidExtViewletsLoad(() => { + // It's possible the external viewlet is uninstalled and not available. + // Restore file explorer in that case. + if (!this.viewletRegistry.getViewlet(id)) { + const defaultViewletId = this.viewletRegistry.getDefaultViewletId(); + this.sidebarPart.openViewlet(defaultViewletId, shouldFocus).then(viewlet => c(viewlet)); + } else { + this.sidebarPart.openViewlet(id, shouldFocus).then(viewlet => c(viewlet)); + } + }); + }); + } + } + + public toggleViewlet(id: string): TPromise { + const index = this.enabledExtViewletIds.indexOf(id); + if (index === -1) { + this.enabledExtViewletIds.push(id); + } else { + this.enabledExtViewletIds.splice(index, 1); + } + + this.setEnabledExtViewlets(); + this._onDidViewletToggle.fire(); + return TPromise.as(null); + } + + public getActiveViewlet(): IViewlet { + return this.sidebarPart.getActiveViewlet(); + } + + public getAllViewlets(): ViewletDescriptor[] { + const stockViewlets = this.getStockViewlets(); + return stockViewlets.concat(this.extViewlets); + } + + public getAllViewletsToDisplay(): ViewletDescriptor[] { + const stockViewlets = this.getStockViewlets(); + const enabledExtViewlets = this.extViewlets + .filter(v => this.enabledExtViewletIds.indexOf(v.id) !== -1) + .sort((v1, v2) => { + return this.enabledExtViewletIds.indexOf(v1.id) - this.enabledExtViewletIds.indexOf(v2.id); + }); + return stockViewlets.concat(enabledExtViewlets); + } + + public isViewletEnabled(id: string): boolean { + return this.enabledExtViewletIds.indexOf(id) !== -1; + } + + // Get an ordered list of all stock viewlets + private getStockViewlets(): ViewletDescriptor[] { + return this.viewletRegistry.getViewlets() + .filter(viewlet => !viewlet.isExternal) + .sort((v1, v2) => v1.order - v2.order); + } + + private setEnabledExtViewlets(): void { + this.storageService.store(ViewletService.ENABLED_EXT_VIEWLETS, JSON.stringify(this.enabledExtViewletIds)); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/viewlet/common/viewletService.ts b/src/vs/workbench/services/viewlet/common/viewletService.ts index dcc0621594a..3611af4dcab 100644 --- a/src/vs/workbench/services/viewlet/common/viewletService.ts +++ b/src/vs/workbench/services/viewlet/common/viewletService.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import Event from 'vs/base/common/event'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; export const IViewletService = createDecorator('viewletService'); @@ -15,8 +16,9 @@ export interface IViewletService { _serviceBrand: ServiceIdentifier; onDidViewletOpen: Event; - onDidViewletClose: Event; + onDidExtViewletsLoad: Event; + onDidViewletToggle: Event; /** * Opens a viewlet with the given identifier and pass keyboard focus to it if specified. @@ -24,7 +26,35 @@ export interface IViewletService { openViewlet(id: string, focus?: boolean): TPromise; /** - * Returns the current active viewlet or null if none + * Restores a viewlet during startup. + * If the viewlet to restore is external, delay restoration until extensions finish loading. + */ + restoreViewlet(id: string): TPromise; + + /** + * Toggles a viewlet with the given identifier. + */ + toggleViewlet(id: string): TPromise; + + /** + * Returns the current active viewlet or null if none. */ getActiveViewlet(): IViewlet; -} \ No newline at end of file + + /** + * Returns all registered viewlets + */ + getAllViewlets(): ViewletDescriptor[]; + + /** + * Returns all viewlets that should be displayed, ordered by: + * - Stock Viewlets: order attribute + * - External Viewlets: enabling sequence + */ + getAllViewletsToDisplay(): ViewletDescriptor[]; + + /** + * Checks if an extension is enabled + */ + isViewletEnabled(id: string): boolean; +} diff --git a/src/vs/workbench/test/browser/services.test.ts b/src/vs/workbench/test/browser/services.test.ts index 73b9e94ddd6..14b7d284c71 100644 --- a/src/vs/workbench/test/browser/services.test.ts +++ b/src/vs/workbench/test/browser/services.test.ts @@ -26,6 +26,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { Position, Direction, IEditor } from 'vs/platform/editor/common/editor'; import { Emitter } from 'vs/base/common/event'; +import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; let activeViewlet: Viewlet = {}; let activeEditor: BaseEditor = { @@ -100,19 +101,43 @@ class TestViewletService implements IViewletService { public _serviceBrand: any; onDidViewletOpenEmitter = new Emitter(); - onDidViewletOpen = this.onDidViewletOpenEmitter.event; - onDidViewletCloseEmitter = new Emitter(); - onDidViewletClose = this.onDidViewletCloseEmitter.event; + onDidExtletsLoadEmitter = new Emitter(); + onDidViewletToggleEmitter = new Emitter(); - public openViewlet(id: string, focus?: boolean): Promise { + onDidViewletOpen = this.onDidViewletOpenEmitter.event; + onDidViewletClose = this.onDidViewletCloseEmitter.event; + onDidExtViewletsLoad = this.onDidExtletsLoadEmitter.event; + onDidViewletToggle = this.onDidViewletToggleEmitter.event; + + public openViewlet(id: string, focus?: boolean): TPromise { return TPromise.as(null); } + public restoreViewlet(id: string): TPromise { + return TPromise.as(null); + } + + public toggleViewlet(id: string): TPromise { + return TPromise.as(null); + } + + public getAllViewlets(): ViewletDescriptor[] { + return []; + } + + public getAllViewletsToDisplay(): ViewletDescriptor[] { + return []; + } + public getActiveViewlet(): IViewlet { return activeViewlet; } + public isViewletEnabled(id: string): boolean { + return true; + } + public dispose() { } diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 314ce9ada2d..342fee04e6b 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -59,6 +59,9 @@ import 'vs/workbench/parts/extensions/electron-browser/extensions.contribution'; import 'vs/workbench/parts/extensions/browser/extensionsQuickOpen'; import 'vs/workbench/parts/extensions/electron-browser/extensionsViewlet'; // can be packaged separately +import 'vs/workbench/parts/explorers/browser/treeExplorer.contribution'; +import 'vs/workbench/parts/explorers/common/treeExplorerActions.contribution'; + import 'vs/workbench/parts/output/browser/output.contribution'; import 'vs/workbench/parts/output/browser/outputPanel'; // can be packaged separately