From 7058d513b94e33ddc8eb21bd8f69957a3d1b2a55 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 19 Nov 2024 19:05:02 +0100 Subject: [PATCH] allow canInstall provide a reason if an extension cannot be installed (#234198) --- .../abstractExtensionManagementService.ts | 10 +++++-- .../common/extensionManagement.ts | 3 +- .../common/extensionManagementIpc.ts | 20 ++++++++----- .../userDataSync/common/extensionsSync.ts | 2 +- ...ensionRecommendationNotificationService.ts | 4 +-- .../extensions/browser/extensionsActions.ts | 22 +++++--------- .../extensions/browser/extensionsViews.ts | 5 ++-- .../browser/extensionsWorkbenchService.ts | 29 +++++++++++-------- .../contrib/extensions/common/extensions.ts | 3 +- .../extensionsWorkbenchService.test.ts | 6 ++-- .../notebookKernelQuickPickStrategy.ts | 2 +- .../common/extensionManagement.ts | 3 +- .../extensionManagementChannelClient.ts | 4 ++- .../common/extensionManagementService.ts | 29 +++++++++++-------- .../remoteExtensionManagementService.ts | 4 ++- .../common/webExtensionManagementService.ts | 9 ++++-- .../nativeExtensionManagementService.ts | 4 ++- .../remoteExtensionManagementService.ts | 4 +-- .../browser/extensionsResource.ts | 4 +-- .../test/browser/workbenchTestServices.ts | 2 +- 20 files changed, 97 insertions(+), 72 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 0f8205c57d7e2..6dc6b35c69c93 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -31,6 +31,7 @@ import { IProductService } from '../../product/common/productService.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; +import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions }; @@ -99,9 +100,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } - async canInstall(extension: IGalleryExtension): Promise { + async canInstall(extension: IGalleryExtension): Promise { const currentTargetPlatform = await this.getTargetPlatform(); - return extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform)); + if (extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform))) { + return true; + } + return new MarkdownString(`${nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.displayName ?? extension.identifier.id, this.productService.nameLong, TargetPlatformToString(currentTargetPlatform))} [${nls.localize('learn more', "Learn More")}](https://aka.ms/vscode-platform-specific-extensions)`); } async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise { @@ -598,7 +602,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } else { - if (!await this.canInstall(extension)) { + if (await this.canInstall(extension) !== true) { const targetPlatform = await this.getTargetPlatform(); throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform); } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 42e8875e7ccbb..f6bf93a0b26a9 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { IStringDictionary } from '../../../base/common/collections.js'; import { Event } from '../../../base/common/event.js'; +import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IPager } from '../../../base/common/paging.js'; import { Platform } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; @@ -562,7 +563,7 @@ export interface IExtensionManagementService { zip(extension: ILocalExtension): Promise; getManifest(vsix: URI): Promise; install(vsix: URI, options?: InstallOptions): Promise; - canInstall(extension: IGalleryExtension): Promise; + canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise; installFromLocation(location: URI, profileLocation: URI): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 33d368d7397f6..4c1821509ea69 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,8 +9,11 @@ import { cloneAndChange } from '../../../base/common/objects.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from '../../../base/common/uriIpc.js'; import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata, UninstallExtensionInfo } from './extensionManagement.js'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata, UninstallExtensionInfo, TargetPlatformToString } from './extensionManagement.js'; import { ExtensionType, IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; +import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; +import { localize } from '../../../nls.js'; +import { IProductService } from '../../product/common/productService.js'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined; @@ -124,9 +127,6 @@ export class ExtensionManagementChannel implements IServerChannel { case 'getTargetPlatform': { return this.service.getTargetPlatform(); } - case 'canInstall': { - return this.service.canInstall(args[0]); - } case 'installFromGallery': { return this.service.installFromGallery(args[0], transformIncomingOptions(args[1], uriTransformer)); } @@ -202,7 +202,10 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; } - constructor(private readonly channel: IChannel) { + constructor( + private readonly channel: IChannel, + protected readonly productService: IProductService + ) { super(); this._register(this.channel.listen('onInstallExtension')(e => this.onInstallExtensionEvent({ ...e, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))); this._register(this.channel.listen('onDidInstallExtensions')(results => this.onDidInstallExtensionsEvent(results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))))); @@ -247,9 +250,12 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return this._targetPlatformPromise; } - async canInstall(extension: IGalleryExtension): Promise { + async canInstall(extension: IGalleryExtension): Promise { const currentTargetPlatform = await this.getTargetPlatform(); - return extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform)); + if (extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform))) { + return true; + } + return new MarkdownString(`${localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.displayName ?? extension.identifier.id, this.productService.nameLong, TargetPlatformToString(currentTargetPlatform))} [${localize('learn more', "Learn More")}](https://aka.ms/vscode-platform-specific-extensions)`); } zip(extension: ILocalExtension): Promise { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index c917d7ac87101..1715f8cbbe958 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -477,7 +477,7 @@ export class LocalExtensionsProvider { || installedExtension.pinned !== e.pinned // Install if the extension pinned preference has changed || (version && installedExtension.manifest.version !== version) // Install if the extension version has changed ) { - if (await this.extensionManagementService.canInstall(extension)) { + if (await this.extensionManagementService.canInstall(extension) === true) { extensionsToInstall.push({ extension, options: { isMachineScoped: false /* set isMachineScoped value to prevent install and sync dialog in web */, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 1fc880f246531..323622a7ac422 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -443,7 +443,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple if (galleryExtensions.length) { const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: 'install-recommendations' }, CancellationToken.None); for (const extension of extensions) { - if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + if (extension.gallery && await this.extensionManagementService.canInstall(extension.gallery) === true) { result.push(extension); } } @@ -451,7 +451,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple if (resourceExtensions.length) { const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true); for (const extension of extensions) { - if (await this.extensionsWorkbenchService.canInstall(extension)) { + if (await this.extensionsWorkbenchService.canInstall(extension) === true) { result.push(extension); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 3930f44a866f0..0cf14b097ffba 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -14,7 +14,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { disposeIfDisposable } from '../../../../base/common/lifecycle.js'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; import { ExtensionsConfigurationInitialContent } from '../common/extensionsFileTemplate.js'; -import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { areSameExtensions, getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -447,7 +447,7 @@ export class InstallAction extends ExtensionAction { } this.hidden = false; this.class = InstallAction.CLASS; - if (await this.extensionsWorkbenchService.canInstall(this.extension)) { + if (await this.extensionsWorkbenchService.canInstall(this.extension) === true) { this.enabled = true; this.updateLabel(); } @@ -947,7 +947,7 @@ export class UpdateAction extends ExtensionAction { const canInstall = await this.extensionsWorkbenchService.canInstall(this.extension); const isInstalled = this.extension.state === ExtensionState.Installed; - this.enabled = canInstall && isInstalled && this.extension.outdated; + this.enabled = canInstall === true && isInstalled && this.extension.outdated; this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass; } @@ -2556,18 +2556,10 @@ export class ExtensionStatusAction extends ExtensionAction { } } - if (this.extension.gallery && this.extension.state === ExtensionState.Uninstalled && !await this.extensionsWorkbenchService.canInstall(this.extension)) { - if (this.extensionManagementServerService.localExtensionManagementServer || this.extensionManagementServerService.remoteExtensionManagementServer) { - const targetPlatform = await (this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getTargetPlatform() : this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getTargetPlatform()); - const message = new MarkdownString(`${localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", this.extension.displayName || this.extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform))} [${localize('learn more', "Learn More")}](https://aka.ms/vscode-platform-specific-extensions)`); - this.updateStatus({ icon: warningIcon, message }, true); - return; - } - - if (this.extensionManagementServerService.webExtensionManagementServer) { - const productName = localize('VS Code for Web', "{0} for the Web", this.productService.nameLong); - const message = new MarkdownString(`${localize('not web tooltip', "The '{0}' extension is not available in {1}.", this.extension.displayName || this.extension.identifier.id, productName)} [${localize('learn why', "Learn Why")}](https://aka.ms/vscode-web-extensions-guide)`); - this.updateStatus({ icon: warningIcon, message }, true); + if (this.extension.gallery && this.extension.state === ExtensionState.Uninstalled) { + const result = await this.extensionsWorkbenchService.canInstall(this.extension); + if (result !== true) { + this.updateStatus({ icon: warningIcon, message: result }, true); return; } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 9fbe55ef7de1d..8abcf780b3d20 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -945,7 +945,8 @@ export class ExtensionsListView extends ViewPane { try { const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: options.source }, token); for (const extension of extensions) { - if (extension.gallery && !extension.deprecationInfo && (await this.extensionManagementService.canInstall(extension.gallery))) { + if (extension.gallery && !extension.deprecationInfo + && await this.extensionManagementService.canInstall(extension.gallery) === true) { result.push(extension); } } @@ -958,7 +959,7 @@ export class ExtensionsListView extends ViewPane { if (resourceExtensions.length) { const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true); for (const extension of extensions) { - if (await this.extensionsWorkbenchService.canInstall(extension)) { + if (await this.extensionsWorkbenchService.canInstall(extension) === true) { result.push(extension); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 8c476bafc7792..3f55d68d56c09 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -64,6 +64,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; +import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; interface IExtensionStateProvider { (extension: Extension): T; @@ -720,7 +721,7 @@ class Extensions extends Disposable { return this.workbenchExtensionManagementService.updateMetadata(localExtension, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId, isPreReleaseVersion }); } - canInstall(galleryExtension: IGalleryExtension): Promise { + canInstall(galleryExtension: IGalleryExtension): Promise { return this.server.extensionManagementService.canInstall(galleryExtension); } @@ -2250,43 +2251,47 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } - async canInstall(extension: IExtension): Promise { + async canInstall(extension: IExtension): Promise { if (!(extension instanceof Extension)) { - return false; + return new MarkdownString().appendText(nls.localize('not an extension', "The provided object is not an extension.")); } if (extension.isMalicious) { - return false; + return new MarkdownString().appendText(nls.localize('malicious', "This extension is reported to be problematic.")); } if (extension.deprecationInfo?.disallowInstall) { - return false; + return new MarkdownString().appendText(nls.localize('disallowed', "This extension is disallowed to be installed.")); } if (extension.gallery) { if (!extension.gallery.isSigned) { - return false; + return new MarkdownString().appendText(nls.localize('not signed', "This extension is not signed.")); } - if (this.localExtensions && await this.localExtensions.canInstall(extension.gallery)) { + const localResult = this.localExtensions ? await this.localExtensions.canInstall(extension.gallery) : undefined; + if (localResult === true) { return true; } - if (this.remoteExtensions && await this.remoteExtensions.canInstall(extension.gallery)) { + const remoteResult = this.remoteExtensions ? await this.remoteExtensions.canInstall(extension.gallery) : undefined; + if (remoteResult === true) { return true; } - if (this.webExtensions && await this.webExtensions.canInstall(extension.gallery)) { + const webResult = this.webExtensions ? await this.webExtensions.canInstall(extension.gallery) : undefined; + if (webResult === true) { return true; } - return false; + + return localResult ?? remoteResult ?? webResult ?? new MarkdownString().appendText(nls.localize('cannot be installed', "Cannot install the '{0}' extension because it is not available in this setup.", extension.displayName ?? extension.identifier.id)); } - if (extension.resourceExtension && await this.extensionManagementService.canInstall(extension.resourceExtension)) { + if (extension.resourceExtension && await this.extensionManagementService.canInstall(extension.resourceExtension) === true) { return true; } - return false; + return new MarkdownString().appendText(nls.localize('cannot be installed', "Cannot install the '{0}' extension because it is not available in this setup.", extension.displayName ?? extension.identifier.id)); } async install(arg: string | URI | IExtension, installOptions: InstallExtensionOptions = {}, progressLocation?: ProgressLocation | string): Promise { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index ce13d92d82785..7298230d2c645 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -20,6 +20,7 @@ import { IExtensionEditorOptions } from './extensionsInput.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { Severity } from '../../../../platform/notification/common/notification.js'; +import { IMarkdownString } from '../../../../base/common/htmlContent.js'; export const VIEWLET_ID = 'workbench.view.extensions'; @@ -133,7 +134,7 @@ export interface IExtensionsWorkbenchService { getExtensions(extensionInfos: IExtensionInfo[], token: CancellationToken): Promise; getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise; getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise; - canInstall(extension: IExtension): Promise; + canInstall(extension: IExtension): Promise; install(id: string, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise; install(vsix: URI, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise; install(extension: IExtension, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise; 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 fd474b58a0051..3283dc88cb408 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 @@ -425,7 +425,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); - assert.ok(!(await testObject.canInstall(target))); + assert.ok(await testObject.canInstall(target) !== true); }); test('test canInstall returns false for a system extension', async () => { @@ -435,7 +435,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const target = testObject.local[0]; - assert.ok(!(await testObject.canInstall(target))); + assert.ok(await testObject.canInstall(target) !== true); }); test('test canInstall returns true for extensions with gallery', async () => { @@ -449,7 +449,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const target = testObject.local[0]; await Event.toPromise(Event.filter(testObject.onChange, e => !!e?.gallery)); - assert.ok(await testObject.canInstall(target)); + assert.equal(await testObject.canInstall(target), true); }); test('test onchange event is triggered while installing', async () => { diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index f14a7619562e8..3d2c44c67ecac 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -295,7 +295,7 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { extensionsToEnable.push(extension); } else { const canInstall = await extensionWorkbenchService.canInstall(extension); - if (canInstall) { + if (canInstall === true) { extensionsToInstall.push(extension); } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 0244e0aa381b1..906d99dc5acd4 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -10,6 +10,7 @@ import { IExtensionManagementService, IGalleryExtension, ILocalExtension, Instal import { URI } from '../../../../base/common/uri.js'; import { FileAccess } from '../../../../base/common/network.js'; import { localize } from '../../../../nls.js'; +import { IMarkdownString } from '../../../../base/common/htmlContent.js'; export type DidChangeProfileEvent = { readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }; @@ -78,7 +79,7 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten getInstalledWorkspaceExtensionLocations(): URI[]; getInstalledWorkspaceExtensions(includeInvalid: boolean): Promise; - canInstall(extension: IGalleryExtension | IResourceExtension): Promise; + canInstall(extension: IGalleryExtension | IResourceExtension): Promise; installVSIX(location: URI, manifest: IExtensionManifest, installOptions?: InstallOptions): Promise; installFromLocation(location: URI): Promise; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index 21a60fa14289c..f8ff8d73b911d 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -14,6 +14,7 @@ import { delta } from '../../../../base/common/arrays.js'; import { compare } from '../../../../base/common/strings.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { DidChangeProfileEvent, IProfileAwareExtensionManagementService } from './extensionManagement.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; export abstract class ProfileAwareExtensionManagementChannelClient extends BaseExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { @@ -30,10 +31,11 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE get onProfileAwareDidUpdateExtensionMetadata() { return this._onDidProfileAwareUpdateExtensionMetadata.event; } constructor(channel: IChannel, + productService: IProductService, protected readonly userDataProfileService: IUserDataProfileService, protected readonly uriIdentityService: IUriIdentityService, ) { - super(channel); + super(channel, productService); this._register(userDataProfileService.onDidChangeCurrentProfile(e => { if (!this.uriIdentityService.extUri.isEqual(e.previous.extensionsResource, e.profile.extensionsResource)) { e.join(this.whenProfileChanged(e)); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index a5bd6a725cfb3..0bf26689d7e3e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -42,6 +42,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; function isGalleryExtension(extension: IResourceExtension | IGalleryExtension): extension is IGalleryExtension { return extension.type === 'gallery'; @@ -365,36 +366,36 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.reject('No Servers'); } - async canInstall(extension: IGalleryExtension | IResourceExtension): Promise { + async canInstall(extension: IGalleryExtension | IResourceExtension): Promise { if (isGalleryExtension(extension)) { return this.canInstallGalleryExtension(extension); } return this.canInstallResourceExtension(extension); } - private async canInstallGalleryExtension(gallery: IGalleryExtension): Promise { + private async canInstallGalleryExtension(gallery: IGalleryExtension): Promise { if (this.extensionManagementServerService.localExtensionManagementServer - && await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(gallery)) { + && await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(gallery) === true) { return true; } const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); if (!manifest) { - return false; + return new MarkdownString().appendText(localize('manifest is not found', "Manifest is not found")); } if (this.extensionManagementServerService.remoteExtensionManagementServer - && await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.canInstall(gallery) + && await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.canInstall(gallery) === true && this.extensionManifestPropertiesService.canExecuteOnWorkspace(manifest)) { return true; } if (this.extensionManagementServerService.webExtensionManagementServer - && await this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.canInstall(gallery) + && await this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.canInstall(gallery) === true && this.extensionManifestPropertiesService.canExecuteOnWeb(manifest)) { return true; } - return false; + return new MarkdownString().appendText(localize('cannot be installed', "Cannot install the '{0}' extension because it is not available in this setup.", gallery.displayName || gallery.name)); } - private canInstallResourceExtension(extension: IResourceExtension): boolean { + private async canInstallResourceExtension(extension: IResourceExtension): Promise { if (this.extensionManagementServerService.localExtensionManagementServer) { return true; } @@ -404,7 +405,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (this.extensionManagementServerService.webExtensionManagementServer && this.extensionManifestPropertiesService.canExecuteOnWeb(extension.manifest)) { return true; } - return false; + return new MarkdownString().appendText(localize('cannot be installed', "Cannot install the '{0}' extension because it is not available in this setup.", extension.manifest.displayName ?? extension.identifier.id)); } async updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions): Promise { @@ -434,7 +435,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench try { const servers = await this.validateAndGetExtensionManagementServersToInstall(extension, options); if (!options.isMachineScoped && this.isExtensionsSyncEnabled()) { - if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) && (await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(extension))) { + if (this.extensionManagementServerService.localExtensionManagementServer + && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) + && await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(extension) === true) { servers.push(this.extensionManagementServerService.localExtensionManagementServer); } } @@ -473,7 +476,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } if (!installOptions.isMachineScoped && this.isExtensionsSyncEnabled()) { - if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) && (await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(gallery))) { + if (this.extensionManagementServerService.localExtensionManagementServer + && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) + && await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(gallery) === true) { servers.push(this.extensionManagementServerService.localExtensionManagementServer); } } @@ -754,7 +759,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (manifest.extensionPack?.length) { const extensions = await this.extensionGalleryService.getExtensions(manifest.extensionPack.map(id => ({ id })), CancellationToken.None); for (const extension of extensions) { - if (!(await this.servers[0].extensionManagementService.canInstall(extension))) { + if (await this.servers[0].extensionManagementService.canInstall(extension) !== true) { nonWebExtensions.push(extension); } } diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts index 4d06795a9cc31..c77f5b649a193 100644 --- a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts @@ -12,17 +12,19 @@ import { ProfileAwareExtensionManagementChannelClient } from './extensionManagem import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; export class RemoteExtensionManagementService extends ProfileAwareExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { constructor( channel: IChannel, + @IProductService productService: IProductService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService ) { - super(channel, userDataProfileService, uriIdentityService); + super(channel, productService, userDataProfileService, uriIdentityService); } protected async filterEvent(profileLocation: URI, applicationScoped: boolean): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 1ada0726fdaff..dc02d1a4e72e8 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -22,6 +22,8 @@ import { compare } from '../../../../base/common/strings.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { localize } from '../../../../nls.js'; export class WebExtensionManagementService extends AbstractExtensionManagementService implements IProfileAwareExtensionManagementService { @@ -78,14 +80,15 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return TargetPlatform.WEB; } - override async canInstall(gallery: IGalleryExtension): Promise { - if (await super.canInstall(gallery)) { + override async canInstall(gallery: IGalleryExtension): Promise { + if (await super.canInstall(gallery) === true) { return true; } if (this.isConfiguredToExecuteOnWeb(gallery)) { return true; } - return false; + const productName = localize('VS Code for Web', "{0} for the Web", this.productService.nameLong); + return new MarkdownString(`${localize('not web tooltip', "The '{0}' extension is not available in {1}.", gallery.displayName || gallery.identifier.id, productName)} [${localize('learn why', "Learn Why")}](https://aka.ms/vscode-web-extensions-guide)`); } async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index af1a1f1fa7c21..4f71691689558 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -18,11 +18,13 @@ import { generateUuid } from '../../../../base/common/uuid.js'; import { ProfileAwareExtensionManagementChannelClient } from '../common/extensionManagementChannelClient.js'; import { ExtensionIdentifier, ExtensionType, isResolverExtension } from '../../../../platform/extensions/common/extensions.js'; import { INativeWorkbenchEnvironmentService } from '../../environment/electron-sandbox/environmentService.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; export class NativeExtensionManagementService extends ProfileAwareExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { constructor( channel: IChannel, + @IProductService productService: IProductService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IFileService private readonly fileService: IFileService, @@ -30,7 +32,7 @@ export class NativeExtensionManagementService extends ProfileAwareExtensionManag @INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService, @ILogService private readonly logService: ILogService, ) { - super(channel, userDataProfileService, uriIdentityService); + super(channel, productService, userDataProfileService, uriIdentityService); } protected filterEvent(profileLocation: URI, isApplicationScoped: boolean): boolean { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 1cd69fbf9f034..6d0db640b30aa 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -32,6 +32,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag constructor( channel: IChannel, private readonly localExtensionManagementServer: IExtensionManagementServer, + @IProductService productService: IProductService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IRemoteUserDataProfilesService remoteUserDataProfilesService: IRemoteUserDataProfilesService, @@ -39,11 +40,10 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag @ILogService private readonly logService: ILogService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IProductService private readonly productService: IProductService, @IFileService private readonly fileService: IFileService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(channel, userDataProfileService, userDataProfilesService, remoteUserDataProfilesService, uriIdentityService); + super(channel, productService, userDataProfileService, userDataProfilesService, remoteUserDataProfilesService, uriIdentityService); } override async install(vsix: URI, options?: InstallOptions): Promise { diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 57305bd2583ee..17138ecb9e7cf 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -74,7 +74,7 @@ export class ExtensionsResourceInitializer implements IProfileResourceInitialize if (!extension) { return; } - if (await this.extensionManagementService.canInstall(extension)) { + if (await this.extensionManagementService.canInstall(extension) === true) { this.logService.trace(`Initializing Profile: Installing extension...`, extension.identifier.id, extension.version); await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false,/* set isMachineScoped value to prevent install and sync dialog in web */ @@ -153,7 +153,7 @@ export class ExtensionsResource implements IProfileResource { if (!extension) { return; } - if (await this.extensionManagementService.canInstall(extension)) { + if (await this.extensionManagementService.canInstall(extension) === true) { installExtensionInfos.push({ extension, options: { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 0a7fef110d822..4d822fd7726c0 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2209,7 +2209,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens install(vsix: URI, options?: InstallOptions | undefined): Promise { throw new Error('Method not implemented.'); } - async canInstall(extension: IGalleryExtension): Promise { return false; } + async canInstall(extension: IGalleryExtension): Promise { return true; } installFromGallery(extension: IGalleryExtension, options?: InstallOptions | undefined): Promise { throw new Error('Method not implemented.'); }