Skip to content

Commit

Permalink
allow canInstall provide a reason if an extension cannot be installed (
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 authored Nov 19, 2024
1 parent 1a5b235 commit 7058d51
Show file tree
Hide file tree
Showing 20 changed files with 97 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -99,9 +100,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}));
}

async canInstall(extension: IGalleryExtension): Promise<boolean> {
async canInstall(extension: IGalleryExtension): Promise<true | IMarkdownString> {
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<ILocalExtension> {
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -562,7 +563,7 @@ export interface IExtensionManagementService {
zip(extension: ILocalExtension): Promise<URI>;
getManifest(vsix: URI): Promise<IExtensionManifest>;
install(vsix: URI, options?: InstallOptions): Promise<ILocalExtension>;
canInstall(extension: IGalleryExtension): Promise<boolean>;
canInstall(extension: IGalleryExtension): Promise<true | IMarkdownString>;
installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>;
installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<InstallExtensionResult[]>;
installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -202,7 +202,10 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter<DidUpdateExtensionMetadata>());
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<InstallExtensionEvent>('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<readonly InstallExtensionResult[]>('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) })))));
Expand Down Expand Up @@ -247,9 +250,12 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
return this._targetPlatformPromise;
}

async canInstall(extension: IGalleryExtension): Promise<boolean> {
async canInstall(extension: IGalleryExtension): Promise<true | IMarkdownString> {
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<URI> {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/userDataSync/common/extensionsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,15 +443,15 @@ 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);
}
}
}
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);
}
}
Expand Down
22 changes: 7 additions & 15 deletions src/vs/workbench/contrib/extensions/browser/extensionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
(extension: Extension): T;
Expand Down Expand Up @@ -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<boolean> {
canInstall(galleryExtension: IGalleryExtension): Promise<true | IMarkdownString> {
return this.server.extensionManagementService.canInstall(galleryExtension);
}

Expand Down Expand Up @@ -2250,43 +2251,47 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
}
}

async canInstall(extension: IExtension): Promise<boolean> {
async canInstall(extension: IExtension): Promise<true | IMarkdownString> {
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<IExtension> {
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/contrib/extensions/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -133,7 +134,7 @@ export interface IExtensionsWorkbenchService {
getExtensions(extensionInfos: IExtensionInfo[], token: CancellationToken): Promise<IExtension[]>;
getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise<IExtension[]>;
getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise<IExtension[]>;
canInstall(extension: IExtension): Promise<boolean>;
canInstall(extension: IExtension): Promise<true | IMarkdownString>;
install(id: string, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise<IExtension>;
install(vsix: URI, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise<IExtension>;
install(extension: IExtension, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation | string): Promise<IExtension>;
Expand Down
Loading

0 comments on commit 7058d51

Please sign in to comment.