diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index f104258997a942f7b932be8557b1c659cd037ccd..0bdcb986ad67d44b6013913b6cd3dc851fc30f85 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -13,6 +13,7 @@ import strings = require('vs/base/common/strings'); import {IAction} from 'vs/base/common/actions'; import {IXHRResponse} from 'vs/base/common/http'; import Severity from 'vs/base/common/severity'; +import { TPromise } from 'vs/base/common/winjs.base'; export interface ErrorListenerCallback { (error: any): void; @@ -88,6 +89,10 @@ export function onUnexpectedError(e: any): void { } } +export function onUnexpectedPromiseError(promise: TPromise): TPromise { + return promise.then(null, onUnexpectedError); +} + export interface IConnectionErrorData { status: number; statusText?: string; diff --git a/src/vs/workbench/parts/extensions/common/extensions.ts b/src/vs/workbench/parts/extensions/common/extensions.ts index 1e1f4c554c063664ad213ca76956f5e9dbdc31fa..2a8eb0cfa4b4f7b8f8383263bfcaaa07b464b211 100644 --- a/src/vs/workbench/parts/extensions/common/extensions.ts +++ b/src/vs/workbench/parts/extensions/common/extensions.ts @@ -5,6 +5,7 @@ 'use strict'; +import nls = require('vs/nls'); import { TPromise } from 'vs/base/common/winjs.base'; import Event from 'vs/base/common/event'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -52,7 +53,7 @@ export interface IGalleryService { export interface IExtensionsService { serviceId: ServiceIdentifier; onInstallExtension: Event; - onDidInstallExtension: Event; + onDidInstallExtension: Event<{ extension: IExtension; error?: Error; }>; onUninstallExtension: Event; onDidUninstallExtension: Event; @@ -67,4 +68,6 @@ export var IExtensionTipsService = createDecorator('exten export interface IExtensionTipsService { serviceId: ServiceIdentifier; getRecommendations(): TPromise; -} \ No newline at end of file +} + +export var commandCategory = nls.localize('extensionsCategory', "Extensions"); \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/common/extensionsUtil.ts b/src/vs/workbench/parts/extensions/common/extensionsUtil.ts new file mode 100644 index 0000000000000000000000000000000000000000..0da4dfaccd75145381993d1f427fdb6d346ff940 --- /dev/null +++ b/src/vs/workbench/parts/extensions/common/extensionsUtil.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IExtension, IExtensionsService, IGalleryService } from 'vs/workbench/parts/extensions/common/extensions'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as semver from 'semver'; + +'use strict'; + +export function extensionEquals(one: IExtension, other: IExtension): boolean { + return one.publisher === other.publisher && one.name === other.name; +} + +export function getOutdatedExtensions(accessor: ServicesAccessor): TPromise { + const extensionsService = accessor.get(IExtensionsService); + const galleryService = accessor.get(IGalleryService); + + if (!galleryService.isEnabled()) { + return TPromise.as([]); + } + + return TPromise.join([galleryService.query(), extensionsService.getInstalled()]) + .then(result => { + const available = result[0]; + const installed = result[1]; + + return available.filter(extension => { + const local = installed.filter(local => extensionEquals(local, extension))[0]; + return local && semver.lt(local.version, extension.version); + }); + }); +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/node/vsoGalleryService.ts b/src/vs/workbench/parts/extensions/common/vsoGalleryService.ts similarity index 100% rename from src/vs/workbench/parts/extensions/node/vsoGalleryService.ts rename to src/vs/workbench/parts/extensions/common/vsoGalleryService.ts 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 fcffb70e5ba5fd520b5cc1c58cb72213d4abc109..2ad0be015ff879ed82f04fba095fa1ea56e95cfe 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/extensions'; import platform = require('vs/platform/platform'); import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import statusbar = require('vs/workbench/browser/parts/statusbar/statusbar'); import { ExtensionsStatusbarItem } from 'vs/workbench/parts/extensions/electron-browser/extensionsWidgets'; import { IGalleryService } from 'vs/workbench/parts/extensions/common/extensions'; -import { GalleryService } from 'vs/workbench/parts/extensions/node/vsoGalleryService'; +import { GalleryService } from 'vs/workbench/parts/extensions/common/vsoGalleryService'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { ExtensionsWorkbenchExtension } from 'vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension'; @@ -24,5 +25,5 @@ registerSingleton(IGalleryService, GalleryService); (platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor( ExtensionsStatusbarItem, statusbar.StatusbarAlignment.LEFT, - 10 /* Low Priority */ + 10000 )); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen.ts index c7087c3e588ed3d1ecccb68ed5d5a5d09d7346a7..e95265c64659b1c1121e5c807757f0c834a87226 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/extensions'; - import nls = require('vs/nls'); import { IDisposable, disposeAll } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -24,9 +22,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IWorkspaceContextService } from 'vs/workbench/services/workspace/common/contextService'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { Action } from 'vs/base/common/actions'; -import * as semver from 'semver'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { shell } from 'electron'; +import { extensionEquals, getOutdatedExtensions } from 'vs/workbench/parts/extensions/common/extensionsUtil'; const $ = dom.emmet; @@ -76,10 +74,6 @@ function getHighlights(input: string, extension: IExtension): IHighlights { return { id, name, displayName, description }; } -function extensionEquals(one: IExtension, other: IExtension): boolean { - return one.publisher === other.publisher && one.name === other.name; -} - function extensionEntryCompare(one: IExtensionEntry, other: IExtensionEntry): number { const oneInstallCount = one.extension.galleryInformation ? one.extension.galleryInformation.installCount : 0; const otherInstallCount = other.extension.galleryInformation ? other.extension.galleryInformation.installCount : 0; @@ -263,7 +257,7 @@ class Renderer implements IRenderer { updateActions(); data.disposables = disposeAll(data.disposables); - data.disposables.push(this.extensionsService.onDidInstallExtension(e => onExtensionStateChange(e, ExtensionState.Installed))); + data.disposables.push(this.extensionsService.onDidInstallExtension(e => onExtensionStateChange(e.extension, ExtensionState.Installed))); data.disposables.push(this.extensionsService.onDidUninstallExtension(e => onExtensionStateChange(e, ExtensionState.Uninstalled))); data.displayName.set(extension.displayName, entry.highlights.displayName); @@ -480,8 +474,7 @@ class OutdatedExtensionsModel implements IModel { public entries: IExtensionEntry[]; constructor( - private galleryExtensions: IExtension[], - private localExtensions: IExtension[], + private outdatedExtensions: IExtension[], @IInstantiationService instantiationService: IInstantiationService ) { this.renderer = instantiationService.createInstance(Renderer); @@ -490,12 +483,9 @@ class OutdatedExtensionsModel implements IModel { } public set input(input: string) { - this.entries = this.galleryExtensions + this.entries = this.outdatedExtensions .map(extension => ({ extension, highlights: getHighlights(input.trim(), extension) })) - .filter(({ extension, highlights }) => { - const local = this.localExtensions.filter(local => extensionEquals(local, extension))[0]; - return local && semver.lt(local.version, extension.version) && !!highlights; - }) + .filter(({ highlights }) => !!highlights) .map(({ extension, highlights }: { extension: IExtension, highlights: IHighlights }) => ({ extension, highlights, @@ -525,8 +515,8 @@ export class OutdatedExtensionsHandler extends QuickOpenHandler { getResults(input: string): TPromise> { if (!this.modelPromise) { this.telemetryService.publicLog('extensionGallery:open'); - this.modelPromise = TPromise.join([this.galleryService.query(), this.extensionsService.getInstalled()]) - .then(result => this.instantiationService.createInstance(OutdatedExtensionsModel, result[0], result[1])); + this.modelPromise = this.instantiationService.invokeFunction(getOutdatedExtensions) + .then(outdated => this.instantiationService.createInstance(OutdatedExtensionsModel, outdated)); } return this.modelPromise.then(model => { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts index cee13bf01cfc3b0eb0e9a14c124cdaad4a27ad6b..14d3e8fec3606ea1cce07fcc3438e56ef0142f37 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts @@ -5,80 +5,166 @@ import nls = require('vs/nls'); import Severity from 'vs/base/common/severity'; -import dom = require('vs/base/browser/dom'); -import lifecycle = require('vs/base/common/lifecycle'); -import {onUnexpectedError} from 'vs/base/common/errors'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { emmet as $, append, toggleClass } from 'vs/base/browser/dom'; +import { IDisposable, combinedDispose } from 'vs/base/common/lifecycle'; +import { onUnexpectedPromiseError } from 'vs/base/common/errors'; +import { assign } from 'vs/base/common/objects'; import { Action } from 'vs/base/common/actions'; import statusbar = require('vs/workbench/browser/parts/statusbar/statusbar'); -import { IExtensionService, IExtensionsStatus } from 'vs/platform/extensions/common/extensions'; +import { IExtensionService, IMessage } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMessageService, CloseAction } from 'vs/platform/message/common/message'; import { UninstallAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; -import { IExtensionsService } from 'vs/workbench/parts/extensions/common/extensions'; +import { IExtensionsService, commandCategory, IExtension, IExtensionManifest } from 'vs/workbench/parts/extensions/common/extensions'; +import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService'; +import { getOutdatedExtensions } from 'vs/workbench/parts/extensions/common/extensionsUtil'; -var $ = dom.emmet; +interface IState { + errors: IMessage[]; + installing: IExtensionManifest[]; + outdated: IExtension[]; +} + +const InitialState: IState = { + errors: [], + installing: [], + outdated: [] +}; + +function extensionEquals(one: IExtensionManifest, other: IExtensionManifest): boolean { + return one.publisher === other.publisher && one.name === other.name; +} + +const OutdatedPeriod = 5 * 60 * 1000; // every 5 minutes export class ExtensionsStatusbarItem implements statusbar.IStatusbarItem { - private toDispose: lifecycle.IDisposable[]; private domNode: HTMLElement; - private status: { [id: string]: IExtensionsStatus }; - private container: HTMLElement; - private messageCount: number; + private state: IState = InitialState; + private outdatedDelayer = new ThrottledDelayer(OutdatedPeriod); constructor( - @IExtensionService extensionService: IExtensionService, + @IExtensionService private extensionService: IExtensionService, @IMessageService private messageService: IMessageService, @IExtensionsService protected extensionsService: IExtensionsService, - @IInstantiationService protected instantiationService: IInstantiationService + @IInstantiationService protected instantiationService: IInstantiationService, + @IQuickOpenService protected quickOpenService: IQuickOpenService + ) {} + + render(container: HTMLElement): IDisposable { + this.domNode = append(container, $('a.extensions-statusbar')); + append(this.domNode, $('.icon')); + this.domNode.onclick = () => this.onClick(); + + this.checkErrors(); + this.checkOutdated(); + + const disposables = []; + this.extensionsService.onInstallExtension(this.onInstallExtension, this, disposables); + this.extensionsService.onDidInstallExtension(this.onDidInstallExtension, this, disposables); + this.extensionsService.onDidUninstallExtension(this.onDidUninstallExtension, this, disposables); + + return combinedDispose(...disposables); + } - ) { - this.toDispose = []; - this.messageCount = 0; + private updateState(obj: any): void { + this.state = assign(this.state, obj); + this.onStateChange(); + } + + private get hasErrors() { return this.state.errors.length > 0; } + private get isInstalling() { return this.state.installing.length > 0; } + private get hasUpdates() { return this.state.outdated.length > 0; } + + private onStateChange(): void { + toggleClass(this.domNode, 'has-errors', this.hasErrors); + toggleClass(this.domNode, 'is-installing', !this.hasErrors && this.isInstalling); + toggleClass(this.domNode, 'has-updates', !this.hasErrors && !this.isInstalling && this.hasUpdates); + + if (this.hasErrors) { + const singular = nls.localize('oneIssue', "Extensions (1 issue)"); + const plural = nls.localize('multipleIssues', "Extensions ({0} issues)", this.state.errors.length); + this.domNode.title = this.state.errors.length > 1 ? plural : singular; + } else if (this.isInstalling) { + this.domNode.title = nls.localize('extensionsInstalling', "Extensions ({0} installing...)", this.state.installing.length); + } else if (this.hasUpdates) { + const singular = nls.localize('oneUpdate', "Extensions (1 update available)"); + const plural = nls.localize('multipleUpdates', "Extensions ({0} updates available)", this.state.outdated.length); + this.domNode.title = this.state.outdated.length > 1 ? plural : singular; + } else { + this.domNode.title = nls.localize('extensions', "Extensions"); + } + } - extensionService.onReady().then(() => { - this.status = extensionService.getExtensionsStatus(); - Object.keys(this.status).forEach(key => { - this.messageCount += this.status[key].messages.filter(message => message.type > Severity.Info).length; + private onClick(): void { + if (this.hasErrors) { + this.showErrors(this.state.errors); + this.updateState({ errors: [] }); + } else if (this.hasUpdates) { + this.quickOpenService.show(`ext update `); + } else { + this.quickOpenService.show(`>${commandCategory}: `); + } + } + + private showErrors(errors: IMessage[]): void { + const promise = onUnexpectedPromiseError(this.extensionsService.getInstalled()); + promise.done(installed => { + errors.forEach(m => { + const extension = installed.filter(ext => ext.path === m.source).pop(); + const actions = [CloseAction]; + const name = (extension && extension.name) || m.source; + const message = `${ name }: ${ m.message }`; + + if (extension) { + const actionLabel = nls.localize('uninstall', "Uninstall"); + actions.push(new Action('extensions.uninstall2', actionLabel, null, true, () => this.instantiationService.createInstance(UninstallAction).run(extension))); + } + + this.messageService.show(m.type, { message, actions }); }); - this.render(this.container); }); } - public render(container: HTMLElement): lifecycle.IDisposable { - this.container = container; - if (this.messageCount > 0) { - this.domNode = dom.append(container, $('a.extensions-statusbar')); - const issueLabel = this.messageCount > 1 ? nls.localize('issues', "issues") : nls.localize('issue', "issue"); - const extensionLabel = nls.localize('extension', "extension"); - this.domNode.title = `${ this.messageCount } ${ extensionLabel } ${ issueLabel }`; - this.domNode.textContent = `${ this.messageCount } ${ issueLabel }`; - - this.toDispose.push(dom.addDisposableListener(this.domNode, 'click', () => { - this.extensionsService.getInstalled().done(installed => { - Object.keys(this.status).forEach(key => { - this.status[key].messages.forEach(m => { - if (m.type > Severity.Info) { - const extension = installed.filter(ext => ext.path === m.source).pop(); - const actions = [CloseAction]; - const name = (extension && extension.name) || m.source; - const message = `${ name }: ${ m.message }`; - - if (extension) { - const actionLabel = nls.localize('uninstall', "Uninstall"); - actions.push(new Action('extensions.uninstall2', actionLabel, null, true, () => this.instantiationService.createInstance(UninstallAction).run(extension))); - } - - this.messageService.show(m.type, { message, actions }); - } - }); - }); - }, onUnexpectedError); - })); - } + private onInstallExtension(manifest: IExtensionManifest): void { + const installing = [...this.state.installing, manifest]; + this.updateState({ installing }); + } + + private onDidInstallExtension({ extension }: { extension: IExtension; }): void { + const installing = this.state.installing + .filter(e => !extensionEquals(extension, e)); + this.updateState({ installing }); + this.outdatedDelayer.trigger(() => this.checkOutdated(), 0); + } - return { - dispose: () => lifecycle.disposeAll(this.toDispose) - }; + private onDidUninstallExtension(): void { + this.outdatedDelayer.trigger(() => this.checkOutdated(), 0); + } + + private checkErrors(): void { + const promise = onUnexpectedPromiseError(this.extensionService.onReady()); + promise.done(() => { + const status = this.extensionService.getExtensionsStatus(); + const errors = Object.keys(status) + .map(k => status[k].messages) + .reduce((r, m) => r.concat(m), []) + .filter(m => m.type > Severity.Info); + + this.updateState({ errors }); + }); + } + + private checkOutdated(): TPromise { + return this.instantiationService.invokeFunction(getOutdatedExtensions) + .then(null, _ => []) // ignore errors + .then(outdated => { + this.updateState({ outdated }); + + // repeat this later + this.outdatedDelayer.trigger(() => this.checkOutdated()); + }); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts index 4e55160274d1c9ab679617e190fa4bb791d89ff8..dc22e493af05802bd2f58590ce4f949d862aebd5 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts @@ -8,7 +8,7 @@ import errors = require('vs/base/common/errors'); import platform = require('vs/platform/platform'); import { Promise } from 'vs/base/common/winjs.base'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IExtensionsService, IGalleryService, IExtensionTipsService } from 'vs/workbench/parts/extensions/common/extensions'; +import { IExtensionsService, IGalleryService, IExtensionTipsService, commandCategory } from 'vs/workbench/parts/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMessageService } from 'vs/platform/message/common/message'; import Severity from 'vs/base/common/severity'; @@ -46,9 +46,8 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution { // add service instantiationService.addSingleton(IExtensionTipsService, this.instantiationService.createInstance(ExtensionTipsService)); - const extensionsCategory = nls.localize('extensionsCategory', "Extensions"); const actionRegistry = ( platform.Registry.as(wbaregistry.Extensions.WorkbenchActions)); - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListExtensionsAction, ListExtensionsAction.ID, ListExtensionsAction.LABEL), extensionsCategory); + actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListExtensionsAction, ListExtensionsAction.ID, ListExtensionsAction.LABEL), commandCategory); (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( new QuickOpenHandlerDescriptor( @@ -62,7 +61,7 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution { if (galleryService.isEnabled()) { this.instantiationService.invokeFunction(checkForLegacyExtensionNeeds); - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(InstallExtensionAction, InstallExtensionAction.ID, InstallExtensionAction.LABEL), extensionsCategory); + actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(InstallExtensionAction, InstallExtensionAction.ID, InstallExtensionAction.LABEL), commandCategory); (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( new QuickOpenHandlerDescriptor( @@ -73,7 +72,7 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution { ) ); - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListOutdatedExtensionsAction, ListOutdatedExtensionsAction.ID, ListOutdatedExtensionsAction.LABEL), extensionsCategory); + actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListOutdatedExtensionsAction, ListOutdatedExtensionsAction.ID, ListOutdatedExtensionsAction.LABEL), commandCategory); (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( new QuickOpenHandlerDescriptor( @@ -85,7 +84,7 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution { ); // add extension tips services - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListSuggestedExtensionsAction, ListSuggestedExtensionsAction.ID, ListSuggestedExtensionsAction.LABEL), extensionsCategory); + actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListSuggestedExtensionsAction, ListSuggestedExtensionsAction.ID, ListSuggestedExtensionsAction.LABEL), commandCategory); (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( new QuickOpenHandlerDescriptor( diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css index 4c9e7bd54c2c49f17e47cf54cfae7ce944f5f158..6913298024cef1b6f5dff9e98735d71e2ecd7779 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css @@ -135,23 +135,54 @@ /* Status bar */ .monaco-shell .extensions-statusbar { - padding: 0 5px 0 25px; - line-height: 22px; - background: url('extensions-status.svg') center center no-repeat; - background-size: 14px; - background-position: 4px 50%; -} - -.monaco-shell .extensions-suggestions { - padding: 0 5px 0 5px; - -webkit-transition: visibility 250ms cubic-bezier(0.175, 0.885, 0.32, 1.275); - transition: visibility 250ms cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -.monaco-shell .extensions-suggestions > .octicon { - font-size: 14px; + position: relative; + padding: 0 5px; + width: 14px; } -.monaco-shell .extensions-suggestions.disabled { - display: none; +.monaco-shell .extensions-statusbar > .icon { + background: url('extensions-status.svg') center center no-repeat; + background-size: 14px; + width: 14px; + height: 100%; +} + +.monaco-shell .extensions-statusbar.has-updates::after { + content: ''; + width: 6px; + height: 6px; + background-color: #39A71E; + position: absolute; + top: 2px; + right: 3px; + border-radius: 10px; + border: 1px solid white; +} + +.monaco-shell .extensions-statusbar.has-errors::after { + content: ''; + width: 6px; + height: 6px; + background-color: #CC6633; + position: absolute; + top: 2px; + right: 3px; + border-radius: 10px; + border: 1px solid white; +} + +@keyframes shake { + from { transform: rotate(0deg); } + 30% { transform: rotate(0deg); } + 40% { transform: rotate(-20deg); } + 48% { transform: rotate(10deg); } + 52% { transform: rotate(-10deg); } + 60% { transform: rotate(20deg); } + 70% { transform: rotate(0deg); } + to { transform: rotate(0deg); } +} + +.monaco-shell .extensions-statusbar.is-installing .icon { + animation: 2s ease-in-out infinite shake; + opacity: 0.7; } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/node/extensionsService.ts b/src/vs/workbench/parts/extensions/node/extensionsService.ts index 896cd8212f2c4f1f3e82988a77e5aba99a5abd70..67863d292af8ec24dfd607ec2b491e92d37e41ec 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsService.ts @@ -92,10 +92,10 @@ export class ExtensionsService implements IExtensionsService { private obsoleteFileLimiter: Limiter; private _onInstallExtension = new Emitter(); - @ServiceEvent onInstallExtension: Event = this._onInstallExtension.event; + @ServiceEvent onInstallExtension: Event = this._onInstallExtension.event; - private _onDidInstallExtension = new Emitter(); - @ServiceEvent onDidInstallExtension: Event = this._onDidInstallExtension.event; + private _onDidInstallExtension = new Emitter<{ extension: IExtension; error?: Error; }>(); + @ServiceEvent onDidInstallExtension: Event<{ extension: IExtension; error?: Error; }> = this._onDidInstallExtension.event; private _onUninstallExtension = new Emitter(); @ServiceEvent onUninstallExtension: Event = this._onUninstallExtension.event; @@ -129,6 +129,8 @@ export class ExtensionsService implements IExtensionsService { return TPromise.wrapError(new Error(nls.localize('missingGalleryInformation', "Gallery information is missing"))); } + this._onInstallExtension.fire(extension); + return this.getLastValidExtensionVersion(extension, extension.galleryInformation.versions).then(versionInfo => { const version = versionInfo.version; const url = versionInfo.downloadUrl; @@ -139,13 +141,11 @@ export class ExtensionsService implements IExtensionsService { return this.request(url) .then(opts => download(zipPath, opts)) .then(() => validate(zipPath, extension, version)) - .then(manifest => { this._onInstallExtension.fire(manifest); return manifest; }) .then(manifest => extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true }).then(() => manifest)) - .then(manifest => { - manifest = assign({ __metadata: galleryInformation }, manifest); - return pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); - }) - .then(() => { this._onDidInstallExtension.fire(extension); return extension; }); + .then(manifest => assign({ __metadata: galleryInformation }, manifest)) + .then(manifest => pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'))) + .then(() => { this._onDidInstallExtension.fire({ extension }); return extension; }) + .then(null, error => { this._onDidInstallExtension.fire({ extension, error }); return TPromise.wrapError(error); }); }); }