From 8a24b3b196d1f821e5f2d9195aff1427b99b4861 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 24 Nov 2023 18:42:54 +0100 Subject: [PATCH] #194188 Implement auto update by extension --- src/vs/base/common/product.ts | 1 + .../common/extensionManagement.ts | 1 + .../node/extensionManagementService.ts | 4 +- .../extensions/browser/extensionEditor.ts | 5 +- .../browser/extensions.contribution.ts | 67 ++++- .../extensions/browser/extensionsActions.ts | 87 +++++- .../extensions/browser/extensionsList.ts | 4 +- .../extensions/browser/extensionsViewlet.ts | 2 +- .../extensions/browser/extensionsViews.ts | 2 +- .../browser/extensionsWorkbenchService.ts | 281 +++++++++++++++--- .../contrib/extensions/common/extensions.ts | 9 +- .../extensionsWorkbenchService.test.ts | 272 ++++++++++++++++- .../common/webExtensionManagementService.ts | 2 +- 13 files changed, 658 insertions(+), 79 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 61a1623d76dcf..05070395a741b 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -103,6 +103,7 @@ export interface IProductConfiguration { readonly nlsBaseUrl: string; }; + readonly publishersByOrganisation?: IStringDictionary; readonly extensionRecommendations?: IStringDictionary; readonly configBasedExtensionTips?: IStringDictionary; readonly exeBasedExtensionTips?: IStringDictionary; diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index ba729ddf9a55a..b545fceeed35d 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -423,6 +423,7 @@ export type InstallOptions = { isBuiltin?: boolean; isMachineScoped?: boolean; isApplicationScoped?: boolean; + pinned?: boolean; donotIncludePackAndDependencies?: boolean; installGivenVersion?: boolean; installPreReleaseVersion?: boolean; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 0cc77466bb0b0..9578d20bd34dc 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -884,7 +884,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { updated: !!existingExtension, isPreReleaseVersion: this.gallery.properties.isPreReleaseVersion, installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : existingExtension?.pinned, + pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), preRelease: this.gallery.properties.isPreReleaseVersion || (isBoolean(this.options.installPreReleaseVersion) ? this.options.installPreReleaseVersion /* Respect the passed flag */ @@ -955,7 +955,7 @@ class InstallVSIXTask extends InstallExtensionTask { isMachineScoped: this.options.isMachineScoped || existing?.isMachineScoped, isBuiltin: this.options.isBuiltin || existing?.isBuiltin, installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : undefined, + pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existing?.pinned), }; if (existing) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 5b5b09c696677..be0ee5dc5a632 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -71,7 +71,8 @@ import { SetFileIconThemeAction, SetLanguageAction, SetProductIconThemeAction, - SkipUpdateAction, + ToggleAutoUpdateForExtensionAction, + ToggleAutoUpdatesForPublisherAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, ToggleSyncExtensionAction, UninstallAction, @@ -344,7 +345,7 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(ReloadAction), this.instantiationService.createInstance(ExtensionStatusLabelAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', - [[this.instantiationService.createInstance(UpdateAction, true)], [this.instantiationService.createInstance(SkipUpdateAction)]]), + [[this.instantiationService.createInstance(UpdateAction, true)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction), this.instantiationService.createInstance(ToggleAutoUpdatesForPublisherAction)]]), this.instantiationService.createInstance(SetColorThemeAction), this.instantiationService.createInstance(SetFileIconThemeAction), this.instantiationService.createInstance(SetProductIconThemeAction), diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b5ed35892c729..bb518d682f978 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -13,8 +13,8 @@ import { EnablementState, IExtensionManagementServerService, IWorkbenchExtension import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; @@ -130,15 +130,17 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'object', properties: { 'extensions.autoUpdate': { - enum: [true, 'onlyEnabledExtensions', false,], + enum: [true, 'onlyEnabledExtensions', 'onlySelectedExtensions', false,], enumItemLabels: [ localize('all', "All Extensions"), localize('enabled', "Only Enabled Extensions"), + localize('selected', "Only Selected Extensions"), localize('none', "None"), ], enumDescriptions: [ localize('extensions.autoUpdate.true', 'Download and install updates automatically for all extensions except for those updates are ignored.'), localize('extensions.autoUpdate.enabled', 'Download and install updates automatically only for enabled extensions except for those updates are ignored. Disabled extensions are not updated automatically.'), + localize('extensions.autoUpdate.selected', 'Download and install updates automatically only for selected extensions.'), localize('extensions.autoUpdate.false', 'Extensions are not automatically updated.'), ], description: localize('extensions.autoUpdate', "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."), @@ -620,7 +622,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: 'configureExtensionsAutoUpdate.all', title: localize('configureExtensionsAutoUpdate.all', "All Extensions"), - toggled: ContextKeyExpr.and(ContextKeyExpr.has(`config.${AutoUpdateConfigurationKey}`), ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions')), + toggled: ContextKeyExpr.and(ContextKeyExpr.has(`config.${AutoUpdateConfigurationKey}`), ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions')), menu: [{ id: autoUpdateExtensionsSubMenu, order: 1, @@ -630,7 +632,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: 'configureExtensionsAutoUpdate.enabled', - title: localize('configureExtensionsAutoUpdate.enabled', "Only Enabled Extensions"), + title: localize('configureExtensionsAutoUpdate.enabled', "Enabled Extensions"), toggled: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), menu: [{ id: autoUpdateExtensionsSubMenu, @@ -639,6 +641,17 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, 'onlyEnabledExtensions') }); + this.registerExtensionAction({ + id: 'configureExtensionsAutoUpdate.selected', + title: localize('configureExtensionsAutoUpdate.selected', "Selected Extensions"), + toggled: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions'), + menu: [{ + id: autoUpdateExtensionsSubMenu, + order: 2, + }], + run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, 'onlySelectedExtensions') + }); + this.registerExtensionAction({ id: 'configureExtensionsAutoUpdate.none', title: localize('configureExtensionsAutoUpdate.none', "None"), @@ -1314,6 +1327,50 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } }); + this.registerExtensionAction({ + id: ToggleAutoUpdateForExtensionAction.ID, + title: { value: ToggleAutoUpdateForExtensionAction.LABEL, original: 'Auto Update' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.ExtensionContext, + group: UPDATE_ACTIONS_GROUP, + order: 1, + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension'), ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions'),) + }, + run: async (accessor: ServicesAccessor, id: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const extension = extensionWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id })); + if (extension) { + const action = instantiationService.createInstance(ToggleAutoUpdateForExtensionAction); + action.extension = extension; + return action.run(); + } + } + }); + + this.registerExtensionAction({ + id: ToggleAutoUpdatesForPublisherAction.ID, + title: { value: ToggleAutoUpdatesForPublisherAction.LABEL, original: 'Auto Update (Publisher)' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.ExtensionContext, + group: UPDATE_ACTIONS_GROUP, + order: 2, + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension'), ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions'),) + }, + run: async (accessor: ServicesAccessor, id: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const extension = extensionWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id })); + if (extension) { + const action = instantiationService.createInstance(ToggleAutoUpdatesForPublisherAction); + action.extension = extension; + return action.run(); + } + } + }); + this.registerExtensionAction({ id: SwitchToPreReleaseVersionAction.ID, title: SwitchToPreReleaseVersionAction.TITLE, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index ede103593cac3..04701ff691a14 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { disposeIfDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -826,15 +826,61 @@ export class UpdateAction extends AbstractUpdateAction { } } -export class SkipUpdateAction extends AbstractUpdateAction { +export class ToggleAutoUpdateForExtensionAction extends AbstractUpdateAction { - static readonly ID = 'workbench.extensions.action.ignoreUpdates'; - static readonly LABEL = localize('ignoreUpdates', "Ignore Updates"); + static readonly ID = 'workbench.extensions.action.toggleAutoUpdateForExtension'; + static readonly LABEL = localize('enableAutoUpdateLabel', "Auto Update"); constructor( @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService ) { - super(SkipUpdateAction.ID, SkipUpdateAction.LABEL, extensionsWorkbenchService); + super(ToggleAutoUpdateForExtensionAction.ID, ToggleAutoUpdateForExtensionAction.LABEL, extensionsWorkbenchService); + } + + override update() { + this.enabled = false; + if (!this.extension) { + return; + } + if (this.extension.isBuiltin) { + return; + } + if (!this.extensionsWorkbenchService.isAutoUpdateEnabled()) { + return; + } + super.update(); + this._checked = this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension); + } + + override async run(): Promise { + if (!this.extension) { + return; + } + + const enableAutoUpdate = !this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension); + await this.extensionsWorkbenchService.updateAutoUpdateEnablementFor(this.extension, enableAutoUpdate); + + if (enableAutoUpdate) { + alert(localize('enableAutoUpdate', "Enabled auto updates for", this.extension.displayName)); + } else { + alert(localize('disableAutoUpdate', "Disabled auto updates for", this.extension.displayName)); + } + } +} + +export class ToggleAutoUpdatesForPublisherAction extends AbstractUpdateAction { + + static readonly ID = 'workbench.extensions.action.toggleAutoUpdatesForPublisher'; + static readonly LABEL = localize('toggleAutoUpdatesForPublisherLabel', "Auto Update (Publisher)"); + + static getLabel(extension: IExtension): string { + return localize('toggleAutoUpdatesForPublisherLabel2', "Auto Update All from {0} (Publisher)", extension.publisherDisplayName); + } + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super(ToggleAutoUpdatesForPublisherAction.ID, ToggleAutoUpdatesForPublisherAction.LABEL, extensionsWorkbenchService); } override update() { @@ -845,17 +891,27 @@ export class SkipUpdateAction extends AbstractUpdateAction { this.enabled = false; return; } + if (this.extensionsWorkbenchService.getAutoUpdateValue() !== 'onlySelectedExtensions') { + this.enabled = false; + return; + } super.update(); - this._checked = this.extension.pinned; + this._checked = this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension.publisher); + this.label = ToggleAutoUpdatesForPublisherAction.getLabel(this.extension); } override async run(): Promise { if (!this.extension) { return; } - alert(localize('ignoreExtensionUpdate', "Ignoring {0} updates", this.extension.displayName)); - const newIgnoresAutoUpdates = !this.extension.pinned; - await this.extensionsWorkbenchService.pinExtension(this.extension, newIgnoresAutoUpdates); + alert(localize('ignoreExtensionUpdatePublisher', "Ignoring updates published by {0}.", this.extension.publisherDisplayName)); + const enableAutoUpdate = !this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension.publisher); + await this.extensionsWorkbenchService.updateAutoUpdateEnablementFor(this.extension.publisher, enableAutoUpdate); + if (enableAutoUpdate) { + alert(localize('enableAutoUpdate', "Enabled auto updates for", this.extension.displayName)); + } else { + alert(localize('disableAutoUpdate', "Disabled auto updates for", this.extension.displayName)); + } } } @@ -1013,7 +1069,6 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['galleryExtensionIsPreReleaseVersion', !!extension.gallery?.properties.isPreReleaseVersion]); cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]); cksOverlay.push(['extensionHasReleaseVersion', extension.hasReleaseVersion]); - cksOverlay.push(['isExtensionPinned', extension.pinned]); const [colorThemes, fileIconThemes, productIconThemes] = await Promise.all([workbenchThemeService.getColorThemes(), workbenchThemeService.getFileIconThemes(), workbenchThemeService.getProductIconThemes()]); cksOverlay.push(['extensionHasColorThemes', colorThemes.some(theme => isThemeFromExtension(theme, extension))]); @@ -1073,10 +1128,12 @@ export class ManageExtensionAction extends ExtensionDropDownAction { async getActionGroups(): Promise { const groups: IAction[][] = []; const contextMenuActionsGroups = await getContextMenuActionsGroups(this.extension, this.contextKeyService, this.instantiationService); - const themeActions: IAction[] = [], installActions: IAction[] = [], otherActionGroups: IAction[][] = []; + const themeActions: IAction[] = [], installActions: IAction[] = [], updateActions: IAction[] = [], otherActionGroups: IAction[][] = []; for (const [group, actions] of contextMenuActionsGroups) { if (group === INSTALL_ACTIONS_GROUP) { installActions.push(...toActions([[group, actions]], this.instantiationService)[0]); + } else if (group === UPDATE_ACTIONS_GROUP) { + updateActions.push(...toActions([[group, actions]], this.instantiationService)[0]); } else if (group === THEME_ACTIONS_GROUP) { themeActions.push(...toActions([[group, actions]], this.instantiationService)[0]); } else { @@ -1096,6 +1153,9 @@ export class ManageExtensionAction extends ExtensionDropDownAction { this.instantiationService.createInstance(DisableGloballyAction), this.instantiationService.createInstance(DisableForWorkspaceAction) ]); + if (updateActions.length) { + groups.push(updateActions); + } groups.push([ ...(installActions.length ? installActions : []), this.instantiationService.createInstance(InstallAnotherVersionAction), @@ -1169,6 +1229,11 @@ export class MenuItemExtensionAction extends ExtensionAction { } if (this.action.id === TOGGLE_IGNORE_EXTENSION_ACTION_ID) { this.checked = !this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension); + } else if (this.action.id === ToggleAutoUpdateForExtensionAction.ID) { + this.checked = this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension); + } else if (this.action.id === ToggleAutoUpdatesForPublisherAction.ID) { + this.label = ToggleAutoUpdatesForPublisherAction.getLabel(this.extension); + this.checked = this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension.publisher); } else { this.checked = this.action.checked; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 4e89ae5c8af22..f894dbecba646 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -13,7 +13,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, SkipUpdateAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; @@ -119,7 +119,7 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, true), this.instantiationService.createInstance(ReloadAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', - [[this.instantiationService.createInstance(UpdateAction, false)], [this.instantiationService.createInstance(SkipUpdateAction)]]), + [[this.instantiationService.createInstance(UpdateAction, false)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction), this.instantiationService.createInstance(ToggleAutoUpdatesForPublisherAction)]]), this.instantiationService.createInstance(InstallDropdownAction), this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(SetLanguageAction), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 05469dbf48a3b..87ca44f407dbc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -849,7 +849,7 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution this.badgeHandle.clear(); const extensionsReloadRequired = this.extensionsWorkbenchService.installed.filter(e => e.reloadRequiredStatus !== undefined); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !e.pinned && !extensionsReloadRequired.includes(e) ? 1 : 0), 0); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !extensionsReloadRequired.includes(e) ? 1 : 0), 0); const newBadgeNumber = outdated + extensionsReloadRequired.length; if (newBadgeNumber > 0) { let msg = ''; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 9ff8b5fd61844..64d33c31756cd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -553,7 +553,7 @@ export class ExtensionsListView extends ViewPane { const reloadRequired: IExtension[] = []; const noActionRequired: IExtension[] = []; result.forEach(e => { - if (e.outdated && !e.pinned) { + if (e.outdated) { outdated.push(e); } else if (e.reloadRequiredStatus) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index f7fe57a1760b6..201bdd2cba40d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -15,15 +15,15 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, - IExtensionsControlManifest, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo + IExtensionsControlManifest, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, ExtensionKey, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -32,7 +32,7 @@ import { IProgressOptions, IProgressService, ProgressLocation } from 'vs/platfor import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform, ExtensionIdentifier, IExtensionIdentifier, IExtensionDescription, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -41,7 +41,7 @@ import { FileAccess } from 'vs/base/common/network'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { isBoolean, isUndefined } from 'vs/base/common/types'; +import { isBoolean, isString, isUndefined } from 'vs/base/common/types'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IExtensionService, IExtensionsStatus, toExtension, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; @@ -424,6 +424,8 @@ ${this.description} } } +const EXTENSIONS_AUTO_UPDATE_KEY = 'extensions.autoUpdate'; + class Extensions extends Disposable { static updateExtensionFromControlManifest(extension: Extension, extensionsControlManifest: IExtensionsControlManifest): void { @@ -447,7 +449,6 @@ class Extensions extends Disposable { private readonly runtimeStateProvider: IExtensionStateProvider, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -575,7 +576,7 @@ class Extensions extends Disposable { private async fetchInstalledExtensions(): Promise { const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); - const all = await this.migrateIgnoredAutoUpdateExtensions(await this.server.extensionManagementService.getInstalled()); + const all = await this.server.extensionManagementService.getInstalled(); // dedup user and system extensions by giving priority to user extensions. const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => { @@ -595,21 +596,6 @@ class Extensions extends Disposable { }); } - private async migrateIgnoredAutoUpdateExtensions(extensions: ILocalExtension[]): Promise { - const ignoredAutoUpdateExtensions = JSON.parse(this.storageService.get('extensions.ignoredAutoUpdateExtension', StorageScope.PROFILE, '[]') || '[]'); - if (!ignoredAutoUpdateExtensions.length) { - return extensions; - } - const result = await Promise.all(extensions.map(extension => { - if (ignoredAutoUpdateExtensions.indexOf(new ExtensionKey(extension.identifier, extension.manifest.version).toString()) !== -1) { - return this.server.extensionManagementService.updateMetadata(extension, { pinned: true }); - } - return extension; - })); - this.storageService.remove('extensions.ignoredAutoUpdateExtension', StorageScope.PROFILE); - return result; - } - private async reset(): Promise { this.installed = []; this.installing = []; @@ -752,6 +738,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension readonly whenInitialized: Promise; + private readonly organizationByPublisher = new Map(); + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @@ -777,6 +765,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @ILifecycleService private readonly lifecycleService: ILifecycleService, @IFileService private readonly fileService: IFileService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IStorageService private readonly storageService: IStorageService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -812,6 +801,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension urlService.registerHandler(this); + if (this.productService.publishersByOrganisation) { + for (const organization of Object.keys(this.productService.publishersByOrganisation)) { + for (const publisher of this.productService.publishersByOrganisation[organization]) { + this.organizationByPublisher.set(publisher, organization); + } + } + } + this.whenInitialized = this.initialize(); } @@ -831,15 +828,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.initializeAutoUpdate(); this.reportInstalledExtensionsTelemetry(); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.reportProgressFromOtherSources())); + this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, EXTENSIONS_AUTO_UPDATE_KEY, this._store)(e => this.onDidAutoUpdateConfigurationChange())); } private initializeAutoUpdate(): void { // Register listeners for auto updates this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - if (this.isAutoUpdateEnabled()) { - this.checkForUpdates(); - } + this.onDidAutoUpdateConfigurationChange(); } if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) { if (this.isAutoCheckUpdatesEnabled()) { @@ -1316,6 +1312,13 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return ExtensionState.Uninstalled; } + private async onDidAutoUpdateConfigurationChange(): Promise { + await this.updateExtensionsPinnedState(); + if (this.isAutoUpdateEnabled()) { + this.checkForUpdates(); + } + } + async checkForUpdates(onlyBuiltin?: boolean): Promise { if (!this.galleryService.isEnabled()) { return; @@ -1339,7 +1342,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Skip if check updates only for builtin extensions and current extension is not builtin. continue; } - if (installed.isBuiltin && !installed.pinned && (installed.type === ExtensionType.System || !installed.local?.identifier.uuid)) { + if (installed.isBuiltin && !installed.local?.pinned && (installed.type === ExtensionType.System || !installed.local?.identifier.uuid)) { // Skip checking updates for a builtin extension if it is a system extension or if it does not has Marketplace identifier continue; } @@ -1402,12 +1405,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } - private getAutoUpdateValue(): boolean | 'onlyEnabledExtensions' { - const autoUpdate = this.configurationService.getValue(AutoUpdateConfigurationKey); - return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' ? autoUpdate : true; + getAutoUpdateValue(): AutoUpdateConfigurationValue { + const autoUpdate = this.configurationService.getValue(AutoUpdateConfigurationKey); + return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' || autoUpdate === 'onlySelectedExtensions' ? autoUpdate : true; } - private isAutoUpdateEnabled(): boolean { + isAutoUpdateEnabled(): boolean { return this.getAutoUpdateValue() !== false; } @@ -1438,7 +1441,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private async syncPinnedBuiltinExtensions(): Promise { const infos: IExtensionInfo[] = []; for (const installed of this.local) { - if (installed.isBuiltin && installed.pinned && installed.local?.identifier.uuid) { + if (installed.isBuiltin && installed.local?.pinned && installed.local?.identifier.uuid) { infos.push({ ...installed.identifier, version: installed.version }); } } @@ -1450,23 +1453,163 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } - private autoUpdateExtensions(): Promise { + private async autoUpdateExtensions(): Promise { if (!this.isAutoUpdateEnabled()) { - return Promise.resolve(); + return; + } + + const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); + + await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true } : undefined))); + } + + private async updateExtensionsPinnedState(): Promise { + await Promise.all(this.installed.map(async e => { + if (e.isBuiltin) { + return; + } + const shouldBePinned = !this.shouldAutoUpdateExtension(e); + if (e.local && e.local.pinned !== shouldBePinned) { + await this.extensionManagementService.updateMetadata(e.local, { pinned: shouldBePinned }); + } + })); + } + + private shouldAutoUpdateExtension(extension: IExtension): boolean { + const autoUpdate = this.getAutoUpdateValue(); + if (isBoolean(autoUpdate)) { + return autoUpdate; + } + + if (autoUpdate === 'onlyEnabledExtensions') { + return this.extensionEnablementService.isEnabledEnablementState(extension.enablementState); + } + + const extensionsToAutoUpdate = this.getSelectedExtensionsToAutoUpdate(); + const extensionId = extension.identifier.id.toLowerCase(); + return extensionsToAutoUpdate.includes(extensionId) || + (!extensionsToAutoUpdate.includes(`-${extensionId}`) && this.isAutoUpdateEnabledForPublisher(extension.publisher)); + } + + isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean { + if (isString(extensionOrPublisher)) { + if (EXTENSION_IDENTIFIER_REGEX.test(extensionOrPublisher)) { + throw new Error('Expected publisher string, found extension identifier'); + } + const autoUpdate = this.getAutoUpdateValue(); + if (isBoolean(autoUpdate)) { + return autoUpdate; + } + if (autoUpdate === 'onlyEnabledExtensions') { + return false; + } + return this.isAutoUpdateEnabledForPublisher(extensionOrPublisher); + } + return !extensionOrPublisher.local?.pinned && this.shouldAutoUpdateExtension(extensionOrPublisher); + } + + private isAutoUpdateEnabledForPublisher(publisher: string): boolean { + publisher = publisher.toLowerCase(); + const publishersToAutoUpdate = this.getPublishersToAutoUpdate(); + if (publishersToAutoUpdate.includes(publisher)) { + return true; + } + const publisherOrganization = this.organizationByPublisher.get(publisher); + if (publisherOrganization) { + return publishersToAutoUpdate.some(p => this.organizationByPublisher.get(publisher) === publisherOrganization); + } + return false; + } + + async updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise { + const autoUpdateValue = this.getAutoUpdateValue(); + + if (autoUpdateValue === false) { + throw new Error('Auto update is disabled'); } - const toUpdate = this.outdated.filter(e => !e.pinned && - (this.getAutoUpdateValue() === true || (e.local && this.extensionEnablementService.isEnabled(e.local))) - ); + if (autoUpdateValue === true || autoUpdateValue === 'onlyEnabledExtensions') { + if (isString(extensionOrPublisher)) { + throw new Error('Expected extension, found publisher string'); + } + if (!extensionOrPublisher.local) { + throw new Error('Only installed extensions can be pinned'); + } + await this.extensionManagementService.updateMetadata(extensionOrPublisher.local, { pinned: !enable }); + if (enable) { + this.eventuallyAutoUpdateExtensions(); + } + return; + } - return Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true } : undefined))); + const autoUpdateExtensions = this.getSelectedExtensionsToAutoUpdate(); + let update = false; + if (isString(extensionOrPublisher)) { + if (EXTENSION_IDENTIFIER_REGEX.test(extensionOrPublisher)) { + throw new Error('Expected publisher string, found extension identifier'); + } + extensionOrPublisher = extensionOrPublisher.toLowerCase(); + if (autoUpdateExtensions.includes(extensionOrPublisher) !== enable) { + update = true; + if (enable) { + autoUpdateExtensions.push(extensionOrPublisher); + } else { + autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionOrPublisher), 1); + } + } + } else { + const extensionId = extensionOrPublisher.identifier.id.toLowerCase(); + const enableAutoUpdatesForPublisher = autoUpdateExtensions.includes(extensionOrPublisher.publisher.toLowerCase()); + const enableAutoUpdatesForExtension = autoUpdateExtensions.includes(extensionId); + const disableAutoUpdatesForExtension = autoUpdateExtensions.includes(`-${extensionId}`); + + if (enable) { + if (disableAutoUpdatesForExtension) { + autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(`-${extensionId}`), 1); + update = true; + } + if (enableAutoUpdatesForPublisher) { + if (enableAutoUpdatesForExtension) { + autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionId), 1); + update = true; + } + } else { + if (!enableAutoUpdatesForExtension) { + autoUpdateExtensions.push(extensionId); + update = true; + } + } + } + // Disable Auto Updates + else { + if (enableAutoUpdatesForExtension) { + autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionId), 1); + update = true; + } + if (enableAutoUpdatesForPublisher) { + if (!disableAutoUpdatesForExtension) { + autoUpdateExtensions.push(`-${extensionId}`); + update = true; + } + } else { + if (disableAutoUpdatesForExtension) { + autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(`-${extensionId}`), 1); + update = true; + } + } + } + } + if (update) { + this.setSelectedExtensionsToAutoUpdate(autoUpdateExtensions); + await this.onDidPinnedViewContainersStorageValueChange(true); + } } - async pinExtension(extension: IExtension, pinned: boolean): Promise { - if (!extension.local) { - throw new Error('Only installed extensions can be pinned'); + private async onDidPinnedViewContainersStorageValueChange(force: boolean): Promise { + if (force || this.selectedExtensionsToAutoUpdateValue !== this.getSelectedExtensionsToAutoUpdateValue() /* This checks if current window changed the value or not */) { + await this.updateExtensionsPinnedState(); + this.eventuallyAutoUpdateExtensions(); } - await this.extensionManagementService.updateMetadata(extension.local, { pinned }); } async canInstall(extension: IExtension): Promise { @@ -1596,7 +1739,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } async installVersion(extension: IExtension, version: string, installOptions: InstallOptions = {}): Promise { - return this.doInstall(extension, async () => { + extension = await this.doInstall(extension, async () => { if (!extension.gallery) { throw new Error('Missing gallery'); } @@ -1610,6 +1753,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installOptions.installGivenVersion = true; return this.installFromGallery(extension, gallery, installOptions); }); + await this.updateAutoUpdateEnablementFor(extension, false); + return extension; } reinstall(extension: IExtension): Promise { @@ -1693,14 +1838,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private async installFromVSIX(vsix: URI, installOptions?: InstallVSIXOptions): Promise { const manifest = await this.extensionManagementService.getManifest(vsix); const existingExtension = this.local.find(local => areSameExtensions(local.identifier, { id: getGalleryExtensionId(manifest.publisher, manifest.name) })); - if (existingExtension && existingExtension.latestVersion !== manifest.version) { + if (existingExtension) { installOptions = installOptions || {}; - installOptions.installGivenVersion = true; + if (existingExtension.latestVersion === manifest.version) { + installOptions.pinned = existingExtension.local?.pinned || !this.shouldAutoUpdateExtension(existingExtension); + } else { + installOptions.installGivenVersion = true; + } } return this.extensionManagementService.installVSIX(vsix, manifest, installOptions); } private installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { + installOptions = installOptions ?? {}; + installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); if (extension.local) { return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); } else { @@ -1935,4 +2086,46 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }).then(undefined, error => this.onError(error)); } + private getPublishersToAutoUpdate(): string[] { + return this.getSelectedExtensionsToAutoUpdate().filter(id => !EXTENSION_IDENTIFIER_REGEX.test(id)); + } + + getSelectedExtensionsToAutoUpdate(): string[] { + try { + const parsedValue = JSON.parse(this.selectedExtensionsToAutoUpdateValue); + if (Array.isArray(parsedValue)) { + return parsedValue; + } + } catch (e) { /* Ignore */ } + return []; + } + + private setSelectedExtensionsToAutoUpdate(selectedExtensionsToAutoUpdate: string[]): void { + this.selectedExtensionsToAutoUpdateValue = JSON.stringify(selectedExtensionsToAutoUpdate); + } + + private _selectedExtensionsToAutoUpdateValue: string | undefined; + private get selectedExtensionsToAutoUpdateValue(): string { + if (!this._selectedExtensionsToAutoUpdateValue) { + this._selectedExtensionsToAutoUpdateValue = this.getSelectedExtensionsToAutoUpdateValue(); + } + + return this._selectedExtensionsToAutoUpdateValue; + } + + private set selectedExtensionsToAutoUpdateValue(placeholderViewContainesValue: string) { + if (this.selectedExtensionsToAutoUpdateValue !== placeholderViewContainesValue) { + this._selectedExtensionsToAutoUpdateValue = placeholderViewContainesValue; + this.setSelectedExtensionsToAutoUpdateValue(placeholderViewContainesValue); + } + } + + private getSelectedExtensionsToAutoUpdateValue(): string { + return this.storageService.get(EXTENSIONS_AUTO_UPDATE_KEY, StorageScope.PROFILE, '[]'); + } + + private setSelectedExtensionsToAutoUpdateValue(value: string): void { + this.storageService.store(EXTENSIONS_AUTO_UPDATE_KEY, value, StorageScope.PROFILE, StorageTarget.USER); + } + } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 3b77ce18a1ef1..77fdf576c770d 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -65,7 +65,6 @@ export interface IExtension { readonly rating?: number; readonly ratingCount?: number; readonly outdated: boolean; - readonly pinned: boolean; readonly outdatedTargetPlatform: boolean; readonly reloadRequiredStatus?: string; readonly enablementState: EnablementState; @@ -113,8 +112,11 @@ export interface IExtensionsWorkbenchService { canSetLanguage(extension: IExtension): boolean; setLanguage(extension: IExtension): Promise; setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise; - pinExtension(extension: IExtension, pin: boolean): Promise; + isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean; + updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise; open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; + isAutoUpdateEnabled(): boolean; + getAutoUpdateValue(): AutoUpdateConfigurationValue; checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; updateAll(): Promise; @@ -139,6 +141,8 @@ export const AutoUpdateConfigurationKey = 'extensions.autoUpdate'; export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates'; export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange'; +export type AutoUpdateConfigurationValue = boolean | 'onlyEnabledExtensions' | 'onlySelectedExtensions'; + export interface IExtensionsConfiguration { autoUpdate: boolean; autoCheckUpdates: boolean; @@ -200,5 +204,6 @@ export const CONTEXT_HAS_GALLERY = new RawContextKey('hasGallery', fals // Context Menu Groups export const THEME_ACTIONS_GROUP = '_theme_'; export const INSTALL_ACTIONS_GROUP = '0_install'; +export const UPDATE_ACTIONS_GROUP = '0_update'; export const extensionsSearchActionsMenu = new MenuId('extensionsSearchActionsMenu'); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 91d873c7af08b..f979e0a9aacb9 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -6,7 +6,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; -import { IExtensionsWorkbenchService, ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, @@ -50,11 +50,13 @@ import { arch } from 'vs/base/common/process'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { Mutable } from 'vs/base/common/types'; +import { IProductConfiguration } from 'vs/base/common/product'; suite('ExtensionsWorkbenchServiceTest', () => { let instantiationService: TestInstantiationService; - let testObject: IExtensionsWorkbenchService; + let testObject: ExtensionsWorkbenchService; const disposableStore = ensureNoDisposablesAreLeakedInTestSuite(); let installEvent: Emitter, @@ -81,12 +83,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); - instantiationService.stub(IConfigurationService, >{ - onDidChangeConfiguration: () => { return undefined!; }, - getValue: (key?: string) => { - return (key === AutoCheckUpdatesConfigurationKey || key === AutoUpdateConfigurationKey) ? true : undefined; - } - }); + stubConfiguration(); instantiationService.stub(IRemoteAgentService, RemoteAgentService); @@ -1409,12 +1406,271 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(actual[0].local, remoteExtension); }); + test('Test disable autoupdate for extension when auto update is enabled for all', async () => { + const extension1 = aLocalExtension('a'); + const extension2 = aLocalExtension('b'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + assert.strictEqual(testObject.local[0].local?.pinned, undefined); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); + + assert.strictEqual(testObject.local[0].local?.pinned, true); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); + }); + + test('Test enable autoupdate for extension when auto update is enabled for all', async () => { + const extension1 = aLocalExtension('a'); + const extension2 = aLocalExtension('b'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + assert.strictEqual(testObject.local[0].local?.pinned, undefined); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); + + assert.strictEqual(testObject.local[0].local?.pinned, false); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); + }); + + test('Test updateAutoUpdateEnablementFor throws error when auto update is disabled', async () => { + stubConfiguration(false); + + const extension1 = aLocalExtension('a'); + const extension2 = aLocalExtension('b'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + testObject = await aWorkbenchService(); + + try { + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); + assert.fail('error expected'); + } catch (error) { + // expected + } + }); + + test('Test updateAutoUpdateEnablementFor throws error for publisher when auto update is enabled', async () => { + const extension1 = aLocalExtension('a'); + const extension2 = aLocalExtension('b'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + testObject = await aWorkbenchService(); + + try { + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); + assert.fail('error expected'); + } catch (error) { + // expected + } + }); + + test('Test updateAutoUpdateEnablementFor throws error for extension id when auto update mode is onlySelectedExtensions', async () => { + stubConfiguration('onlySelectedExtensions'); + + const extension1 = aLocalExtension('a'); + const extension2 = aLocalExtension('b'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + testObject = await aWorkbenchService(); + + try { + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].identifier.id, true); + assert.fail('error expected'); + } catch (error) { + // expected + } + }); + + test('Test enable autoupdate for extension when auto update is set to onlySelectedExtensions', async () => { + stubConfiguration('onlySelectedExtensions'); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', undefined, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + assert.strictEqual(testObject.local[0].local?.pinned, true); + assert.strictEqual(testObject.local[1].local?.pinned, true); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); + + assert.strictEqual(testObject.local[0].local?.pinned, false); + assert.strictEqual(testObject.local[1].local?.pinned, true); + + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub.a']); + }); + + test('Test disable autoupdate for extension when auto update is set to onlySelectedExtensions', async () => { + stubConfiguration('onlySelectedExtensions'); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', undefined, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); + + assert.strictEqual(testObject.local[0].local?.pinned, true); + assert.strictEqual(testObject.local[1].local?.pinned, true); + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); + }); + + test('Test enable auto update for publisher when auto update mode is onlySelectedExtensions', async () => { + stubConfiguration('onlySelectedExtensions'); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', undefined, { pinned: true }); + const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); + + assert.strictEqual(testObject.local[0].local?.pinned, false); + assert.strictEqual(testObject.local[1].local?.pinned, false); + assert.strictEqual(testObject.local[2].local?.pinned, true); + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub']); + }); + + test('Test disable auto update for publisher when auto update mode is onlySelectedExtensions', async () => { + stubConfiguration('onlySelectedExtensions'); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', undefined, { pinned: true }); + const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, false); + + assert.strictEqual(testObject.local[0].local?.pinned, true); + assert.strictEqual(testObject.local[1].local?.pinned, true); + assert.strictEqual(testObject.local[2].local?.pinned, true); + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); + }); + + test('Test disable auto update for an extension when auto update for publisher is enabled and update mode is onlySelectedExtensions', async () => { + stubConfiguration('onlySelectedExtensions'); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', undefined, { pinned: true }); + const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); + + assert.strictEqual(testObject.local[0].local?.pinned, true); + assert.strictEqual(testObject.local[1].local?.pinned, false); + assert.strictEqual(testObject.local[2].local?.pinned, true); + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub', '-pub.a']); + }); + + test('Test enable auto update for an extension when auto updates is enabled for publisher and disabled for extension and update mode is onlySelectedExtensions', async () => { + stubConfiguration('onlySelectedExtensions'); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', undefined, { pinned: true }); + const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); + + assert.strictEqual(testObject.local[0].local?.pinned, false); + assert.strictEqual(testObject.local[1].local?.pinned, false); + assert.strictEqual(testObject.local[2].local?.pinned, true); + assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub']); + }); + + test('Test enable auto update for publisher enables for the associated organisation', async () => { + stubConfiguration('onlySelectedExtensions'); + stubProductConfiguration({ publishersByOrganisation: { 'org1': ['pub', 'pub1'] } }); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', { publisher: 'pub1' }, { pinned: true }); + const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); + + assert.strictEqual(testObject.isAutoUpdateEnabledFor('pub'), true); + assert.strictEqual(testObject.isAutoUpdateEnabledFor('pub1'), true); + assert.strictEqual(testObject.isAutoUpdateEnabledFor('pub2'), false); + assert.strictEqual(testObject.isAutoUpdateEnabledFor(testObject.local[0]), true); + assert.strictEqual(testObject.isAutoUpdateEnabledFor(testObject.local[1]), true); + assert.strictEqual(testObject.isAutoUpdateEnabledFor(testObject.local[2]), false); + }); + async function aWorkbenchService(): Promise { const workbenchService: ExtensionsWorkbenchService = disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService)); await workbenchService.queryLocal(); return workbenchService; } + function stubConfiguration(autoUpdateValue?: any, autoCheckUpdatesValue?: any): void { + instantiationService.stub(IConfigurationService, >{ + onDidChangeConfiguration: () => { return undefined!; }, + getValue: (key?: string) => { + return key === AutoUpdateConfigurationKey ? autoUpdateValue ?? true + : key === AutoCheckUpdatesConfigurationKey ? autoCheckUpdatesValue ?? true + : undefined; + } + }); + } + + function stubProductConfiguration(productConfiguration: Partial): void { + instantiationService.stub(IProductService, productConfiguration); + } + function aLocalExtension(name: string = 'someext', manifest: any = {}, properties: any = {}): ILocalExtension { manifest = { name, publisher: 'pub', version: '1.0.0', ...manifest }; properties = { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 1d3a1d85b4001..9927bd3d34250 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -289,7 +289,7 @@ class InstallExtensionTask extends AbstractExtensionTask implem ? this.options.installPreReleaseVersion /* Respect the passed flag */ : metadata?.preRelease /* Respect the existing pre-release flag if it was set */); } - metadata.pinned = this.options.installGivenVersion ? true : metadata.pinned; + metadata.pinned = this.options.installGivenVersion ? true : (this.options.pinned ?? metadata.pinned); this._profileLocation = metadata.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : this.options.profileLocation; const scannedExtension = URI.isUri(this.extension) ? await this.webExtensionsScannerService.addExtension(this.extension, metadata, this.profileLocation)