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 2dd869dcd19..b8335765f13 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -37,14 +37,16 @@ import jsonContributionRegistry = require('vs/platform/jsonschemas/common/jsonCo 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'; +import { KeymapExtensions } from 'vs/workbench/parts/extensions/electron-browser/keymapExtensions'; // Singletons registerSingleton(IExtensionGalleryService, ExtensionGalleryService); registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(StatusUpdater); +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(StatusUpdater); +workbenchRegistry.registerWorkbenchContribution(KeymapExtensions); Registry.as(OutputExtensions.OutputChannels) .registerChannel(ExtensionsChannelId, ExtensionsLabel); diff --git a/src/vs/workbench/parts/extensions/electron-browser/keymapExtensions.ts b/src/vs/workbench/parts/extensions/electron-browser/keymapExtensions.ts new file mode 100644 index 00000000000..c38eee4498f --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/keymapExtensions.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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 arrays = require('vs/base/common/arrays'); +import nls = require('vs/nls'); +import { chain, any } from 'vs/base/common/event'; +import { onUnexpectedError, canceled } from 'vs/base/common/errors'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IExtensionManagementService, ILocalExtension, IExtensionEnablementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IChoiceService } from 'vs/platform/message/common/message'; +import Severity from 'vs/base/common/severity'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; + +interface IExtension { + identifier: string; + local: ILocalExtension; +} + +export class KeymapExtensions implements IWorkbenchContribution { + + private disposables: IDisposable[] = []; + + constructor( + @IExtensionManagementService private extensionService: IExtensionManagementService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService, + @IExtensionTipsService private tipsService: IExtensionTipsService, + @IChoiceService private choiceService: IChoiceService, + @ILifecycleService lifecycleService: ILifecycleService, + @ITelemetryService private telemetryService: ITelemetryService, + ) { + this.disposables.push( + lifecycleService.onShutdown(() => this.dispose()), + any( + chain(extensionService.onDidInstallExtension) + .map(e => stripVersion(e.id)) + .event, + extensionEnablementService.onEnablementChanged + )((id => { + this.checkForOtherKeymaps(id) + .then(null, onUnexpectedError); + })) + ); + } + + getId(): string { + return 'vs.extensions.keymapExtensions'; + } + + private checkForOtherKeymaps(extensionId: string): TPromise { + return this.extensionService.getInstalled().then(extensions => { + const installedExtensions = extensions.map(ext => ({ identifier: stripVersion(ext.id), local: ext })); + const extension = arrays.first(installedExtensions, ext => ext.identifier === extensionId); + const globallyDisabled = this.extensionEnablementService.getGloballyDisabledExtensions(); + if (extension && this.isKeymapExtension(extension) && globallyDisabled.indexOf(extensionId) === -1) { + const otherKeymaps = installedExtensions.filter(ext => ext.identifier !== extensionId && + this.isKeymapExtension(ext) && + globallyDisabled.indexOf(ext.identifier) === -1); + if (otherKeymaps.length) { + return this.promptForDisablingOtherKeymaps(extension, otherKeymaps); + } + } + return undefined; + }); + } + + private isKeymapExtension(extension: IExtension): boolean { + const cats = extension.local.manifest.categories; + return cats && cats.indexOf('Keymaps') !== -1 || this.tipsService.getKeymapRecommendations().indexOf(extension.identifier) !== -1; + } + + private promptForDisablingOtherKeymaps(newKeymap: IExtension, oldKeymaps: IExtension[]): TPromise { + const telemetryData: { [key: string]: any; } = { + newKeymap: newKeymap.identifier, + oldKeymaps: oldKeymaps.map(k => k.identifier) + }; + this.telemetryService.publicLog('disableOtherKeymapsConfirmation', telemetryData); + const message = nls.localize('disableOtherKeymapsConfirmation', "Disable other keymaps to avoid conflicts between keybindings?"); + const options = [ + nls.localize('yes', "Yes"), + nls.localize('no', "No") + ]; + return this.choiceService.choose(Severity.Info, message, options, false) + .then(value => { + const confirmed = value === 0; + telemetryData['confirmed'] = confirmed; + this.telemetryService.publicLog('disableOtherKeymaps', telemetryData); + if (confirmed) { + return TPromise.join(oldKeymaps.map(keymap => { + return this.extensionEnablementService.setEnablement(keymap.identifier, false); + })); + } + return undefined; + }, error => TPromise.wrapError(canceled())); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +function stripVersion(id: string): string { + return id.replace(/-\d+\.\d+\.\d+$/, ''); +} diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 2ad7c5eeba5..cc9c315b64f 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -15,7 +15,7 @@ import { LinkedMap as Map } from 'vs/base/common/map'; import { assign } from 'vs/base/common/objects'; import { isUUID } from 'vs/base/common/uuid'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { isPromiseCanceledError, onUnexpectedError, canceled } from 'vs/base/common/errors'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; @@ -689,8 +689,6 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { installed.local = local; } else { this.installed.push(extension); - this.checkForOtherKeymaps(extension) - .then(null, onUnexpectedError); } } if (extension.gallery) { @@ -701,48 +699,6 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { this._onChange.fire(); } - private checkForOtherKeymaps(extension: Extension): TPromise { - if (!extension.disabledGlobally && this.isKeymapExtension(extension)) { - const otherKeymaps = this.installed.filter(ext => ext.identifier !== extension.identifier && - !ext.disabledGlobally && - this.isKeymapExtension(ext)); - if (otherKeymaps.length) { - return this.promptForDisablingOtherKeymaps(extension, otherKeymaps); - } - } - return TPromise.as(undefined); - } - - private isKeymapExtension(extension: Extension): boolean { - const cats = extension.local.manifest.categories; - return cats && cats.indexOf('Keymaps') !== -1 || this.tipsService.getKeymapRecommendations().indexOf(extension.identifier) !== -1; - } - - private promptForDisablingOtherKeymaps(newKeymap: Extension, oldKeymaps: Extension[]): TPromise { - const telemetryData: { [key: string]: any; } = { - newKeymap: newKeymap.identifier, - oldKeymaps: oldKeymaps.map(k => k.identifier) - }; - this.telemetryService.publicLog('disableOtherKeymapsConfirmation', telemetryData); - const message = nls.localize('disableOtherKeymapsConfirmation', "Disable other keymaps to avoid conflicts between keybindings?"); - const options = [ - nls.localize('yes', "Yes"), - nls.localize('no', "No") - ]; - return this.choiceService.choose(Severity.Info, message, options, false) - .then(value => { - const confirmed = value === 0; - telemetryData['confirmed'] = confirmed; - this.telemetryService.publicLog('disableOtherKeymaps', telemetryData); - if (confirmed) { - return TPromise.join(oldKeymaps.map(keymap => { - return this.setEnablement(keymap, false); - })); - } - return undefined; - }, error => TPromise.wrapError(canceled())); - } - private onUninstallExtension(id: string): void { const extension = this.installed.filter(e => e.local.id === id)[0]; const newLength = this.installed.filter(e => e.local.id !== id).length; @@ -785,8 +741,6 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { extension.disabledGlobally = globallyDisabledExtensions.indexOf(extension.identifier) !== -1; extension.disabledForWorkspace = workspaceDisabledExtensions.indexOf(extension.identifier) !== -1; this._onChange.fire(); - this.checkForOtherKeymaps(extension) - .then(null, onUnexpectedError); } }