From 8f92dcd2bc33b276ffab3a96a0bc886c5ca3a597 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 28 Nov 2016 10:17:41 +0100 Subject: [PATCH] Manage extension from status bar and viewlets (for #15155) --- .../base/browser/ui/actionbar/actionbar.css | 1 - src/vs/platform/statusbar/common/statusbar.ts | 5 + src/vs/workbench/api/node/extHost.api.impl.ts | 2 +- src/vs/workbench/api/node/extHost.protocol.ts | 2 +- src/vs/workbench/api/node/extHostStatusBar.ts | 13 +- .../workbench/api/node/mainThreadStatusBar.ts | 4 +- .../api/node/mainThreadTreeExplorers.ts | 4 +- .../parts/activitybar/activityAction.ts | 127 +++++++++++++++--- .../parts/activitybar/activitybarPart.ts | 94 ++++--------- .../browser/parts/statusbar/statusbarPart.ts | 34 +++++ src/vs/workbench/browser/viewlet.ts | 8 +- .../workbench/electron-browser/integration.ts | 2 +- .../explorers/{ => browser}/media/Refresh.svg | 0 .../{ => browser}/media/Refresh_inverse.svg | 0 .../media/treeExplorer.contribution.css | 0 .../browser/treeExplorer.contribution.ts | 12 +- ...lorerService.ts => treeExplorerService.ts} | 4 +- .../browser/views/treeExplorerView.ts | 12 +- .../browser/views/treeExplorerViewer.ts | 10 +- ...lorerService.ts => treeExplorerService.ts} | 4 +- .../extensions.contribution.ts | 14 +- .../viewlet/browser/viewletService.ts | 4 +- 22 files changed, 229 insertions(+), 127 deletions(-) rename src/vs/workbench/parts/explorers/{ => browser}/media/Refresh.svg (100%) rename src/vs/workbench/parts/explorers/{ => browser}/media/Refresh_inverse.svg (100%) rename src/vs/workbench/parts/explorers/{ => browser}/media/treeExplorer.contribution.css (100%) rename src/vs/workbench/parts/explorers/browser/{customTreeExplorerService.ts => treeExplorerService.ts} (93%) rename src/vs/workbench/parts/explorers/common/{customTreeExplorerService.ts => treeExplorerService.ts} (87%) diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 6a0a66de8f3..81c86cb8eff 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -71,7 +71,6 @@ } .monaco-action-bar.vertical .action-item { - padding-bottom: 0.1em; display: block; } diff --git a/src/vs/platform/statusbar/common/statusbar.ts b/src/vs/platform/statusbar/common/statusbar.ts index b5f9a3c0e5d..6e45f806115 100644 --- a/src/vs/platform/statusbar/common/statusbar.ts +++ b/src/vs/platform/statusbar/common/statusbar.ts @@ -40,6 +40,11 @@ export interface IStatusbarEntry { * An optional id of a command that is known to the workbench to execute on click */ command?: string; + + /** + * An optional extension ID if this entry is provided from an extension. + */ + extensionId?: string; } export interface IStatusbarService { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index be5a2dfb6b0..bc4b9611d82 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -269,7 +269,7 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ return extHostQuickOpen.showInput(options, token); }, createStatusBarItem(position?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { - return extHostStatusBar.createStatusBarEntry(position, priority); + return extHostStatusBar.createStatusBarEntry(extension.id, position, priority); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 43d81718bf8..6637c5b3e35 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -201,7 +201,7 @@ export abstract class MainThreadQuickOpenShape { } export abstract class MainThreadStatusBarShape { - $setEntry(id: number, text: string, tooltip: string, command: string, color: string, alignment: MainThreadStatusBarAlignment, priority: number): void { throw ni(); } + $setEntry(id: number, extensionId: string, text: string, tooltip: string, command: string, color: string, alignment: MainThreadStatusBarAlignment, priority: number): void { throw ni(); } $dispose(id: number) { throw ni(); } } diff --git a/src/vs/workbench/api/node/extHostStatusBar.ts b/src/vs/workbench/api/node/extHostStatusBar.ts index aa695ebe033..97c917caf51 100644 --- a/src/vs/workbench/api/node/extHostStatusBar.ts +++ b/src/vs/workbench/api/node/extHostStatusBar.ts @@ -27,11 +27,14 @@ export class ExtHostStatusBarEntry implements StatusBarItem { private _timeoutHandle: number; private _proxy: MainThreadStatusBarShape; - constructor(proxy: MainThreadStatusBarShape, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + private _extensionId: string; + + constructor(proxy: MainThreadStatusBarShape, extensionId: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { this._id = ExtHostStatusBarEntry.ID_GEN++; this._proxy = proxy; this._alignment = alignment; this._priority = priority; + this._extensionId = extensionId; } public get id(): number { @@ -105,7 +108,7 @@ export class ExtHostStatusBarEntry implements StatusBarItem { this._timeoutHandle = undefined; // Set to status bar - this._proxy.$setEntry(this.id, this.text, this.tooltip, this.command, this.color, + this._proxy.$setEntry(this.id, this._extensionId, this.text, this.tooltip, this.command, this.color, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority); }, 0); @@ -123,7 +126,7 @@ class StatusBarMessage { private _messages: { message: string }[] = []; constructor(statusBar: ExtHostStatusBar) { - this._item = statusBar.createStatusBarEntry(ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); + this._item = statusBar.createStatusBarEntry(void 0, ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); } dispose() { @@ -165,8 +168,8 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(alignment?: ExtHostStatusBarAlignment, priority?: number): StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, alignment, priority); + createStatusBarEntry(extensionId: string, alignment?: ExtHostStatusBarAlignment, priority?: number): StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, extensionId, alignment, priority); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/node/mainThreadStatusBar.ts b/src/vs/workbench/api/node/mainThreadStatusBar.ts index dd5b87b78ab..ed435c97485 100644 --- a/src/vs/workbench/api/node/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/node/mainThreadStatusBar.ts @@ -18,13 +18,13 @@ export class MainThreadStatusBar extends MainThreadStatusBarShape { this.mapIdToDisposable = Object.create(null); } - $setEntry(id: number, text: string, tooltip: string, command: string, color: string, alignment: MainThreadStatusBarAlignment, priority: number): void { + $setEntry(id: number, extensionId: string, text: string, tooltip: string, command: string, color: string, alignment: MainThreadStatusBarAlignment, priority: number): void { // Dispose any old this.$dispose(id); // Add new - let disposeable = this.statusbarService.addEntry({ text, tooltip, command, color }, alignment, priority); + let disposeable = this.statusbarService.addEntry({ text, tooltip, command, color, extensionId }, alignment, priority); this.mapIdToDisposable[id] = disposeable; } diff --git a/src/vs/workbench/api/node/mainThreadTreeExplorers.ts b/src/vs/workbench/api/node/mainThreadTreeExplorers.ts index 1116abd95c4..c0f8ded4be4 100644 --- a/src/vs/workbench/api/node/mainThreadTreeExplorers.ts +++ b/src/vs/workbench/api/node/mainThreadTreeExplorers.ts @@ -7,7 +7,7 @@ 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/common/customTreeExplorerService'; +import { ITreeExplorerService } from 'vs/workbench/parts/explorers/common/treeExplorerService'; 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'; @@ -17,7 +17,7 @@ export class MainThreadTreeExplorers extends MainThreadTreeExplorersShape { constructor( @IThreadService private threadService: IThreadService, - @ICustomTreeExplorerService private treeExplorerService: ICustomTreeExplorerService, + @ITreeExplorerService private treeExplorerService: ITreeExplorerService, @IMessageService private messageService: IMessageService, @ICommandService private commandService: ICommandService ) { diff --git a/src/vs/workbench/browser/parts/activitybar/activityAction.ts b/src/vs/workbench/browser/parts/activitybar/activityAction.ts index 7d2094c0866..f828641da67 100644 --- a/src/vs/workbench/browser/parts/activitybar/activityAction.ts +++ b/src/vs/workbench/browser/parts/activitybar/activityAction.ts @@ -7,15 +7,25 @@ import 'vs/css!./media/activityaction'; import nls = require('vs/nls'); +import DOM = require('vs/base/browser/dom'); +import errors = require('vs/base/common/errors'); +import { TPromise } from 'vs/base/common/winjs.base'; import { Builder, $ } from 'vs/base/browser/builder'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { Action } from 'vs/base/common/actions'; import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { ProgressBadge, TextBadge, NumberBadge, IconBadge, IBadge } from 'vs/workbench/services/activity/common/activityService'; import Event, { Emitter } from 'vs/base/common/event'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; export class ActivityAction extends Action { - private badge: IBadge; private _onDidChangeBadge = new Emitter(); @@ -51,6 +61,49 @@ export class ActivityAction extends Action { } } +export class ViewletActivityAction extends ActivityAction { + + private static preventDoubleClickDelay = 300; + + private lastRun: number = 0; + + constructor( + id: string, + private viewlet: ViewletDescriptor, + @IViewletService private viewletService: IViewletService, + @IPartService private partService: IPartService + ) { + super(id, viewlet.name, viewlet.cssClass); + } + + public run(event): TPromise { + if (event instanceof MouseEvent && event.button === 2) { + return TPromise.as(false); // do not run on right click + } + + // prevent accident trigger on a doubleclick (to help nervous people) + const now = Date.now(); + if (now - this.lastRun < ViewletActivityAction.preventDoubleClickDelay) { + return TPromise.as(true); + } + this.lastRun = now; + + const sideBarVisible = this.partService.isVisible(Parts.SIDEBAR_PART); + const activeViewlet = this.viewletService.getActiveViewlet(); + + // Hide sidebar if selected viewlet already visible + if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) { + this.partService.setSideBarHidden(true); + } else { + this.viewletService.openViewlet(this.viewlet.id, true).done(null, errors.onUnexpectedError); + this.activate(); + } + + return TPromise.as(true); + } +} + +let manageExtensionAction: ManageExtensionAction; export class ActivityActionItem extends BaseActionItem { private $e: Builder; @@ -59,14 +112,34 @@ export class ActivityActionItem extends BaseActionItem { private cssClass: string; private $badge: Builder; private $badgeContent: Builder; + private toDispose: IDisposable[]; - constructor(action: ActivityAction, activityName: string = action.label, keybinding: string = null) { + constructor( + action: ActivityAction, + private viewlet: ViewletDescriptor, + @IContextMenuService private contextMenuService: IContextMenuService, + @IKeybindingService private keybindingService: IKeybindingService, + @IInstantiationService instantiationService: IInstantiationService + ) { super(null, action); this.cssClass = action.class; - this.name = activityName; - this._keybinding = keybinding; - action.onDidChangeBadge(this._handleBadgeChangeEvenet, this, this._callOnDispose); + this.name = viewlet.name; + this._keybinding = this.getKeybindingLabel(viewlet.id); + action.onDidChangeBadge(this.handleBadgeChangeEvenet, this, this._callOnDispose); + + if (!manageExtensionAction) { + manageExtensionAction = instantiationService.createInstance(ManageExtensionAction); + } + } + + private getKeybindingLabel(id: string): string { + const keys = this.keybindingService.lookupKeybindings(id).map(k => this.keybindingService.getLabelFor(k)); + if (keys && keys.length) { + return keys[0]; + } + + return null; } public render(container: HTMLElement): void { @@ -77,6 +150,18 @@ export class ActivityActionItem extends BaseActionItem { role: 'button' }).appendTo(this.builder); + if (this.viewlet.extensionId) { + $(container).on('contextmenu', e => { + DOM.EventHelper.stop(e, true); + + this.contextMenuService.showContextMenu({ + getAnchor: () => container, + getActionsContext: () => this.viewlet.extensionId, + getActions: () => TPromise.as([manageExtensionAction]) + }); + }, this.toDispose); + } + if (this.cssClass) { this.$e.addClass(this.cssClass); } @@ -113,7 +198,6 @@ export class ActivityActionItem extends BaseActionItem { } let title: string; - if (keybinding) { title = nls.localize('titleKeybinding', "{0} ({1})", this.name, keybinding); } else { @@ -132,17 +216,15 @@ export class ActivityActionItem extends BaseActionItem { // Number if (badge instanceof NumberBadge) { - let n = (badge).number; - - if (n) { - this.$badgeContent.text(n > 99 ? '99+' : n.toString()); + if (badge.number) { + this.$badgeContent.text(badge.number > 99 ? '99+' : badge.number.toString()); this.$badge.show(); } } // Text else if (badge instanceof TextBadge) { - this.$badgeContent.text((badge).text); + this.$badgeContent.text(badge.text); this.$badge.show(); } @@ -156,7 +238,7 @@ export class ActivityActionItem extends BaseActionItem { this.$badge.show(); } - this.$e.attr('aria-label', this.name + ' - ' + badge.getDescription()); + this.$e.attr('aria-label', `${this.name} - ${badge.getDescription()}`); } } @@ -177,10 +259,10 @@ export class ActivityActionItem extends BaseActionItem { } } - private _handleBadgeChangeEvenet(): void { - let action = this.getAction(); + private handleBadgeChangeEvenet(): void { + const action = this.getAction(); if (action instanceof ActivityAction) { - this.updateBadge((action).getBadge()); + this.updateBadge(action.getBadge()); } } @@ -195,7 +277,22 @@ export class ActivityActionItem extends BaseActionItem { public dispose(): void { super.dispose(); + dispose(this.toDispose); + this.$badge.destroy(); this.$e.destroy(); } +} + +class ManageExtensionAction extends Action { + + constructor( + @ICommandService private commandService: ICommandService + ) { + super('statusbar.manage.extension', nls.localize('manageExtension', "Manage Extension")); + } + + public run(extensionId: string): TPromise { + return this.commandService.executeCommand('_extensions.manage', extensionId); + } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index ad8e8f2b76a..2495758d828 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -7,18 +7,16 @@ import 'vs/css!./media/activitybarpart'; import nls = require('vs/nls'); -import { TPromise } from 'vs/base/common/winjs.base'; 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 { IComposite } from 'vs/workbench/common/composite'; 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 { ViewletActivityAction, ActivityAction, ActivityActionItem } from 'vs/workbench/browser/parts/activitybar/activityAction'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IActivityService, IBadge } from 'vs/workbench/services/activity/common/activityService'; -import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; +import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; @@ -28,7 +26,7 @@ export class ActivitybarPart extends Part implements IActivityService { private viewletSwitcherBar: ActionBar; private activityActionItems: { [actionId: string]: IActionItem; }; - private compositeIdToActions: { [compositeId: string]: ActivityAction; }; + private viewletIdToActions: { [viewletId: string]: ActivityAction; }; constructor( id: string, @@ -41,7 +39,7 @@ export class ActivitybarPart extends Part implements IActivityService { super(id); this.activityActionItems = {}; - this.compositeIdToActions = {}; + this.viewletIdToActions = {}; // Update viewlet switcher when external viewlets become ready this.extensionService.onReady().then(() => this.updateViewletSwitcher()); @@ -59,19 +57,19 @@ export class ActivitybarPart extends Part implements IActivityService { } private onActiveCompositeChanged(composite: IComposite): void { - if (this.compositeIdToActions[composite.getId()]) { - this.compositeIdToActions[composite.getId()].activate(); + if (this.viewletIdToActions[composite.getId()]) { + this.viewletIdToActions[composite.getId()].activate(); } } private onCompositeClosed(composite: IComposite): void { - if (this.compositeIdToActions[composite.getId()]) { - this.compositeIdToActions[composite.getId()].deactivate(); + if (this.viewletIdToActions[composite.getId()]) { + this.viewletIdToActions[composite.getId()].deactivate(); } } - public showActivity(compositeId: string, badge: IBadge, clazz?: string): void { - const action = this.compositeIdToActions[compositeId]; + public showActivity(viewletId: string, badge: IBadge, clazz?: string): void { + const action = this.viewletIdToActions[viewletId]; if (action) { action.setBadge(badge); if (clazz) { @@ -80,8 +78,8 @@ export class ActivitybarPart extends Part implements IActivityService { } } - public clearActivity(compositeId: string): void { - this.showActivity(compositeId, null); + public clearActivity(viewletId: string): void { + this.showActivity(viewletId, null); } public createContentArea(parent: Builder): Builder { @@ -109,7 +107,7 @@ export class ActivitybarPart extends Part implements IActivityService { // Pull out viewlets no longer needed const newViewletIds = viewlets.map(v => v.id); - const existingViewletIds = Object.keys(this.compositeIdToActions); + const existingViewletIds = Object.keys(this.viewletIdToActions); existingViewletIds.forEach(viewletId => { if (newViewletIds.indexOf(viewletId) === -1) { this.pullViewlet(viewletId); @@ -118,7 +116,7 @@ export class ActivitybarPart extends Part implements IActivityService { // Built actions for viewlets to show const actionsToPush = viewlets - .filter(viewlet => !this.compositeIdToActions[viewlet.id]) + .filter(viewlet => !this.viewletIdToActions[viewlet.id]) .map(viewlet => this.toAction(viewlet)); // Add to viewlet switcher @@ -127,7 +125,7 @@ export class ActivitybarPart extends Part implements IActivityService { // Make sure to activate the active one const activeViewlet = this.viewletService.getActiveViewlet(); if (activeViewlet) { - const activeViewletEntry = this.compositeIdToActions[activeViewlet.getId()]; + const activeViewletEntry = this.viewletIdToActions[activeViewlet.getId()]; if (activeViewletEntry) { activeViewletEntry.activate(); } @@ -135,11 +133,11 @@ export class ActivitybarPart extends Part implements IActivityService { } private pullViewlet(viewletId: string): void { - const index = Object.keys(this.compositeIdToActions).indexOf(viewletId); + const index = Object.keys(this.viewletIdToActions).indexOf(viewletId); - const action = this.compositeIdToActions[viewletId]; + const action = this.viewletIdToActions[viewletId]; action.dispose(); - delete this.compositeIdToActions[viewletId]; + delete this.viewletIdToActions[viewletId]; const actionItem = this.activityActionItems[action.id]; actionItem.dispose(); @@ -148,24 +146,15 @@ export class ActivitybarPart extends Part implements IActivityService { this.viewletSwitcherBar.pull(index); } - private toAction(composite: ViewletDescriptor): ActivityAction { - const action = this.instantiationService.createInstance(ViewletActivityAction, `${composite.id}.activity-bar-action`, composite); + private toAction(viewlet: ViewletDescriptor): ActivityAction { + const action = this.instantiationService.createInstance(ViewletActivityAction, `${viewlet.id}.activity-bar-action`, viewlet); - this.activityActionItems[action.id] = new ActivityActionItem(action, composite.name, this.getKeybindingLabel(composite.id)); - this.compositeIdToActions[composite.id] = action; + this.activityActionItems[action.id] = this.instantiationService.createInstance(ActivityActionItem, action, viewlet); + this.viewletIdToActions[viewlet.id] = action; return action; }; - private getKeybindingLabel(id: string): string { - const keys = this.keybindingService.lookupKeybindings(id).map(k => this.keybindingService.getLabelFor(k)); - if (keys && keys.length) { - return keys[0]; - } - - return null; - } - public dispose(): void { if (this.viewletSwitcherBar) { this.viewletSwitcherBar.dispose(); @@ -174,41 +163,4 @@ export class ActivitybarPart extends Part implements IActivityService { super.dispose(); } -} - -class ViewletActivityAction extends ActivityAction { - private static preventDoubleClickDelay = 300; - private lastRun: number = 0; - - constructor( - id: string, - private viewlet: ViewletDescriptor, - @IViewletService private viewletService: IViewletService, - @IPartService private partService: IPartService - ) { - super(id, viewlet.name, viewlet.cssClass); - } - - public run(): TPromise { - - // prevent accident trigger on a doubleclick (to help nervous people) - const now = Date.now(); - if (now - this.lastRun < ViewletActivityAction.preventDoubleClickDelay) { - return TPromise.as(true); - } - this.lastRun = now; - - const sideBarVisible = this.partService.isVisible(Parts.SIDEBAR_PART); - const activeViewlet = this.viewletService.getActiveViewlet(); - - // Hide sidebar if selected viewlet already visible - if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) { - this.partService.setSideBarHidden(true); - } else { - this.viewletService.openViewlet(this.viewlet.id, true).done(null, errors.onUnexpectedError); - this.activate(); - } - - return TPromise.as(true); - } -} +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 8523116bda5..3be9c056516 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -24,6 +24,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { IStatusbarService, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; import { getCodeEditor } from 'vs/editor/common/services/codeEditorService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { Action } from 'vs/base/common/actions'; export class StatusbarPart extends Part implements IStatusbarService { @@ -184,6 +186,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } } +let manageExtensionAction: ManageExtensionAction; class StatusBarEntryItem implements IStatusbarItem { private entry: IStatusbarEntry; @@ -193,9 +196,14 @@ class StatusBarEntryItem implements IStatusbarItem { @IInstantiationService private instantiationService: IInstantiationService, @IMessageService private messageService: IMessageService, @ITelemetryService private telemetryService: ITelemetryService, + @IContextMenuService private contextMenuService: IContextMenuService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService ) { this.entry = entry; + + if (!manageExtensionAction) { + manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); + } } public render(el: HTMLElement): IDisposable { @@ -225,6 +233,19 @@ class StatusBarEntryItem implements IStatusbarItem { $(textContainer).color(this.entry.color); } + // Context Menu + if (this.entry.extensionId) { + $(textContainer).on('contextmenu', e => { + dom.EventHelper.stop(e, true); + + this.contextMenuService.showContextMenu({ + getAnchor: () => el, + getActionsContext: () => this.entry.extensionId, + getActions: () => TPromise.as([manageExtensionAction]) + }); + }, toDispose); + } + el.appendChild(textContainer); return { @@ -264,3 +285,16 @@ class StatusBarEntryItem implements IStatusbarItem { this.commandService.executeCommand(id).done(undefined, err => this.messageService.show(Severity.Error, toErrorMessage(err))); } } + +class ManageExtensionAction extends Action { + + constructor( + @ICommandService private commandService: ICommandService + ) { + super('statusbar.manage.extension', nls.localize('manageExtension', "Manage Extension")); + } + + public run(extensionId: string): TPromise { + return this.commandService.executeCommand('_extensions.manage', extensionId); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 26c750ed022..f39bf7369bc 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -160,17 +160,17 @@ export class ViewletDescriptor extends CompositeDescriptor { name: string, cssClass?: string, order?: number, - private _fromExtension = false + private _extensionId?: string ) { super(moduleId, ctorName, id, name, cssClass, order); - if (_fromExtension) { + if (_extensionId) { this.appendStaticArguments([id]); // Pass viewletId to external viewlet, which doesn't know its id until runtime. } } - public get fromExtension(): boolean { - return this._fromExtension; + public get extensionId(): string { + return this._extensionId; } } diff --git a/src/vs/workbench/electron-browser/integration.ts b/src/vs/workbench/electron-browser/integration.ts index 862198cae98..7818d5d6fae 100644 --- a/src/vs/workbench/electron-browser/integration.ts +++ b/src/vs/workbench/electron-browser/integration.ts @@ -114,7 +114,7 @@ export class ElectronIntegration { // Send over all extension viewlets when extensions are ready this.extensionService.onReady().then(() => { - ipc.send('vscode:extensionViewlets', JSON.stringify(this.viewletService.getViewlets().filter(v => v.fromExtension).map(v => { return { id: v.id, label: v.name }; }))); + ipc.send('vscode:extensionViewlets', JSON.stringify(this.viewletService.getViewlets().filter(v => !!v.extensionId).map(v => { return { id: v.id, label: v.name }; }))); }); ipc.on('vscode:reportError', (event, error) => { diff --git a/src/vs/workbench/parts/explorers/media/Refresh.svg b/src/vs/workbench/parts/explorers/browser/media/Refresh.svg similarity index 100% rename from src/vs/workbench/parts/explorers/media/Refresh.svg rename to src/vs/workbench/parts/explorers/browser/media/Refresh.svg diff --git a/src/vs/workbench/parts/explorers/media/Refresh_inverse.svg b/src/vs/workbench/parts/explorers/browser/media/Refresh_inverse.svg similarity index 100% rename from src/vs/workbench/parts/explorers/media/Refresh_inverse.svg rename to src/vs/workbench/parts/explorers/browser/media/Refresh_inverse.svg diff --git a/src/vs/workbench/parts/explorers/media/treeExplorer.contribution.css b/src/vs/workbench/parts/explorers/browser/media/treeExplorer.contribution.css similarity index 100% rename from src/vs/workbench/parts/explorers/media/treeExplorer.contribution.css rename to src/vs/workbench/parts/explorers/browser/media/treeExplorer.contribution.css diff --git a/src/vs/workbench/parts/explorers/browser/treeExplorer.contribution.ts b/src/vs/workbench/parts/explorers/browser/treeExplorer.contribution.ts index 474caf43f57..a5f958a63b6 100644 --- a/src/vs/workbench/parts/explorers/browser/treeExplorer.contribution.ts +++ b/src/vs/workbench/parts/explorers/browser/treeExplorer.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import 'vs/css!../media/treeExplorer.contribution'; +import 'vs/css!./media/treeExplorer.contribution'; import { localize } from 'vs/nls'; import { join } from 'vs/base/common/paths'; @@ -12,8 +12,8 @@ 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 } from 'vs/workbench/parts/explorers/common/customTreeExplorerService'; -import { CustomTreeExplorerService } from 'vs/workbench/parts/explorers/browser/customTreeExplorerService'; +import { ITreeExplorerService } from 'vs/workbench/parts/explorers/common/treeExplorerService'; +import { TreeExplorerService } from 'vs/workbench/parts/explorers/browser/treeExplorerService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ToggleViewletAction } from 'vs/workbench/browser/viewlet'; import { ITreeExplorer } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -24,7 +24,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -registerSingleton(ICustomTreeExplorerService, CustomTreeExplorerService); +registerSingleton(ITreeExplorerService, TreeExplorerService); const explorerSchema: IJSONSchema = { description: localize('vscode.extension.contributes.explorer', 'Contributes custom tree explorer viewlet to the sidebar'), @@ -103,8 +103,8 @@ export class ExtensionExplorersContribtion implements IWorkbenchContribution { viewletId, treeLabel, viewletCSSClass, - -1, // External viewlets are ordered by enabling sequence, so order here doesn't matter. - true // from extension + -1, + extension.description.id )); } }); diff --git a/src/vs/workbench/parts/explorers/browser/customTreeExplorerService.ts b/src/vs/workbench/parts/explorers/browser/treeExplorerService.ts similarity index 93% rename from src/vs/workbench/parts/explorers/browser/customTreeExplorerService.ts rename to src/vs/workbench/parts/explorers/browser/treeExplorerService.ts index 9983ec6ccc8..28db0e0c146 100644 --- a/src/vs/workbench/parts/explorers/browser/customTreeExplorerService.ts +++ b/src/vs/workbench/parts/explorers/browser/treeExplorerService.ts @@ -10,9 +10,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { InternalTreeExplorerNode, InternalTreeExplorerNodeProvider } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; -import { ICustomTreeExplorerService } from 'vs/workbench/parts/explorers/common/customTreeExplorerService'; +import { ITreeExplorerService } from 'vs/workbench/parts/explorers/common/treeExplorerService'; -export class CustomTreeExplorerService implements ICustomTreeExplorerService { +export class TreeExplorerService implements ITreeExplorerService { public _serviceBrand: any; private _onTreeExplorerNodeProviderRegistered = new Emitter(); diff --git a/src/vs/workbench/parts/explorers/browser/views/treeExplorerView.ts b/src/vs/workbench/parts/explorers/browser/views/treeExplorerView.ts index 5024a6f1baa..e8326f1461b 100644 --- a/src/vs/workbench/parts/explorers/browser/views/treeExplorerView.ts +++ b/src/vs/workbench/parts/explorers/browser/views/treeExplorerView.ts @@ -15,7 +15,7 @@ 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ICustomTreeExplorerService } from 'vs/workbench/parts/explorers/common/customTreeExplorerService'; +import { ITreeExplorerService } from 'vs/workbench/parts/explorers/common/treeExplorerService'; 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'; @@ -35,7 +35,7 @@ export class TreeExplorerView extends CollapsibleViewletView { @IContextMenuService contextMenuService: IContextMenuService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IInstantiationService private instantiationService: IInstantiationService, - @ICustomTreeExplorerService private treeExplorerViewletService: ICustomTreeExplorerService, + @ITreeExplorerService private treeExplorerService: ITreeExplorerService, @IProgressService private progressService: IProgressService ) { super(actionRunner, false, nls.localize('treeExplorerViewlet.tree', "Tree Explorer Section"), messageService, keybindingService, contextMenuService, headerSize); @@ -86,8 +86,8 @@ export class TreeExplorerView extends CollapsibleViewletView { } public updateInput(): TPromise { - if (this.treeExplorerViewletService.hasProvider(this.treeNodeProviderId)) { - return this.treeExplorerViewletService.provideRootNode(this.treeNodeProviderId).then(tree => { + if (this.treeExplorerService.hasProvider(this.treeNodeProviderId)) { + return this.treeExplorerService.provideRootNode(this.treeNodeProviderId).then(tree => { this.tree.setInput(tree); }); } @@ -96,9 +96,9 @@ export class TreeExplorerView extends CollapsibleViewletView { // is registered. // This renders the viewlet first and wait for a corresponding provider is registered. else { - this.treeExplorerViewletService.onTreeExplorerNodeProviderRegistered(providerId => { + this.treeExplorerService.onTreeExplorerNodeProviderRegistered(providerId => { if (this.treeNodeProviderId === providerId) { - return this.treeExplorerViewletService.provideRootNode(this.treeNodeProviderId).then(tree => { + return this.treeExplorerService.provideRootNode(this.treeNodeProviderId).then(tree => { this.tree.setInput(tree); }); } diff --git a/src/vs/workbench/parts/explorers/browser/views/treeExplorerViewer.ts b/src/vs/workbench/parts/explorers/browser/views/treeExplorerViewer.ts index 27fc03aa8a1..6ed86f1c419 100644 --- a/src/vs/workbench/parts/explorers/browser/views/treeExplorerViewer.ts +++ b/src/vs/workbench/parts/explorers/browser/views/treeExplorerViewer.ts @@ -16,14 +16,14 @@ import { ContributableActionProvider } from 'vs/workbench/browser/actionBarRegis 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/common/customTreeExplorerService'; +import { ITreeExplorerService } from 'vs/workbench/parts/explorers/common/treeExplorerService'; import { IProgressService } from 'vs/platform/progress/common/progress'; export class TreeDataSource implements IDataSource { constructor( private treeNodeProviderId: string, - @ICustomTreeExplorerService private treeExplorerViewletService: ICustomTreeExplorerService, + @ITreeExplorerService private treeExplorerService: ITreeExplorerService, @IProgressService private progressService: IProgressService ) { @@ -38,7 +38,7 @@ export class TreeDataSource implements IDataSource { } public getChildren(tree: ITree, node: InternalTreeExplorerNode): TPromise { - const promise = this.treeExplorerViewletService.resolveChildren(this.treeNodeProviderId, node); + const promise = this.treeExplorerService.resolveChildren(this.treeNodeProviderId, node); this.progressService.showWhile(promise, 800); @@ -89,7 +89,7 @@ export class TreeController extends DefaultController { constructor( private treeNodeProviderId: string, - @ICustomTreeExplorerService private treeExplorerViewletService: ICustomTreeExplorerService + @ITreeExplorerService private treeExplorerService: ITreeExplorerService ) { super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }); } @@ -98,7 +98,7 @@ export class TreeController extends DefaultController { super.onLeftClick(tree, node, event, origin); if (node.clickCommand) { - this.treeExplorerViewletService.executeCommand(this.treeNodeProviderId, node); + this.treeExplorerService.executeCommand(this.treeNodeProviderId, node); } return true; diff --git a/src/vs/workbench/parts/explorers/common/customTreeExplorerService.ts b/src/vs/workbench/parts/explorers/common/treeExplorerService.ts similarity index 87% rename from src/vs/workbench/parts/explorers/common/customTreeExplorerService.ts rename to src/vs/workbench/parts/explorers/common/treeExplorerService.ts index 00a402e68c9..1ce4896a478 100644 --- a/src/vs/workbench/parts/explorers/common/customTreeExplorerService.ts +++ b/src/vs/workbench/parts/explorers/common/treeExplorerService.ts @@ -9,9 +9,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { InternalTreeExplorerNode, InternalTreeExplorerNodeProvider } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; -export const ICustomTreeExplorerService = createDecorator('customTreeExplorerService'); +export const ITreeExplorerService = createDecorator('treeExplorerService'); -export interface ICustomTreeExplorerService { +export interface ITreeExplorerService { _serviceBrand: any; onTreeExplorerNodeProviderRegistered: Event; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts index 30c5172ee4c..08c8cf09980 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -5,6 +5,7 @@ import 'vs/css!./media/extensions'; import { localize } from 'vs/nls'; +import * as errors from 'vs/base/common/errors'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/platform'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; @@ -34,6 +35,8 @@ import { IQuickOpenRegistry, Extensions, QuickOpenHandlerDescriptor } from 'vs/w import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import jsonContributionRegistry = require('vs/platform/jsonschemas/common/jsonContributionRegistry'); import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/parts/extensions/common/extensionsFileTemplate'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; // Singletons registerSingleton(IExtensionGalleryService, ExtensionGalleryService); @@ -161,4 +164,13 @@ Registry.as(ConfigurationExtensions.Configuration) }); const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); -jsonRegistry.registerSchema(ExtensionsConfigurationSchemaId, ExtensionsConfigurationSchema); \ No newline at end of file +jsonRegistry.registerSchema(ExtensionsConfigurationSchemaId, ExtensionsConfigurationSchema); + +// Register Commands +CommandsRegistry.registerCommand('_extensions.manage', (accessor: ServicesAccessor, extensionId: string) => { + const extensionService = accessor.get(IExtensionsWorkbenchService); + const extension = extensionService.local.filter(e => e.identifier === extensionId); + if (extension.length === 1) { + extensionService.open(extension[0]).done(null, errors.onUnexpectedError); + } +}); \ No newline at end of file diff --git a/src/vs/workbench/services/viewlet/browser/viewletService.ts b/src/vs/workbench/services/viewlet/browser/viewletService.ts index 45e46aa42f5..fc4f3188875 100644 --- a/src/vs/workbench/services/viewlet/browser/viewletService.ts +++ b/src/vs/workbench/services/viewlet/browser/viewletService.ts @@ -47,7 +47,7 @@ export class ViewletService implements IViewletService { this.extensionService.onReady().then(() => { const viewlets = this.viewletRegistry.getViewlets(); viewlets.forEach(v => { - if (v.fromExtension) { + if (!!v.extensionId) { this.extensionViewlets.push(v); } }); @@ -88,7 +88,7 @@ export class ViewletService implements IViewletService { private getBuiltInViewlets(): ViewletDescriptor[] { return this.viewletRegistry.getViewlets() - .filter(viewlet => !viewlet.fromExtension) + .filter(viewlet => !viewlet.extensionId) .sort((v1, v2) => v1.order - v2.order); } } \ No newline at end of file