diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 5a2c4059f1e56a18556cf8679923351b768d6654..2e7fc957f91e77b91b626240d111c861272ffb75 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -7,11 +7,11 @@ import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { StorageScope } from 'vs/platform/storage/common/storage'; export interface IExtensionDescription { id: string; name: string; + displayName: string; version: string; publisher: string; isBuiltin: boolean; @@ -68,6 +68,17 @@ export const IExtensionsRuntimeService = createDecorator; - getDisabledExtensions(scope?: StorageScope): string[]; + + /** + * Enable or disable the given extension. + * Returns a promise that resolves to boolean value. + * if resolves to `true` then requires restart for the change to take effect. + */ + setEnablement(identifier: string, enable: boolean, displayName: string): TPromise; + /** + * if `true` returns extensions disabled for workspace + * if `false` returns extensions disabled globally + * if `undefined` returns all disabled extensions + */ + getDisabledExtensions(workspace?: boolean): string[]; } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.ts index 150c6ba3ddfd285a950e7cba152e7d1c86a28022..f6aae85d4094d38ee129edb9c0ee068cbf002188 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.ts @@ -19,7 +19,6 @@ export interface IExtensionsViewlet extends IViewlet { export enum ExtensionState { Installing, Installed, - NeedsRestart, Uninstalled, Disabled } @@ -71,6 +70,7 @@ export interface IExtensionsWorkbenchService { install(vsix: string): TPromise; install(extension: IExtension, promptToInstallDependencies?: boolean): TPromise; uninstall(extension: IExtension): TPromise; + setEnablement(extension: IExtension, enable: boolean): TPromise; loadDependencies(extension: IExtension): TPromise; open(extension: IExtension, sideByside?: boolean): TPromise; } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index f7f1a0e05a1e370dbdeb59571818af3aa330640a..40d8666c807e60ae687b6bd104e2d4980a20e285 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -109,7 +109,7 @@ export class UninstallAction extends Action { return; } - this.enabled = this.extension.state === ExtensionState.Installed || this.extension.state === ExtensionState.NeedsRestart || this.extension.state === ExtensionState.Disabled; + this.enabled = this.extension.state === ExtensionState.Installed || this.extension.state === ExtensionState.Disabled; } run(): TPromise { @@ -232,7 +232,7 @@ export class UpdateAction extends Action { const canInstall = this.extensionsWorkbenchService.canInstall(this.extension); const isInstalled = this.extension.state === ExtensionState.Installed - || this.extension.state === ExtensionState.NeedsRestart; + || this.extension.state === ExtensionState.Disabled; this.enabled = canInstall && isInstalled && this.extension.outdated; this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass; @@ -259,8 +259,7 @@ export class EnableAction extends Action { set extension(extension: IExtension) { this._extension = extension; this.update(); } constructor( - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService private instantiationService: IInstantiationService + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService ) { super('extensions.enable', localize('enableAction', "Enable"), EnableAction.DisabledClass, false); @@ -275,22 +274,47 @@ export class EnableAction extends Action { return; } - this.enabled = this.extension.state === ExtensionState.NeedsRestart; + this.enabled = this.extension.state === ExtensionState.Disabled; this.class = this.enabled ? EnableAction.EnabledClass : EnableAction.DisabledClass; } run(): TPromise { - if (!window.confirm(localize('restart', "In order to enable this extension, this window of VS Code needs to be restarted.\n\nDo you want to continue?"))) { - return TPromise.as(null); + return this.extensionsWorkbenchService.setEnablement(this.extension, true); + } +} + +export class DisableAction extends Action { + + private static EnabledClass = 'extension-action disable'; + private static DisabledClass = `${DisableAction.EnabledClass} disabled`; + + private disposables: IDisposable[] = []; + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor( + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + ) { + super('extensions.disable', localize('disableAction', "Disable"), DisableAction.DisabledClass, false); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private update(): void { + if (!this.extension) { + this.enabled = false; + this.class = DisableAction.DisabledClass; + return; } - const action = this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, localize('restartNow', "Restart Now")); - return action.run(); + this.enabled = this.extension.state === ExtensionState.Installed; + this.class = this.enabled ? DisableAction.EnabledClass : DisableAction.DisabledClass; } - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extension, false); } } @@ -316,7 +340,7 @@ export class UpdateAllAction extends Action { return this.extensionsWorkbenchService.local.filter( e => this.extensionsWorkbenchService.canInstall(e) && e.type === LocalExtensionType.User - && (e.state === ExtensionState.Installed || e.state === ExtensionState.NeedsRestart) + && (e.state === ExtensionState.Installed || e.state === ExtensionState.Disabled) && e.outdated ); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts index f4ea8d895461ee11d8020814e4fb2316fe25c70c..578693359f22ce9e490799de3e20dcd283bede63 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts @@ -15,7 +15,7 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { once } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension } from './extensions'; -import { CombinedInstallAction, UpdateAction, EnableAction, BuiltinStatusLabelAction } from './extensionsActions'; +import { CombinedInstallAction, UpdateAction, EnableAction, DisableAction, BuiltinStatusLabelAction } from './extensionsActions'; import { Label, RatingsWidget, InstallWidget, StatusWidget } from './extensionsWidgets'; import { EventType } from 'vs/base/common/events'; @@ -75,9 +75,10 @@ export class Renderer implements IPagedRenderer { const installAction = this.instantiationService.createInstance(CombinedInstallAction); const updateAction = this.instantiationService.createInstance(UpdateAction); const restartAction = this.instantiationService.createInstance(EnableAction); + const disableAction = this.instantiationService.createInstance(DisableAction); - actionbar.push([restartAction, updateAction, installAction, builtinStatusAction], actionOptions); - const disposables = [versionWidget, installCountWidget, ratingsWidget, installAction, builtinStatusAction, updateAction, restartAction, actionbar]; + actionbar.push([restartAction, updateAction, disableAction, installAction, builtinStatusAction], actionOptions); + const disposables = [versionWidget, installCountWidget, ratingsWidget, installAction, builtinStatusAction, updateAction, restartAction, disableAction, actionbar]; return { element, icon, name, installCount, ratings, status, author, description, disposables, @@ -90,6 +91,7 @@ export class Renderer implements IPagedRenderer { installAction.extension = extension; updateAction.extension = extension; restartAction.extension = extension; + disableAction.extension = extension; statusWidget.extension = extension; } }; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts index d04809c9872cb01efab9a18d14d06df45b9c6895..ed8c989c633a774ed4800fdf47f689327a409d49 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts @@ -11,7 +11,6 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IExtension, IExtensionsWorkbenchService, ExtensionState } from './extensions'; import { append, $, addClass, toggleClass } from 'vs/base/browser/dom'; import { IExtensionsRuntimeService } from 'vs/platform/extensions/common/extensions'; -import { StorageScope } from 'vs/platform/storage/common/storage'; export interface IOptions { extension?: IExtension; @@ -69,7 +68,7 @@ export class StatusWidget implements IDisposable { const state = this.extension.state; const installed = state === ExtensionState.Installed; const disabled = state === ExtensionState.Disabled; - const disabledInWorkspace = this.extensionsRuntimeService.getDisabledExtensions(StorageScope.WORKSPACE).indexOf(this.extension.identifier) !== -1; + const disabledInWorkspace = this.extensionsRuntimeService.getDisabledExtensions(true).indexOf(this.extension.identifier) !== -1; toggleClass(status, 'disabled', disabled); toggleClass(status, 'active', installed); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts index 7f412665848f4fba65c6b9b640ed325f9b20e2ec..d9c4a9653729df44d2172b272a6d4e27a3d2a3d1 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts @@ -25,7 +25,7 @@ import { import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionTelemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IMessageService } from 'vs/platform/message/common/message'; +import { IMessageService, LaterAction } from 'vs/platform/message/common/message'; import Severity from 'vs/base/common/severity'; import * as semver from 'semver'; import * as path from 'path'; @@ -474,6 +474,19 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { return this.extensionService.installFromGallery(gallery, promptToInstallDependencies); } + setEnablement(extension: IExtension, enable: boolean): TPromise { + return this.extensionsRuntimeService.setEnablement(extension.identifier, enable, extension.displayName).then(restart => { + if (restart) { + const message = enable ? localize('postEnableMessage', "In order to enable '{0}' extension, this window of VS Code needs to be restarted.", extension.displayName) + : localize('postDisableMessage', "In order to disable '{0}' extension, this window of VS Code needs to be restarted.", extension.displayName); + return this.messageService.show(Severity.Info, { + message, + actions: [this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, localize('restartNow', "Restart Now")), LaterAction] + }); + } + }); + } + uninstall(extension: IExtension): TPromise { if (!(extension instanceof Extension)) { return; @@ -590,7 +603,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { if (local) { if (local.needsRestart) { - return ExtensionState.NeedsRestart; + return ExtensionState.Disabled; } else { return disabledExtensions.indexOf(`${local.publisher}.${local.name}`) === -1 ? ExtensionState.Installed : ExtensionState.Disabled; } diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css index 03cbc75cb8db7f179492474c0fb13304cfcb3b9e..340bc2b3f52866ab17b6c953bc224b163510c7ec 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css @@ -83,6 +83,7 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.install:not(.installing), .monaco-action-bar .action-item.disabled .action-label.extension-action.update, .monaco-action-bar .action-item.disabled .action-label.extension-action.enable, +.monaco-action-bar .action-item.disabled .action-label.extension-action.disable, .monaco-action-bar .action-item.disabled .action-label.extension-action.built-in-status.user { display: none; } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensions.ts b/src/vs/workbench/services/extensions/electron-browser/extensions.ts index be2816343802a659de59c16845da5baa960f311a..ee8551bc937b058a0922b9325d4ca5fe8ad9f989 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensions.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensions.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; import { distinct } from 'vs/base/common/arrays'; import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { IExtensionsRuntimeService } from 'vs/platform/extensions/common/extensions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IChoiceService } from 'vs/platform/message/common/message'; +import { IChoiceService, Severity } from 'vs/platform/message/common/message'; const DISABLED_EXTENSIONS_STORAGE_PATH = 'extensions/disabled'; @@ -28,18 +30,67 @@ export class ExtensionsRuntimeService implements IExtensionsRuntimeService { this.workspace = contextService.getWorkspace(); } - public getDisabledExtensions(scope?: StorageScope): string[] { + public setEnablement(identifier: string, enable: boolean, displayName: string): TPromise { + const disabled = this.getDisabledExtensionsFromStorage().indexOf(identifier) !== -1; + + if (!enable === disabled) { + return TPromise.wrap(true); + } + + if (!this.workspace) { + return this.setGlobalEnablement(identifier, enable, displayName); + } + + if (enable) { + if (this.getDisabledExtensionsFromStorage(StorageScope.GLOBAL).indexOf(identifier) !== -1) { + return this.choiceService.choose(Severity.Info, localize('enableExtensionGlobally', "Would you like to enable '{0}' extension globally?", displayName), + [localize('yes', "Yes"), localize('no', "No")]) + .then((option) => { + if (option === 0) { + return TPromise.join([this.enableExtension(identifier, StorageScope.GLOBAL), this.enableExtension(identifier, StorageScope.WORKSPACE)]).then(() => true); + } + return TPromise.wrap(false); + }); + } + return this.choiceService.choose(Severity.Info, localize('enableExtensionForWorkspace', "Would you like to enable '{0}' extension for this workspace?", displayName), + [localize('yes', "Yes"), localize('no', "No")]) + .then((option) => { + if (option === 0) { + return this.enableExtension(identifier, StorageScope.WORKSPACE).then(() => true); + } + return TPromise.wrap(false); + }); + } else { + return this.choiceService.choose(Severity.Info, localize('disableExtension', "Would you like to disable '{0}' extension for this workspace or globally?", displayName), + [localize('workspace', "Workspace"), localize('globally', "Globally"), localize('cancel', "Cancel")]) + .then((option) => { + switch (option) { + case 0: + return this.disableExtension(identifier, StorageScope.WORKSPACE); + case 1: + return this.disableExtension(identifier, StorageScope.GLOBAL); + default: return TPromise.wrap(false); + } + }); + } + } + + public getDisabledExtensions(workspace?: boolean): string[] { if (!this.allDisabledExtensions) { this.globalDisabledExtensions = this.getDisabledExtensionsFromStorage(StorageScope.GLOBAL); this.workspaceDisabledExtensions = this.getDisabledExtensionsFromStorage(StorageScope.WORKSPACE); this.allDisabledExtensions = distinct([...this.globalDisabledExtensions, ...this.workspaceDisabledExtensions]); } - switch (scope) { - case StorageScope.GLOBAL: return this.globalDisabledExtensions; - case StorageScope.WORKSPACE: return this.workspaceDisabledExtensions; + if (workspace === void 0) { + return this.allDisabledExtensions; } - return this.allDisabledExtensions; + + if (workspace) { + return this.workspaceDisabledExtensions; + } + + return this.globalDisabledExtensions; } private getDisabledExtensionsFromStorage(scope?: StorageScope): string[] { @@ -52,8 +103,56 @@ export class ExtensionsRuntimeService implements IExtensionsRuntimeService { return [...globallyDisabled, ...workspaceDisabled]; } + private setGlobalEnablement(identifier: string, enable: boolean, displayName: string): TPromise { + if (enable) { + return this.choiceService.choose(Severity.Info, localize('enableExtensionGloballyNoWorkspace', "Would you like to enable '{0}' extension globally?", displayName), + [localize('yes', "Yes"), localize('no', "No")]) + .then((option) => { + if (option === 0) { + return this.enableExtension(identifier, StorageScope.GLOBAL).then(() => true); + } + return TPromise.wrap(false); + }); + } else { + return this.choiceService.choose(Severity.Info, localize('disableExtensionGlobally', "Would you like to disable '{0}' extension globally?", displayName), + [localize('yes', "Yes"), localize('no', "No")]) + .then((option) => { + if (option === 0) { + return this.disableExtension(identifier, StorageScope.GLOBAL).then(() => true); + } + return TPromise.wrap(false); + }); + } + } + + private disableExtension(identifier: string, scope: StorageScope): TPromise { + let disabledExtensions = this._getDisabledExtensions(scope); + disabledExtensions.push(identifier); + this._setDisabledExtensions(disabledExtensions, scope); + return TPromise.wrap(true); + } + + private enableExtension(identifier: string, scope: StorageScope): TPromise { + let disabledExtensions = this._getDisabledExtensions(scope); + const index = disabledExtensions.indexOf(identifier); + if (index !== -1) { + disabledExtensions.splice(index, 1); + this._setDisabledExtensions(disabledExtensions, scope); + return TPromise.wrap(true); + } + return TPromise.wrap(false); + } + private _getDisabledExtensions(scope: StorageScope): string[] { const value = this.storageService.get(DISABLED_EXTENSIONS_STORAGE_PATH, scope, ''); return value ? distinct(value.split(',')) : []; } + + private _setDisabledExtensions(disabledExtensions: string[], scope: StorageScope): void { + if (disabledExtensions.length) { + this.storageService.store(DISABLED_EXTENSIONS_STORAGE_PATH, disabledExtensions.join(','), scope); + } else { + this.storageService.remove(DISABLED_EXTENSIONS_STORAGE_PATH, scope); + } + } } \ No newline at end of file