diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 8fe108cabdb45ace311bcdce8ac78dac9ab5cd7f..34fa6e435bac20ffef333ff21a98dae46d7e521b 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -47,6 +47,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionActivationProgress } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActivationProgress'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { ExtensionDependencyChecker } from 'vs/workbench/contrib/extensions/electron-browser/extensionsDependencyChecker'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -61,6 +62,7 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, LifecyclePhase.Eventually); Registry.as(OutputExtensions.OutputChannels) .registerChannel({ id: ExtensionsChannelId, label: ExtensionsLabel, log: false }); @@ -239,6 +241,13 @@ Registry.as(ConfigurationExtensions.Configuration) description: localize('extensionsCloseExtensionDetailsOnViewChange', "When enabled, editors with extension details will be automatically closed upon navigating away from the Extensions View."), default: false }, + 'extensions.autoInstallMissingDependencies': { + type: 'boolean', + description: localize('autoInstallMissingDependencies', "When enabled, automatically installs the missing dependencies of currently enabled extensions."), + default: false, + scope: ConfigurationScope.APPLICATION, + tags: ['usesOnlineServices'] + }, 'extensions.enableExperimentalAzureSearch': { type: 'boolean', description: localize('enableExperimentalAzureSearch', "Enable experimental Azure based search for searching extensions in Marketplace."), diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsDependencyChecker.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsDependencyChecker.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0d50251245de8d9b514a6dba01446f2b3af60ff --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsDependencyChecker.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { values } from 'vs/base/common/map'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Action } from 'vs/base/common/actions'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export class ExtensionDependencyChecker extends Disposable implements IWorkbenchContribution { + + constructor( + @IExtensionService private readonly extensionService: IExtensionService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @INotificationService private readonly notificationService: INotificationService, + @IWindowService private readonly windowService: IWindowService + ) { + super(); + CommandsRegistry.registerCommand('workbench.extensions.installMissingDepenencies', () => this.installMissingDependencies()); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.extensions.installMissingDepenencies', + category: localize('extensions', "Extensions"), + title: localize('auto install missing deps', "Install Missing Dependencies") + } + }); + this.checkAndInstallMissingDependencies(); + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('extensions.autoInstallMissingDependencies')) { this.checkAndInstallMissingDependencies(); } })); + } + + private async getUninstalledMissingDependencies(): Promise { + const allMissingDependencies = await this.getAllMissingDependencies(); + const localExtensions = await this.extensionsWorkbenchService.queryLocal(); + return allMissingDependencies.filter(id => localExtensions.every(l => !areSameExtensions(l.identifier, { id }))); + } + + private async getAllMissingDependencies(): Promise { + const runningExtensions = await this.extensionService.getExtensions(); + const runningExtensionsIds: Set = runningExtensions.reduce((result, r) => { result.add(r.identifier.value.toLowerCase()); return result; }, new Set()); + const missingDependencies: Set = new Set(); + for (const extension of runningExtensions) { + if (extension.extensionDependencies) { + extension.extensionDependencies.forEach(dep => { + if (!runningExtensionsIds.has(dep.toLowerCase())) { + missingDependencies.add(dep); + } + }); + } + } + return values(missingDependencies); + } + + private async checkAndInstallMissingDependencies(): Promise { + const missingDependencies = await this.getUninstalledMissingDependencies(); + if (missingDependencies.length > 0 && this.configurationService.getValue('extensions.autoInstallMissingDependencies')) { + this.installMissingDependencies(); + } + } + + private async installMissingDependencies(): Promise { + const missingDependencies = await this.getUninstalledMissingDependencies(); + if (missingDependencies.length) { + const extensions = (await this.extensionsWorkbenchService.queryGallery({ names: missingDependencies, pageSize: missingDependencies.length })).firstPage; + if (extensions.length) { + await Promise.all(extensions.map(extension => this.extensionsWorkbenchService.install(extension))); + this.notificationService.notify({ + severity: Severity.Info, + message: localize('finished installing missing deps', "Finished installing missing dependencies. Please reload the window now."), + actions: { + primary: [new Action('realod', localize('reload', "Realod Window"), '', true, + () => this.windowService.reloadWindow())] + } + }); + } + } else { + this.notificationService.info(localize('no missing deps', "There are no missing dependencies to install.")); + } + } +} \ No newline at end of file