diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 30c4f1c51eb0b..9dae82eba0716 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -227,6 +227,8 @@ export interface IGalleryExtension { supportLink?: string; } +export type InstallSource = 'gallery' | 'vsix' | 'resource'; + export interface IGalleryMetadata { id: string; publisherId: string; @@ -245,9 +247,11 @@ export type Metadata = Partial; export interface ILocalExtension extends IExtension { + isWorkspaceScoped: boolean; isMachineScoped: boolean; isApplicationScoped: boolean; publisherId: string | null; @@ -258,6 +262,7 @@ export interface ILocalExtension extends IExtension { preRelease: boolean; updated: boolean; pinned: boolean; + source: InstallSource; } export const enum SortBy { @@ -372,6 +377,7 @@ export interface InstallExtensionEvent { readonly source: URI | IGalleryExtension; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export interface InstallExtensionResult { @@ -383,12 +389,14 @@ export interface InstallExtensionResult { readonly context?: IStringDictionary; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export interface UninstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export interface DidUninstallExtensionEvent { @@ -396,6 +404,7 @@ export interface DidUninstallExtensionEvent { readonly error?: string; readonly profileLocation?: URI; readonly applicationScoped?: boolean; + readonly workspaceScoped?: boolean; } export enum ExtensionManagementErrorCode { @@ -450,6 +459,7 @@ export class ExtensionGalleryError extends Error { export type InstallOptions = { isBuiltin?: boolean; + isWorkspaceScoped?: boolean; isMachineScoped?: boolean; isApplicationScoped?: boolean; pinned?: boolean; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index b5f0317af5548..6c3e289db7d9e 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -264,6 +264,9 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt } uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { + if (extension.isWorkspaceScoped) { + throw new Error('Cannot uninstall a workspace extension'); + } return Promise.resolve(this.channel.call('uninstall', [extension, options])); } diff --git a/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts index 7864019ad8314..98f5a2194f9c2 100644 --- a/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService.ts @@ -7,10 +7,11 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { AbstractExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; +import { AbstractExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IFileService } from 'vs/platform/files/common/files'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class ExtensionsProfileScannerService extends AbstractExtensionsProfileScannerService { constructor( @@ -24,3 +25,5 @@ export class ExtensionsProfileScannerService extends AbstractExtensionsProfileSc super(URI.file(environmentService.extensionsPath), fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); } } + +registerSingleton(IExtensionsProfileScannerService, ExtensionsProfileScannerService, InstantiationType.Delayed); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 3d7c9420e5001..58ce27065ed67 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -173,7 +173,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi if (!local || !local.manifest.name || !local.manifest.version) { throw new Error(`Cannot find a valid extension from the location ${location.toString()}`); } - await this.addExtensionsToProfile([[local, undefined]], profileLocation); + await this.addExtensionsToProfile([[local, { source: 'resource' }]], profileLocation); this.logService.info('Successfully installed extension', local.identifier.id, profileLocation.toString()); return local; } @@ -715,6 +715,8 @@ export class ExtensionsScanner extends Disposable { installedTimestamp: extension.metadata?.installedTimestamp, updated: !!extension.metadata?.updated, pinned: !!extension.metadata?.pinned, + isWorkspaceScoped: false, + source: extension.metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'vsix') }; } @@ -906,7 +908,8 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), preRelease: isBoolean(this.options.preRelease) ? this.options.preRelease - : this.options.installPreReleaseVersion || this.gallery.properties.isPreReleaseVersion || existingExtension?.preRelease + : this.options.installPreReleaseVersion || this.gallery.properties.isPreReleaseVersion || existingExtension?.preRelease, + source: 'gallery', }; if (existingExtension?.manifest.version === this.gallery.version) { @@ -978,6 +981,7 @@ class InstallVSIXTask extends InstallExtensionTask { isBuiltin: this.options.isBuiltin || existing?.isBuiltin, installedTimestamp: Date.now(), pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existing?.pinned), + source: 'vsix', }; if (existing) { diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index 118cbf8d5ec9d..07639a7e7b622 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const enum RecommendationSource { @@ -43,6 +44,6 @@ export interface IExtensionRecommendationNotificationService { hasToIgnoreRecommendationNotifications(): boolean; promptImportantExtensionsInstallNotification(recommendations: IExtensionRecommendations): Promise; - promptWorkspaceRecommendations(recommendations: string[]): Promise; + promptWorkspaceRecommendations(recommendations: Array): Promise; } diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index 775e225433810..28ae3c3e3ed22 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -73,7 +73,7 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { private toExtensionRecommendation(tip: IConfigBasedExtensionTip): ConfigBasedExtensionRecommendation { return { - extensionId: tip.extensionId, + extension: tip.extensionId, reason: { reasonId: ExtensionRecommendationReason.WorkspaceConfig, reasonText: localize('exeBasedRecommendation', "This extension is recommended because of the current workspace configuration") diff --git a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 6096e57e82ad8..9e75f3fb4a961 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -58,7 +58,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { private toExtensionRecommendation(tip: IExecutableBasedExtensionTip): ExtensionRecommendation { return { - extensionId: tip.extensionId.toLowerCase(), + extension: tip.extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Executable, reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", tip.exeFriendlyName) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index b9de3cbbb0c74..3a56ad0de0448 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -44,6 +44,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { buttonForeground, buttonHoverBackground, editorBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ViewContainerLocation } from 'vs/workbench/common/views'; @@ -78,6 +79,7 @@ import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, VerifiedPublisherWidget, onClick } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; +import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; @@ -86,6 +88,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { VIEW_ID as EXPLORER_VIEW_ID } from 'vs/workbench/contrib/files/common/files'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; class NavBar extends Disposable { @@ -155,6 +160,7 @@ interface IExtensionEditorTemplate { builtin: HTMLElement; publisher: HTMLElement; publisherDisplayName: HTMLElement; + resource: HTMLElement; installCount: HTMLElement; rating: HTMLElement; description: HTMLElement; @@ -245,6 +251,10 @@ export class ExtensionEditor extends EditorPane { @ILanguageService private readonly languageService: ILanguageService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IExplorerService private readonly explorerService: IExplorerService, + @IViewsService private readonly viewsService: IViewsService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(ExtensionEditor.ID, group, telemetryService, themeService, storageService); this.extensionReadme = null; @@ -291,6 +301,9 @@ export class ExtensionEditor extends EditorPane { const publisherDisplayName = append(publisher, $('.publisher-name')); const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $('.verified-publisher')), false); + const resource = append(append(subtitle, $('.subtitle-entry.resource')), $('', { tabIndex: 0 })); + resource.setAttribute('role', 'button'); + const installCount = append(append(subtitle, $('.subtitle-entry')), $('span.install', { tabIndex: 0 })); this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), installCount, localize('install count', "Install count"))); const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCount, false); @@ -419,6 +432,7 @@ export class ExtensionEditor extends EditorPane { preview, publisher, publisherDisplayName, + resource, rating, actionsAndStatusContainer, extensionActionBar, @@ -473,6 +487,12 @@ export class ExtensionEditor extends EditorPane { } private async getGalleryVersionToShow(extension: IExtension, preRelease?: boolean): Promise { + if (extension.resourceExtension) { + return null; + } + if (extension.local?.source === 'resource') { + return null; + } if (isUndefined(preRelease)) { return null; } @@ -521,6 +541,25 @@ export class ExtensionEditor extends EditorPane { // subtitle template.publisher.classList.toggle('clickable', !!extension.url); template.publisherDisplayName.textContent = extension.publisherDisplayName; + template.publisher.parentElement?.classList.toggle('hide', !!extension.resourceExtension || extension.local?.source === 'resource'); + + const location = extension.resourceExtension?.location ?? (extension.local?.source === 'resource' ? extension.local?.location : undefined); + template.resource.parentElement?.classList.toggle('hide', !location); + if (location) { + const workspaceFolder = this.contextService.getWorkspaceFolder(location); + if (workspaceFolder && extension.isWorkspaceScoped) { + template.resource.parentElement?.classList.add('clickable'); + this.transientDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); + template.resource.textContent = localize('workspace extension', "Workspace Extension"); + this.transientDisposables.add(onClick(template.resource, () => { + this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); + })); + } else { + template.resource.parentElement?.classList.remove('clickable'); + this.transientDisposables.add(setupCustomHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); + template.resource.textContent = localize('local extension', "Local Extension"); + } + } template.installCount.parentElement?.classList.toggle('hide', !extension.url); template.rating.parentElement?.classList.toggle('hide', !extension.url); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 18274088c5b3b..96c6d79346814 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -10,6 +10,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, isDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isString } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -18,6 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -49,6 +52,8 @@ type RecommendationsNotificationActions = { onDidNeverShowRecommendedExtensionsAgain(extensions: IExtension[]): void; }; +type ExtensionRecommendations = Omit & { extensions: Array }; + class RecommendationsNotification extends Disposable { private _onDidClose = this._register(new Emitter()); @@ -139,6 +144,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); } @@ -179,14 +185,16 @@ export class ExtensionRecommendationNotificationService extends Disposable imple }); } - async promptWorkspaceRecommendations(recommendations: string[]): Promise { + async promptWorkspaceRecommendations(recommendations: Array): Promise { if (this.storageService.getBoolean(donotShowWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false)) { return; } let installed = await this.extensionManagementService.getInstalled(); installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind - recommendations = recommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + recommendations = recommendations.filter(recommendation => installed.every(local => + isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.location) + )); if (!recommendations.length) { return; } @@ -203,7 +211,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple } - private async promptRecommendationsNotification({ extensions: extensionIds, source, name, searchValue }: IExtensionRecommendations, recommendationsNotificationActions: RecommendationsNotificationActions): Promise { + private async promptRecommendationsNotification({ extensions: extensionIds, source, name, searchValue }: ExtensionRecommendations, recommendationsNotificationActions: RecommendationsNotificationActions): Promise { if (this.hasToIgnoreRecommendationNotifications()) { return RecommendationsNotificationResult.Ignored; @@ -224,7 +232,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple this.recommendationSources.push(source); // Ignore exe recommendation if recommendations are already shown - if (source === RecommendationSource.EXE && extensionIds.every(id => this.recommendedExtensions.includes(id))) { + if (source === RecommendationSource.EXE && extensionIds.every(id => isString(id) && this.recommendedExtensions.includes(id))) { return RecommendationsNotificationResult.Ignored; } @@ -233,7 +241,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple return RecommendationsNotificationResult.Ignored; } - this.recommendedExtensions = distinct([...this.recommendedExtensions, ...extensionIds]); + this.recommendedExtensions = distinct([...this.recommendedExtensions, ...extensionIds.filter(isString)]); let extensionsMessage = ''; if (extensions.length === 1) { @@ -414,15 +422,30 @@ export class ExtensionRecommendationNotificationService extends Disposable imple this.visibleNotification = undefined; } - private async getInstallableExtensions(extensionIds: string[]): Promise { + private async getInstallableExtensions(recommendations: Array): Promise { const result: IExtension[] = []; - if (extensionIds.length) { - const extensions = await this.extensionsWorkbenchService.getExtensions(extensionIds.map(id => ({ id })), { source: 'install-recommendations' }, CancellationToken.None); - for (const extension of extensions) { - if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { - result.push(extension); + if (recommendations.length) { + const galleryExtensions: string[] = []; + const resourceExtensions: URI[] = []; + for (const recommendation of recommendations) { + if (typeof recommendation === 'string') { + galleryExtensions.push(recommendation); + } else { + resourceExtensions.push(recommendation); + } + } + 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))) { + result.push(extension); + } } } + if (resourceExtensions.length) { + const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true); + result.push(...extensions); + } } return result; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index fc28afa900367..bc811fe8ccfaf 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts @@ -4,13 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { IExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; -export type ExtensionRecommendation = { - readonly extensionId: string; +export type GalleryExtensionRecommendation = { + readonly extension: string; readonly reason: IExtensionRecommendationReason; }; +export type ResourceExtensionRecommendation = { + readonly extension: URI; + readonly reason: IExtensionRecommendationReason; +}; + +export type ExtensionRecommendation = GalleryExtensionRecommendation | ResourceExtensionRecommendation; + export abstract class ExtensionRecommendations extends Disposable { readonly abstract recommendations: ReadonlyArray; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index a6806b20159a1..b266d936a122b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -8,7 +8,7 @@ import { IExtensionManagementService, IExtensionGalleryService, InstallOperation import { IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { distinct, shuffle } from 'vs/base/common/arrays'; +import { shuffle } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { LifecyclePhase, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -28,6 +28,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { RemoteRecommendations } from 'vs/workbench/contrib/extensions/browser/remoteRecommendations'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; +import { isString } from 'vs/base/common/types'; type IgnoreRecommendationClassification = { owner: 'sandy081'; @@ -150,9 +151,9 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.webRecommendations.recommendations, ]; - for (const { extensionId, reason } of allRecommendations) { - if (this.isExtensionAllowedToBeRecommended(extensionId)) { - output[extensionId.toLowerCase()] = reason; + for (const { extension, reason } of allRecommendations) { + if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension)) { + output[extension.toLowerCase()] = reason; } } @@ -162,8 +163,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte async getConfigBasedRecommendations(): Promise<{ important: string[]; others: string[] }> { await this.configBasedRecommendations.activate(); return { - important: this.toExtensionRecommendations(this.configBasedRecommendations.importantRecommendations), - others: this.toExtensionRecommendations(this.configBasedRecommendations.otherRecommendations) + important: this.toExtensionIds(this.configBasedRecommendations.importantRecommendations), + others: this.toExtensionIds(this.configBasedRecommendations.otherRecommendations) }; } @@ -177,11 +178,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.webRecommendations.recommendations ]; - const extensionIds = distinct(recommendations.map(e => e.extensionId)) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); - + const extensionIds = this.toExtensionIds(recommendations); shuffle(extensionIds, this.sessionSeed); - return extensionIds; } @@ -194,43 +192,50 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.exeBasedRecommendations.importantRecommendations, ]; - const extensionIds = distinct(recommendations.map(e => e.extensionId)) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); - + const extensionIds = this.toExtensionIds(recommendations); shuffle(extensionIds, this.sessionSeed); - return extensionIds; } getKeymapRecommendations(): string[] { - return this.toExtensionRecommendations(this.keymapRecommendations.recommendations); + return this.toExtensionIds(this.keymapRecommendations.recommendations); } getLanguageRecommendations(): string[] { - return this.toExtensionRecommendations(this.languageRecommendations.recommendations); + return this.toExtensionIds(this.languageRecommendations.recommendations); } getRemoteRecommendations(): string[] { - return this.toExtensionRecommendations(this.remoteRecommendations.recommendations); + return this.toExtensionIds(this.remoteRecommendations.recommendations); } - async getWorkspaceRecommendations(): Promise { + async getWorkspaceRecommendations(): Promise> { if (!this.isEnabled()) { return []; } await this.workspaceRecommendations.activate(); - return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations); + const result: Array = []; + for (const { extension } of this.workspaceRecommendations.recommendations) { + if (isString(extension)) { + if (!result.includes(extension.toLowerCase()) && this.isExtensionAllowedToBeRecommended(extension)) { + result.push(extension.toLowerCase()); + } + } else { + result.push(extension); + } + } + return result; } async getExeBasedRecommendations(exe?: string): Promise<{ important: string[]; others: string[] }> { await this.exeBasedRecommendations.activate(); const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe) : { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations }; - return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) }; + return { important: this.toExtensionIds(important), others: this.toExtensionIds(others) }; } getFileBasedRecommendations(): string[] { - return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations); + return this.toExtensionIds(this.fileBasedRecommendations.recommendations); } private onDidInstallExtensions(results: readonly InstallExtensionResult[]): void { @@ -254,10 +259,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte } } - private toExtensionRecommendations(recommendations: ReadonlyArray): string[] { - const extensionIds = distinct(recommendations.map(e => e.extensionId)) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); - + private toExtensionIds(recommendations: ReadonlyArray): string[] { + const extensionIds: string[] = []; + for (const { extension } of recommendations) { + if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension) && !extensionIds.includes(extension.toLowerCase())) { + extensionIds.push(extension.toLowerCase()); + } + } return extensionIds; } @@ -272,8 +280,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte ...this.configBasedRecommendations.importantRecommendations.filter( recommendation => !recommendation.whenNotInstalled || recommendation.whenNotInstalled.every(id => installed.every(local => !areSameExtensions(local.identifier, { id })))) ] - .map(({ extensionId }) => extensionId) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); + .map(({ extension }) => extension) + .filter(extension => !isString(extension) || this.isExtensionAllowedToBeRecommended(extension)); if (allowedRecommendations.length) { await this._registerP(timeout(5000)); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 6d09f44869ae4..eac6f1ae04980 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -1535,7 +1535,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate(), ContextKeyExpr.has('isBuiltinExtension').negate()), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate(), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), order: 3 }, run: async (accessor: ServicesAccessor, id: string) => { @@ -1552,7 +1552,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT), + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), order: 4 }, run: async (accessor: ServicesAccessor, id: string) => { @@ -1593,7 +1593,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate(), ContextKeyExpr.notEquals('extensionSource', 'resource')), order: 2 }, run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkspaceExtensionsConfigService).toggleRecommendation(id) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 9768c107db0ff..aa1601b8649d4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -322,6 +322,7 @@ export class InstallAction extends ExtensionAction { @IDialogService private readonly dialogService: IDialogService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { super('extensions.install', localize('install', "Install"), InstallAction.Class, false); this.options = { ...options, isMachineScoped: false }; @@ -502,6 +503,9 @@ export class InstallAction extends ExtensionAction { } getLabel(primary?: boolean): string { + if (this.extension?.isWorkspaceScoped && this.extension.resourceExtension && this.contextService.isInsideWorkspace(this.extension.resourceExtension.location)) { + return localize('install workspace version', "Install Workspace Extension"); + } /* install pre-release version */ if (this.options.installPreReleaseVersion && this.extension?.hasPreReleaseVersion) { return primary ? localize('install pre-release', "Install Pre-Release") : localize('install pre-release version', "Install Pre-Release Version"); @@ -1080,6 +1084,10 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isBuiltinExtension', extension.isBuiltin]); cksOverlay.push(['isDefaultApplicationScopedExtension', extension.local && isApplicationScopedExtension(extension.local.manifest)]); cksOverlay.push(['isApplicationScopedExtension', extension.local && extension.local.isApplicationScoped]); + cksOverlay.push(['isWorkspaceScopedExtension', extension.isWorkspaceScoped]); + if (extension.local) { + cksOverlay.push(['extensionSource', extension.local.source]); + } cksOverlay.push(['extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration]); cksOverlay.push(['extensionHasKeybindings', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.keybindings]); cksOverlay.push(['extensionHasCommands', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes?.commands]); @@ -1414,7 +1422,7 @@ export class EnableForWorkspaceAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped) { this.enabled = this.extension.state === ExtensionState.Installed && !this.extensionEnablementService.isEnabled(this.extension.local) && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); @@ -1445,7 +1453,7 @@ export class EnableGloballyAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped) { this.enabled = this.extension.state === ExtensionState.Installed && this.extensionEnablementService.isDisabledGlobally(this.extension.local) && this.extensionEnablementService.canChangeEnablement(this.extension.local); @@ -1479,7 +1487,7 @@ export class DisableForWorkspaceAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); @@ -1512,7 +1520,7 @@ export class DisableGloballyAction extends ExtensionAction { update(): void { this.enabled = false; - if (this.extension && this.extension.local && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))) { + if (this.extension && this.extension.local && !this.extension.isWorkspaceScoped && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) && this.extensionEnablementService.canChangeEnablement(this.extension.local); @@ -2536,7 +2544,7 @@ export class ExtensionStatusAction extends ExtensionAction { const isEnabled = this.workbenchExtensionEnablementService.isEnabled(this.extension.local); const isRunning = this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); - if (isEnabled && isRunning) { + if (!this.extension.isWorkspaceScoped && isEnabled && isRunning) { if (this.extension.enablementState === EnablementState.EnabledWorkspace) { this.updateStatus({ message: new MarkdownString(localize('workspace enabled', "This extension is enabled for this workspace by the user.")) }, true); return; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 2595d6010cc5a..2466a15d0e9a1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -223,7 +223,7 @@ export class Renderer implements IPagedRenderer { data.description.textContent = extension.description; const updatePublisher = () => { - data.publisherDisplayName.textContent = extension.publisherDisplayName; + data.publisherDisplayName.textContent = !extension.resourceExtension && extension.local?.source !== 'resource' ? extension.publisherDisplayName : ''; }; updatePublisher(); Event.filter(this.extensionsWorkbenchService.onChange, e => !!e && areSameExtensions(e.identifier, extension.identifier))(() => updatePublisher(), this, data.extensionDisposables); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 8e64f3bacaa5b..09f88bf6bf818 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { isCancellationError, getErrorMessage } from 'vs/base/common/errors'; import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions, getExtensionDependencies } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -57,6 +57,9 @@ import { isOfflineError } from 'vs/base/parts/request/common/request'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IExtensionFeatureRenderer, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { URI } from 'vs/base/common/uri'; +import { isString } from 'vs/base/common/types'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; export const NONE_CATEGORY = 'none'; @@ -148,6 +151,7 @@ export class ExtensionsListView extends ViewPane { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, + @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService ) { super({ @@ -878,20 +882,35 @@ export class ExtensionsListView extends ViewPane { return new PagedModel([]); } - protected async getInstallableRecommendations(recommendations: string[], options: IQueryOptions, token: CancellationToken): Promise { + protected async getInstallableRecommendations(recommendations: Array, options: IQueryOptions, token: CancellationToken): Promise { const result: IExtension[] = []; if (recommendations.length) { - const extensions = await this.extensionsWorkbenchService.getExtensions(recommendations.map(id => ({ id })), { source: options.source }, token); - for (const extension of extensions) { - if (extension.gallery && !extension.deprecationInfo && (await this.extensionManagementService.canInstall(extension.gallery))) { - result.push(extension); + const galleryExtensions: string[] = []; + const resourceExtensions: URI[] = []; + for (const recommendation of recommendations) { + if (typeof recommendation === 'string') { + galleryExtensions.push(recommendation); + } else { + resourceExtensions.push(recommendation); + } + } + if (galleryExtensions.length) { + 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))) { + result.push(extension); + } } } + if (resourceExtensions.length) { + const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true); + result.push(...extensions); + } } return result; } - protected async getWorkspaceRecommendations(): Promise { + protected async getWorkspaceRecommendations(): Promise> { const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations(); const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations(); for (const configBasedRecommendation of important) { @@ -905,8 +924,7 @@ export class ExtensionsListView extends ViewPane { private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const recommendations = await this.getWorkspaceRecommendations(); const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)); - const result: IExtension[] = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(result); + return new PagedModel(installableRecommendations); } private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { @@ -951,7 +969,7 @@ export class ExtensionsListView extends ViewPane { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)) .map(e => e.identifier.id.toLowerCase()); const workspaceRecommendations = (await this.getWorkspaceRecommendations()) - .map(extensionId => extensionId.toLowerCase()); + .map(extensionId => isString(extensionId) ? extensionId.toLowerCase() : extensionId); return distinct( flatten(await Promise.all([ @@ -974,12 +992,10 @@ export class ExtensionsListView extends ViewPane { this.extensionRecommendationsService.getImportantRecommendations(), this.extensionRecommendationsService.getFileBasedRecommendations(), this.extensionRecommendationsService.getOtherRecommendations() - ])).filter(extensionId => !local.includes(extensionId.toLowerCase()) - ), extensionId => extensionId.toLowerCase()); + ])).filter(extensionId => !isString(extensionId) || !local.includes(extensionId.toLowerCase()))); const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token); - const result: IExtension[] = coalesce(allRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(result.slice(0, 8)); + return new PagedModel(installableRecommendations.slice(0, 8)); } private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { @@ -987,8 +1003,7 @@ export class ExtensionsListView extends ViewPane { const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]); const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token)) .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); - const result = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(this.sortExtensions(result, options)); + return new PagedModel(this.sortExtensions(installableRecommendations, options)); } private setModel(model: IPagedModel, error?: any, donotResetScrollTop?: boolean) { @@ -1326,12 +1341,14 @@ export class StaticQueryExtensionsView extends ExtensionsListView { @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, + @IUriIdentityService uriIdentityService: IUriIdentityService, @ILogService logService: ILogService ) { super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, extensionRecommendationsService, telemetryService, configurationService, contextService, extensionManagementServerService, extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService, - preferencesService, storageService, workspaceTrustManagementService, extensionEnablementService, layoutService, extensionFeaturesManagementService, logService); + preferencesService, storageService, workspaceTrustManagementService, extensionEnablementService, layoutService, extensionFeaturesManagementService, + uriIdentityService, logService); } override show(): Promise> { @@ -1464,18 +1481,30 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView imple return model; } - private async getInstallableWorkspaceRecommendations() { + private async getInstallableWorkspaceRecommendations(): Promise { const installed = (await this.extensionsWorkbenchService.queryLocal()) .filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind const recommendations = (await this.getWorkspaceRecommendations()) - .filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + .filter(recommendation => installed.every(local => isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.local?.location))); return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None); } async installWorkspaceRecommendations(): Promise { const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); if (installableRecommendations.length) { - await this.extensionManagementService.installGalleryExtensions(installableRecommendations.map(i => ({ extension: i.gallery!, options: {} }))); + const galleryExtensions: InstallExtensionInfo[] = []; + const resourceExtensions: IExtension[] = []; + for (const recommendation of installableRecommendations) { + if (recommendation.gallery) { + galleryExtensions.push({ extension: recommendation.gallery, options: {} }); + } else { + resourceExtensions.push(recommendation); + } + } + await Promise.all([ + this.extensionManagementService.installGalleryExtensions(galleryExtensions), + ...resourceExtensions.map(extension => this.extensionsWorkbenchService.install(extension)) + ]); } else { this.notificationService.notify({ severity: Severity.Info, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 01bd5f4b89b63..50c5e3559722a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -42,6 +42,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -209,6 +210,14 @@ export class VerifiedPublisherWidget extends ExtensionWidget { return; } + if (this.extension.resourceExtension) { + return; + } + + if (this.extension.local?.source === 'resource') { + return; + } + const publisherDomainLink = URI.parse(this.extension.publisherDomain.link); const verifiedPublisher = append(this.container, $('span.extension-verified-publisher.clickable')); append(verifiedPublisher, renderIcon(verifiedPublisherIcon)); @@ -529,6 +538,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService, @IThemeService private readonly themeService: IThemeService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { super(); } @@ -595,6 +605,16 @@ export class ExtensionHoverWidget extends ExtensionWidget { } } + const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined); + if (location) { + if (this.extension.isWorkspaceScoped && this.contextService.isInsideWorkspace(location)) { + markdown.appendMarkdown(localize('workspace extension', "Workspace Extension")); + } else { + markdown.appendMarkdown(localize('local extension', "Local Extension")); + } + markdown.appendText(`\n`); + } + if (this.extension.description) { markdown.appendMarkdown(`${this.extension.description}`); markdown.appendText(`\n`); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 5846f2bb915d8..e305774c3634f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -18,7 +18,7 @@ import { IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; 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'; @@ -55,6 +55,7 @@ import { mainWindow } from 'vs/base/browser/window'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; interface IExtensionStateProvider { (extension: Extension): T; @@ -74,6 +75,7 @@ type ExtensionsLoadClassification = { export class Extension implements IExtension { public enablementState: EnablementState = EnablementState.EnabledGlobally; + public readonly resourceExtension: IResourceExtension | undefined; constructor( private stateProvider: IExtensionStateProvider, @@ -81,12 +83,15 @@ export class Extension implements IExtension { public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, + private readonly resourceExtensionInfo: { resourceExtension: IResourceExtension; isWorkspaceScoped: boolean } | undefined, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService - ) { } + ) { + this.resourceExtension = resourceExtensionInfo?.resourceExtension; + } get type(): ExtensionType { return this.local ? this.local.type : ExtensionType.User; @@ -96,8 +101,21 @@ export class Extension implements IExtension { return this.local ? this.local.isBuiltin : false; } + get isWorkspaceScoped(): boolean { + if (this.local) { + return this.local.isWorkspaceScoped; + } + if (this.resourceExtensionInfo) { + return this.resourceExtensionInfo.isWorkspaceScoped; + } + return false; + } + get name(): string { - return this.gallery ? this.gallery.name : this.local!.manifest.name; + if (this.gallery) { + return this.gallery.name; + } + return this.getManifestFromLocalOrResource()?.name ?? ''; } get displayName(): string { @@ -105,22 +123,28 @@ export class Extension implements IExtension { return this.gallery.displayName || this.gallery.name; } - return this.local!.manifest.displayName || this.local!.manifest.name; + return this.getManifestFromLocalOrResource()?.displayName ?? this.name; } get identifier(): IExtensionIdentifier { if (this.gallery) { return this.gallery.identifier; } + if (this.resourceExtension) { + return this.resourceExtension.identifier; + } return this.local!.identifier; } get uuid(): string | undefined { - return this.gallery ? this.gallery.identifier.uuid : this.local!.identifier.uuid; + return this.gallery ? this.gallery.identifier.uuid : this.local?.identifier.uuid; } get publisher(): string { - return this.gallery ? this.gallery.publisher : this.local!.manifest.publisher; + if (this.gallery) { + return this.gallery.publisher; + } + return this.getManifestFromLocalOrResource()?.publisher ?? ''; } get publisherDisplayName(): string { @@ -132,7 +156,7 @@ export class Extension implements IExtension { return this.local.publisherDisplayName; } - return this.local!.manifest.publisher; + return this.publisher; } get publisherUrl(): URI | undefined { @@ -160,11 +184,11 @@ export class Extension implements IExtension { } get latestVersion(): string { - return this.gallery ? this.gallery.version : this.local!.manifest.version; + return this.gallery ? this.gallery.version : this.getManifestFromLocalOrResource()?.version ?? ''; } get description(): string { - return this.gallery ? this.gallery.description : this.local!.manifest.description || ''; + return this.gallery ? this.gallery.description : this.getManifestFromLocalOrResource()?.description ?? ''; } get url(): string | undefined { @@ -176,11 +200,11 @@ export class Extension implements IExtension { } get iconUrl(): string { - return this.galleryIconUrl || this.localIconUrl || this.defaultIconUrl; + return this.galleryIconUrl || this.resourceExtensionIconUrl || this.localIconUrl || this.defaultIconUrl; } get iconUrlFallback(): string { - return this.galleryIconUrlFallback || this.localIconUrl || this.defaultIconUrl; + return this.galleryIconUrlFallback || this.resourceExtensionIconUrl || this.localIconUrl || this.defaultIconUrl; } private get localIconUrl(): string | null { @@ -190,6 +214,13 @@ export class Extension implements IExtension { return null; } + private get resourceExtensionIconUrl(): string | null { + if (this.resourceExtension?.manifest.icon) { + return FileAccess.uriToBrowserUri(resources.joinPath(this.resourceExtension.location, this.resourceExtension.manifest.icon)).toString(true); + } + return null; + } + private get galleryIconUrl(): string | null { return this.gallery?.assets.icon ? this.gallery.assets.icon.uri : null; } @@ -284,8 +315,10 @@ export class Extension implements IExtension { if (gallery) { return getGalleryExtensionTelemetryData(gallery); + } else if (local) { + return getLocalExtensionTelemetryData(local); } else { - return getLocalExtensionTelemetryData(local!); + return {}; } } @@ -309,7 +342,7 @@ export class Extension implements IExtension { } get hasReleaseVersion(): boolean { - return !!this.gallery?.hasReleaseVersion; + return !!this.resourceExtension || !!this.gallery?.hasReleaseVersion; } private getLocal(): ILocalExtension | undefined { @@ -330,6 +363,10 @@ export class Extension implements IExtension { return null; } + if (this.resourceExtension) { + return this.resourceExtension.manifest; + } + return null; } @@ -342,6 +379,10 @@ export class Extension implements IExtension { return true; } + if (this.resourceExtension?.readmeUri) { + return true; + } + return this.type === ExtensionType.System; } @@ -367,6 +408,11 @@ ${this.description} `); } + if (this.resourceExtension?.readmeUri) { + const content = await this.fileService.readFile(this.resourceExtension?.readmeUri); + return content.value.toString(); + } + return Promise.reject(new Error('not available')); } @@ -401,13 +447,16 @@ ${this.description} } get categories(): readonly string[] { - const { local, gallery } = this; + const { local, gallery, resourceExtension } = this; if (local && local.manifest.categories && !this.outdated) { return local.manifest.categories; } if (gallery) { return gallery.categories; } + if (resourceExtension) { + return resourceExtension.manifest.categories ?? []; + } return []; } @@ -420,26 +469,42 @@ ${this.description} } get dependencies(): string[] { - const { local, gallery } = this; + const { local, gallery, resourceExtension } = this; if (local && local.manifest.extensionDependencies && !this.outdated) { return local.manifest.extensionDependencies; } if (gallery) { return gallery.properties.dependencies || []; } + if (resourceExtension) { + return resourceExtension.manifest.extensionDependencies || []; + } return []; } get extensionPack(): string[] { - const { local, gallery } = this; + const { local, gallery, resourceExtension } = this; if (local && local.manifest.extensionPack && !this.outdated) { return local.manifest.extensionPack; } if (gallery) { return gallery.properties.extensionPack || []; } + if (resourceExtension) { + return resourceExtension.manifest.extensionPack || []; + } return []; } + + private getManifestFromLocalOrResource(): IExtensionManifest | null { + if (this.local) { + return this.local.manifest; + } + if (this.resourceExtension) { + return this.resourceExtension.manifest; + } + return null; + } } const EXTENSIONS_AUTO_UPDATE_KEY = 'extensions.autoUpdate'; @@ -465,8 +530,10 @@ class Extensions extends Disposable { readonly server: IExtensionManagementServer, private readonly stateProvider: IExtensionStateProvider, private readonly runtimeStateProvider: IExtensionStateProvider, + private readonly isWorkspaceServer: boolean, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -479,6 +546,29 @@ class Extensions extends Disposable { this._register(server.extensionManagementService.onDidChangeProfile(() => this.reset())); this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e))); this._register(Event.any(this.onChange, this.onReset)(() => this._local = undefined)); + if (this.isWorkspaceServer) { + this._register(this.workbenchExtensionManagementService.onInstallExtension(e => { + if (e.workspaceScoped) { + this.onInstallExtension(e); + } + })); + this._register(this.workbenchExtensionManagementService.onDidInstallExtensions(e => { + const result = e.filter(e => e.workspaceScoped); + if (result.length) { + this.onDidInstallExtensions(result); + } + })); + this._register(this.workbenchExtensionManagementService.onUninstallExtension(e => { + if (e.workspaceScoped) { + this.onUninstallExtension(e.identifier); + } + })); + this._register(this.workbenchExtensionManagementService.onDidUninstallExtension(e => { + if (e.workspaceScoped) { + this.onDidUninstallExtension(e); + } + })); + } } private _local: IExtension[] | undefined; @@ -553,9 +643,11 @@ class Extensions extends Disposable { continue; } } - const gallery = byID.get(installed.identifier.id.toLowerCase()); - if (gallery) { - mappedExtensions.push([installed, gallery]); + if (installed.local?.source !== 'resource') { + const gallery = byID.get(installed.identifier.id.toLowerCase()); + if (gallery) { + mappedExtensions.push([installed, gallery]); + } } } return mappedExtensions; @@ -583,7 +675,7 @@ class Extensions extends Disposable { const { source } = event; if (source && !URI.isUri(source)) { const extension = this.installed.find(e => areSameExtensions(e.identifier, source.identifier)) - ?? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, undefined, source); + ?? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, undefined, source, undefined); this.installing.push(extension); this._onChange.fire({ extension }); } @@ -592,6 +684,9 @@ class Extensions extends Disposable { private async fetchInstalledExtensions(productVersion?: IProductVersion): Promise { const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); const all = await this.server.extensionManagementService.getInstalled(undefined, undefined, productVersion); + if (this.isWorkspaceServer) { + all.push(...await this.workbenchExtensionManagementService.getInstalledWorkspaceExtensions()); + } // dedup user and system extensions by giving priority to user extensions. const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => { @@ -603,7 +698,7 @@ class Extensions extends Disposable { const byId = index(this.installed, e => e.local ? e.local.identifier.id : e.identifier.id); this.installed = installed.map(local => { - const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined); + const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined); extension.local = local; extension.enablementState = this.extensionEnablementService.getEnablementState(local); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); @@ -628,7 +723,7 @@ class Extensions extends Disposable { this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; let extension: Extension | undefined = installingExtension ? installingExtension - : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined) + : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined) : undefined; if (extension) { if (local) { @@ -647,7 +742,7 @@ class Extensions extends Disposable { } } this._onChange.fire(!local || !extension ? undefined : { extension, operation: event.operation }); - if (extension && extension.local && !extension.gallery) { + if (extension && extension.local && !extension.gallery && extension.local.source !== 'resource') { await this.syncInstalledExtensionWithGallery(extension); } } @@ -783,6 +878,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IDialogService private readonly dialogService: IDialogService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IUpdateService private readonly updateService: IUpdateService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -791,19 +887,34 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { - this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); + this.localExtensions = this._register(instantiationService.createInstance(Extensions, + extensionManagementServerService.localExtensionManagementServer, + ext => this.getExtensionState(ext), + ext => this.getRuntimeState(ext), + !extensionManagementServerService.remoteExtensionManagementServer + )); this._register(this.localExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.localExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.localExtensions); } if (extensionManagementServerService.remoteExtensionManagementServer) { - this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); + this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, + extensionManagementServerService.remoteExtensionManagementServer, + ext => this.getExtensionState(ext), + ext => this.getRuntimeState(ext), + true + )); this._register(this.remoteExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.remoteExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.remoteExtensions); } if (extensionManagementServerService.webExtensionManagementServer) { - this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); + this.webExtensions = this._register(instantiationService.createInstance(Extensions, + extensionManagementServerService.webExtensionManagementServer, + ext => this.getExtensionState(ext), + ext => this.getRuntimeState(ext), + !(extensionManagementServerService.remoteExtensionManagementServer || extensionManagementServerService.localExtensionManagementServer) + )); this._register(this.webExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.webExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.webExtensions); @@ -1043,6 +1154,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return galleryExtensions.map(gallery => this.fromGallery(gallery, extensionsControlManifest)); } + async getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise { + const result: IExtension[] = []; + await Promise.all(locations.map(async location => { + const resourceExtension = await this.extensionManagementService.getExtension(location); + if (!resourceExtension) { + return; + } + const extension = this.getInstalledExtensionMatchingLocation(resourceExtension.location) + ?? this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, undefined, { resourceExtension, isWorkspaceScoped }); + result.push(extension); + })); + return result; + } + private resolveQueryText(text: string): string { text = text.replace(/@web/g, `tag:"${WEB_EXTENSION_TAG}"`); @@ -1069,7 +1194,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension { let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); } return extension; @@ -1081,7 +1206,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (installed.identifier.uuid === gallery.identifier.uuid) { return installed; } - } else { + } else if (installed.local?.source !== 'resource') { if (areSameExtensions(installed.identifier, gallery.identifier)) { // Installed from other sources return installed; } @@ -1090,6 +1215,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return null; } + private getInstalledExtensionMatchingLocation(location: URI): IExtension | null { + return this.local.find(e => e.local && this.uriIdentityService.extUri.isEqualOrParent(location, e.local?.location)) ?? null; + } + async open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise { if (typeof extension === 'string') { const id = extension; @@ -1415,6 +1544,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Skip checking updates for a builtin extension if it is a system extension or if it does not has Marketplace identifier continue; } + if (installed.local?.source === 'resource') { + continue; + } infos.push({ ...installed.identifier, preRelease: !!installed.local?.preRelease }); } if (infos.length) { @@ -1722,19 +1854,22 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } - if (!extension.gallery) { - return false; - } + if (extension.gallery) { + if (this.localExtensions && await this.localExtensions.canInstall(extension.gallery)) { + return true; + } - if (this.localExtensions && await this.localExtensions.canInstall(extension.gallery)) { - return true; - } + if (this.remoteExtensions && await this.remoteExtensions.canInstall(extension.gallery)) { + return true; + } - if (this.remoteExtensions && await this.remoteExtensions.canInstall(extension.gallery)) { - return true; + if (this.webExtensions && await this.webExtensions.canInstall(extension.gallery)) { + return true; + } + return false; } - if (this.webExtensions && await this.webExtensions.canInstall(extension.gallery)) { + if (extension.resourceExtension) { return true; } @@ -1742,7 +1877,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } async install(arg: string | URI | IExtension, installOptions: InstallExtensionOptions = {}, progressLocation?: ProgressLocation): Promise { - let installable: URI | IGalleryExtension | undefined; + let installable: URI | IGalleryExtension | IResourceExtension | undefined; let extension: IExtension | undefined; if (arg instanceof URI) { @@ -1755,19 +1890,22 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extension?.isBuiltin) { installableInfo = { id: arg, version: installOptions.version, preRelease: installOptions.installPreReleaseVersion ?? this.preferPreReleases }; } - } else { + } else if (arg.gallery) { extension = arg; gallery = arg.gallery; if (installOptions.version && installOptions.version !== gallery?.version) { installableInfo = { id: extension.identifier.id, version: installOptions.version }; } + } else if (arg.resourceExtension) { + extension = arg; + installable = arg.resourceExtension; } if (installableInfo) { const targetPlatform = extension?.server ? await extension.server.extensionManagementService.getTargetPlatform() : undefined; gallery = firstOrDefault(await this.galleryService.getExtensions([installableInfo], { targetPlatform }, CancellationToken.None)); } if (!extension && gallery) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); Extensions.updateExtensionFromControlManifest(extension as Extension, await this.extensionManagementService.getExtensionsControlManifest()); } if (extension?.isMalicious) { @@ -1775,18 +1913,23 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } // Do not install if requested to enable and extension is already installed if (!(installOptions.enable && extension?.local)) { - if (!gallery) { - const id = isString(arg) ? arg : (arg).identifier.id; - if (installOptions.version) { - throw new Error(nls.localize('not found version', "Unable to install extension '{0}' because the requested version '{1}' is not found.", id, installOptions.version)); - } else { - throw new Error(nls.localize('not found', "Unable to install extension '{0}' because it is not found.", id)); + if (!installable) { + if (!gallery) { + const id = isString(arg) ? arg : (arg).identifier.id; + if (installOptions.version) { + throw new Error(nls.localize('not found version', "Unable to install extension '{0}' because the requested version '{1}' is not found.", id, installOptions.version)); + } else { + throw new Error(nls.localize('not found', "Unable to install extension '{0}' because it is not found.", id)); + } } + installable = gallery; } - installable = gallery; if (installOptions.version) { installOptions.installGivenVersion = true; } + if (extension?.isWorkspaceScoped) { + installOptions.isWorkspaceScoped = true; + } } } @@ -1819,7 +1962,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (installable instanceof URI) { extension = await this.doInstall(undefined, () => this.installFromVSIX(installable, installOptions), progressLocation); } else if (extension) { - extension = await this.doInstall(extension, () => this.installFromGallery(extension!, installable, installOptions), progressLocation); + if (extension.resourceExtension) { + extension = await this.doInstall(extension, () => this.extensionManagementService.installResourceExtension(installable as IResourceExtension, installOptions), progressLocation); + } else { + extension = await this.doInstall(extension, () => this.installFromGallery(extension!, installable as IGalleryExtension, installOptions), progressLocation); + } } } diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index c043223dc06b6..74ce489d01d24 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { ExtensionRecommendations, GalleryExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -40,8 +40,8 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private readonly fileBasedRecommendations = new Map(); private readonly fileBasedImportantRecommendations = new Set(); - get recommendations(): ReadonlyArray { - const recommendations: ExtensionRecommendation[] = []; + get recommendations(): ReadonlyArray { + const recommendations: GalleryExtensionRecommendation[] = []; [...this.fileBasedRecommendations.keys()] .sort((a, b) => { if (this.fileBasedRecommendations.get(a)!.recommendedTime === this.fileBasedRecommendations.get(b)!.recommendedTime) { @@ -56,7 +56,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { }) .forEach(extensionId => { recommendations.push({ - extensionId, + extension: extensionId, reason: { reasonId: ExtensionRecommendationReason.File, reasonText: localize('fileBasedRecommendation', "This extension is recommended based on the files you recently opened.") @@ -66,12 +66,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return recommendations; } - get importantRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => this.fileBasedImportantRecommendations.has(e.extensionId)); + get importantRecommendations(): ReadonlyArray { + return this.recommendations.filter(e => this.fileBasedImportantRecommendations.has(e.extension)); } - get otherRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => !this.fileBasedImportantRecommendations.has(e.extensionId)); + get otherRecommendations(): ReadonlyArray { + return this.recommendations.filter(e => !this.fileBasedImportantRecommendations.has(e.extension)); } constructor( diff --git a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index a40eed0f23fdf..3ad131168e558 100644 --- a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts @@ -21,7 +21,7 @@ export class KeymapRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.keymapExtensionTips) { this._recommendations = this.productService.keymapExtensionTips.map(extensionId => ({ - extensionId: extensionId.toLowerCase(), + extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: '' diff --git a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts index 9258305b84aa2..ac493c927fea7 100644 --- a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts @@ -21,7 +21,7 @@ export class LanguageRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.languageExtensionTips) { this._recommendations = this.productService.languageExtensionTips.map(extensionId => ({ - extensionId: extensionId.toLowerCase(), + extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: '' diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 44f8b720581e7..59a07a33c6f78 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -163,7 +163,7 @@ margin-left: 6px; } -.extension-editor > .header > .details > .subtitle > div:not(:first-child):not(:empty) { +.extension-editor > .header > .details > .subtitle > div:not(:first-child):not(:empty):not(.resource) { border-left: 1px solid rgba(128, 128, 128, 0.7); margin-left: 14px; padding-left: 14px; diff --git a/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts index 43d9c8a2dd8a1..f3ccbdd5a8b18 100644 --- a/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/remoteRecommendations.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { ExtensionRecommendations, GalleryExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { PlatformToString, platform } from 'vs/base/common/platform'; export class RemoteRecommendations extends ExtensionRecommendations { - private _recommendations: ExtensionRecommendation[] = []; - get recommendations(): ReadonlyArray { return this._recommendations; } + private _recommendations: GalleryExtensionRecommendation[] = []; + get recommendations(): ReadonlyArray { return this._recommendations; } constructor( @IProductService private readonly productService: IProductService, @@ -23,7 +23,7 @@ export class RemoteRecommendations extends ExtensionRecommendations { const extensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips }; const currentPlatform = PlatformToString(platform); this._recommendations = Object.values(extensionTips).filter(({ supportedPlatforms }) => !supportedPlatforms || supportedPlatforms.includes(currentPlatform)).map(extension => ({ - extensionId: extension.extensionId.toLowerCase(), + extension: extension.extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: '' diff --git a/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts index 8688c14b82405..bb72b0236d1b3 100644 --- a/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/webRecommendations.ts @@ -25,7 +25,7 @@ export class WebRecommendations extends ExtensionRecommendations { const isOnlyWeb = this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer; if (isOnlyWeb && Array.isArray(this.productService.webExtensionTips)) { this._recommendations = this.productService.webExtensionTips.map(extensionId => ({ - extensionId: extensionId.toLowerCase(), + extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: localize('reason', "This extension is recommended for {0} for the Web", this.productService.nameLong) diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index e2e34e8f8fc3e..913ced02d9dec 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -11,6 +11,9 @@ import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRe import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { IExtensionsConfigContent, IWorkspaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IFileService } from 'vs/platform/files/common/files'; export class WorkspaceRecommendations extends ExtensionRecommendations { @@ -25,6 +28,9 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { constructor( @IWorkspaceExtensionsConfigService private readonly workspaceExtensionsConfigService: IWorkspaceExtensionsConfigService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileService private readonly fileService: IFileService, @INotificationService private readonly notificationService: INotificationService, ) { super(); @@ -62,7 +68,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { for (const extensionId of extensionsConfig.recommendations) { if (invalidRecommendations.indexOf(extensionId) === -1) { this._recommendations.push({ - extensionId, + extension: extensionId, reason: { reasonId: ExtensionRecommendationReason.Workspace, reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.") @@ -72,6 +78,27 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } } + + for (const workspaceFolder of this.contextService.getWorkspace().folders) { + const extensionsLocaiton = this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, '.vscode/extensions'); + try { + const stat = await this.fileService.resolve(extensionsLocaiton); + for (const extension of stat.children ?? []) { + if (!extension.isDirectory) { + continue; + } + this._recommendations.push({ + extension: extension.resource, + reason: { + reasonId: ExtensionRecommendationReason.Workspace, + reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.") + } + }); + } + } catch (error) { + // ignore + } + } } private async validateExtensions(contents: IExtensionsConfigContent[]): Promise<{ validRecommendations: string[]; invalidRecommendations: string[]; message: string }> { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 8bae2eeae25d6..737e37568bc6b 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -7,7 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { IPager } from 'vs/base/common/paging'; import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { EnablementState, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { EnablementState, IExtensionManagementServer, IResourceExtension } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -52,6 +52,7 @@ export type ExtensionRuntimeState = { action: ExtensionRuntimeActionType; reason export interface IExtension { readonly type: ExtensionType; readonly isBuiltin: boolean; + readonly isWorkspaceScoped: boolean; readonly state: ExtensionState; readonly name: string; readonly displayName: string; @@ -95,6 +96,7 @@ export interface IExtension { readonly server?: IExtensionManagementServer; readonly local?: ILocalExtension; gallery?: IGalleryExtension; + readonly resourceExtension?: IResourceExtension; readonly isMalicious: boolean; readonly deprecationInfo?: IDeprecationInfo; } @@ -121,6 +123,7 @@ export interface IExtensionsWorkbenchService { queryGallery(options: IQueryOptions, token: CancellationToken): Promise>; getExtensions(extensionInfos: IExtensionInfo[], token: CancellationToken): Promise; getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise; + getResourceExtensions(locations: URI[], isWorkspaceScoped: boolean): Promise; canInstall(extension: IExtension): Promise; install(id: string, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; install(vsix: URI, installOptions?: InstallExtensionOptions, progressLocation?: ProgressLocation): Promise; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts index 4186a04ec3556..8c96cf7a48ead 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts @@ -27,78 +27,78 @@ suite('Extension Test', () => { }); test('extension is not outdated when there is no local and gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, undefined); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, undefined, undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when there is local and no gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), undefined); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), undefined, undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when there is no local and has gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, aGalleryExtension()); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, aGalleryExtension(), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local and gallery are on same version', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), aGalleryExtension()); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), aGalleryExtension(), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local is older than gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local is built in and older than gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is not outdated when local is built in and older than gallery but product quality is stable', () => { instantiationService.stub(IProductService, { quality: 'stable' }); - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local and gallery are on same version but on different target platforms', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_ARM64 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_ARM64 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is not outdated when local and gallery are on same version and local is on web', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), aGalleryExtension('somext')); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), aGalleryExtension('somext'), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local and gallery are on same version and gallery is on web', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext'), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WEB })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext'), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local is not pre-release but gallery is pre-release', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true }), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local and gallery are pre-releases', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local was opted to pre-release but current version is not pre-release', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local is pre-release but gallery is not', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local was opted pre-release but current version is not and gallery is not', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }), undefined); assert.strictEqual(extension.outdated, true); }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index d0bb47b7529b9..0eef5e664a8e5 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -64,6 +64,10 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { DisposableStore } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; import { IUpdateService, State } from 'vs/platform/update/common/update'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; + +const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -208,6 +212,12 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.stub(ILifecycleService, disposableStore.add(new TestLifecycleService())); testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); + instantiationService.stub(ILogService, NullLogService); + const fileService = new FileService(instantiationService.get(ILogService)); + instantiationService.stub(IFileService, disposableStore.add(fileService)); + const fileSystemProvider = disposableStore.add(new InMemoryFileSystemProvider()); + disposableStore.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); + instantiationService.stub(IUriIdentityService, disposableStore.add(new UriIdentityService(instantiationService.get(IFileService)))); instantiationService.stub(INotificationService, new TestNotificationService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IWorkbenchExtensionManagementService, { @@ -311,12 +321,7 @@ suite('ExtensionRecommendationsService Test', () => { } async function setUpFolder(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); - const logService = new NullLogService(); - const fileService = disposableStore.add(new FileService(logService)); - const fileSystemProvider = disposableStore.add(new InMemoryFileSystemProvider()); - disposableStore.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - + const fileService = instantiationService.get(IFileService); const folderDir = joinPath(ROOT, folderName); const workspaceSettingsDir = joinPath(folderDir, '.vscode'); await fileService.createFolder(workspaceSettingsDir); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index f7c858e86d9fd..ef441b7e0f8d8 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -95,6 +95,7 @@ function setupTest(disposables: Pick) { onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, async getInstalled() { return []; }, + async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, async updateMetadata(local: ILocalExtension, metadata: Partial) { local.identifier.uuid = metadata.id; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index d37a7df877550..399fb443e55cc 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -95,6 +95,7 @@ suite('ExtensionsViews Tests', () => { onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, async getInstalled() { return []; }, + async getInstalledWorkspaceExtensions() { return []; }, async canInstall() { return true; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, async getTargetPlatform() { return getTargetPlatform(platform, arch); }, 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 701a823db7dd5..cfb7e3f150da9 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 @@ -95,6 +95,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUpdateExtensionMetadata: Event.None, onDidChangeProfile: Event.None, async getInstalled() { return []; }, + async getInstalledWorkspaceExtensions() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, async updateMetadata(local: ILocalExtension, metadata: Partial) { local.identifier.uuid = metadata.id; diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts new file mode 100644 index 0000000000000..23880e570a91c --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService } from 'vs/platform/log/common/log'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { AbstractExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +export class ExtensionsProfileScannerService extends AbstractExtensionsProfileScannerService { + constructor( + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IFileService fileService: IFileService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @ITelemetryService telemetryService: ITelemetryService, + @ILogService logService: ILogService, + ) { + super(environmentService.userRoamingDataHome, fileService, userDataProfilesService, uriIdentityService, telemetryService, logService); + } +} + +registerSingleton(IExtensionsProfileScannerService, ExtensionsProfileScannerService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index d87d00552d8b0..71e9054fa9958 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IExtension, ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IExtension, ExtensionType, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, Metadata, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { FileAccess } from 'vs/base/common/network'; @@ -41,6 +41,14 @@ export interface IExtensionManagementServerService { export const DefaultIconPath = FileAccess.asBrowserUri('vs/workbench/services/extensionManagement/common/media/defaultIcon.png').toString(true); +export interface IResourceExtension { + readonly identifier: IExtensionIdentifier; + readonly location: URI; + readonly manifest: IExtensionManifest; + readonly readmeUri?: URI; + readonly changelogUri?: URI; +} + export type InstallExtensionOnServerEvent = InstallExtensionEvent & { server: IExtensionManagementServer }; export type UninstallExtensionOnServerEvent = UninstallExtensionEvent & { server: IExtensionManagementServer }; export type DidUninstallExtensionOnServerEvent = DidUninstallExtensionEvent & { server: IExtensionManagementServer }; @@ -56,8 +64,13 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten onDidUninstallExtension: Event; onDidChangeProfile: Event; + getExtension(location: URI): Promise; + getInstalledWorkspaceExtensions(): Promise; + installVSIX(location: URI, manifest: IExtensionManifest, installOptions?: InstallOptions): Promise; installFromLocation(location: URI): Promise; + installResourceExtension(extension: IResourceExtension, installOptions: InstallOptions): Promise; + updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions): Promise; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index c60c940c4d5ee..81e757ef157ef 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, EventMultiplexer } from 'vs/base/common/event'; +import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; import { ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -19,7 +19,7 @@ import { localize } from 'vs/nls'; import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; import { IDownloadService } from 'vs/platform/download/common/download'; -import { flatten } from 'vs/base/common/arrays'; +import { coalesce } from 'vs/base/common/arrays'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; @@ -28,20 +28,35 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { isUndefined } from 'vs/base/common/types'; +import { isString, isUndefined } from 'vs/base/common/types'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -import { CancellationError } from 'vs/base/common/errors'; +import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IExtensionsScannerService, IScannedExtension } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { + private static readonly WORKSPACE_EXTENSIONS_KEY = 'workspaceExtensions.locations'; + declare readonly _serviceBrand: undefined; + private readonly _onInstallExtension = this._register(new Emitter()); readonly onInstallExtension: Event; + + private readonly _onDidInstallExtensions = this._register(new Emitter()); readonly onDidInstallExtensions: Event; + + private readonly _onUninstallExtension = this._register(new Emitter()); readonly onUninstallExtension: Event; + + private readonly _onDidUninstallExtension = this._register(new Emitter()); readonly onDidUninstallExtension: Event; + readonly onDidUpdateExtensionMetadata: Event; readonly onDidChangeProfile: Event; @@ -61,6 +76,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IStorageService private readonly storageService: IStorageService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); if (this.extensionManagementServerService.localExtensionManagementServer) { @@ -73,20 +93,55 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.servers.push(this.extensionManagementServerService.webExtensionManagementServer); } - this.onInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onInstallExtension, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; - this.onDidInstallExtensions = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(server.extensionManagementService.onDidInstallExtensions)); return emitter; }, this._register(new EventMultiplexer()))).event; - this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onUninstallExtension, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; - this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; - this.onDidUpdateExtensionMetadata = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(server.extensionManagementService.onDidUpdateExtensionMetadata)); return emitter; }, this._register(new EventMultiplexer()))).event; - this.onDidChangeProfile = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; + const onInstallExtensionEventMultiplexer = this._register(new EventMultiplexer()); + this._register(onInstallExtensionEventMultiplexer.add(this._onInstallExtension.event)); + this.onInstallExtension = onInstallExtensionEventMultiplexer.event; + + const onDidInstallExtensionsEventMultiplexer = this._register(new EventMultiplexer()); + this._register(onDidInstallExtensionsEventMultiplexer.add(this._onDidInstallExtensions.event)); + this.onDidInstallExtensions = onDidInstallExtensionsEventMultiplexer.event; + + const onUninstallExtensionEventMultiplexer = this._register(new EventMultiplexer()); + this._register(onUninstallExtensionEventMultiplexer.add(this._onUninstallExtension.event)); + this.onUninstallExtension = onUninstallExtensionEventMultiplexer.event; + + const onDidUninstallExtensionEventMultiplexer = this._register(new EventMultiplexer()); + this._register(onDidUninstallExtensionEventMultiplexer.add(this._onDidUninstallExtension.event)); + this.onDidUninstallExtension = onDidUninstallExtensionEventMultiplexer.event; + + const onDidUpdateExtensionMetadaEventMultiplexer = this._register(new EventMultiplexer()); + this.onDidUpdateExtensionMetadata = onDidUpdateExtensionMetadaEventMultiplexer.event; + + const onDidChangeProfileEventMultiplexer = this._register(new EventMultiplexer()); + this.onDidChangeProfile = onDidChangeProfileEventMultiplexer.event; + + for (const server of this.servers) { + this._register(onInstallExtensionEventMultiplexer.add(Event.map(server.extensionManagementService.onInstallExtension, e => ({ ...e, server })))); + this._register(onDidInstallExtensionsEventMultiplexer.add(server.extensionManagementService.onDidInstallExtensions)); + this._register(onUninstallExtensionEventMultiplexer.add(Event.map(server.extensionManagementService.onUninstallExtension, e => ({ ...e, server })))); + this._register(onDidUninstallExtensionEventMultiplexer.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server })))); + this._register(onDidUpdateExtensionMetadaEventMultiplexer.add(server.extensionManagementService.onDidUpdateExtensionMetadata)); + this._register(onDidChangeProfileEventMultiplexer.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); + } } async getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise { - const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, profileLocation, productVersion))); - return flatten(result); + const result: ILocalExtension[] = []; + await Promise.all(this.servers.map(async server => { + const installed = await server.extensionManagementService.getInstalled(type, profileLocation, productVersion); + if (server === this.getWorkspaceExtensionsServer()) { + const workspaceExtensions = await this.getInstalledWorkspaceExtensions(); + installed.push(...workspaceExtensions); + } + result.push(...installed); + })); + return result; } async uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { + if (extension.isWorkspaceScoped) { + return this.uninstallExtensionFromWorkspace(extension); + } const server = this.getServer(extension); if (!server) { return Promise.reject(`Invalid location ${extension.location.toString()}`); @@ -346,6 +401,206 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promises.settled(servers.map(server => server.extensionManagementService.installFromGallery(gallery, installOptions))).then(([local]) => local); } + async getExtension(location: URI): Promise { + const workspaceExtension = await this.scanWorkspaceExtension(location); + if (workspaceExtension) { + return { + identifier: workspaceExtension.identifier, + location: workspaceExtension.location, + manifest: workspaceExtension.manifest, + changelogUri: workspaceExtension.changelogUrl, + readmeUri: workspaceExtension.readmeUrl, + }; + } + return null; + } + + async getInstalledWorkspaceExtensions(): Promise { + const existingLocations = this.getWorkspaceExtensionsLocations(); + if (!existingLocations.length) { + return []; + } + + const workspaceExtensions: ILocalExtension[] = []; + let save = false; + await Promise.allSettled(existingLocations.map(async location => { + if (!this.workspaceService.isInsideWorkspace(location)) { + save = true; + this.logService.info(`Removing the workspace extension ${location.toString()} as it is not inside the workspace`); + return; + } + if (!(await this.fileService.exists(location))) { + save = true; + this.logService.info(`Removing the workspace extension ${location.toString()} as it does not exist`); + return; + } + try { + const extension = await this.scanWorkspaceExtension(location); + if (extension) { + workspaceExtensions.push(extension); + } else { + this.logService.info(`Skipping workspace extension ${location.toString()} as it does not exist`); + } + } catch (error) { + this.logService.error('Skipping the workspace extension', location.toString(), error); + } + })); + if (save) { + this.saveWorkspaceExtensionsLocations(workspaceExtensions.map(e => e.location)); + } + return workspaceExtensions; + } + + async installResourceExtension(extension: IResourceExtension, installOptions: InstallOptions): Promise { + if (!installOptions.isWorkspaceScoped) { + return this.installFromLocation(extension.location); + } + + this.logService.info(`Installing the extension ${extension.identifier.id} from ${extension.location.toString()} in workspace`); + const server = this.getWorkspaceExtensionsServer(); + this._onInstallExtension.fire({ + identifier: extension.identifier, + source: extension.location, + server, + applicationScoped: false, + profileLocation: this.userDataProfileService.currentProfile.extensionsResource, + workspaceScoped: true + }); + + try { + const workspaceExtension = await this.scanWorkspaceExtension(extension.location); + if (!workspaceExtension) { + throw new Error('Cannot install the extension as it does not exist.'); + } + if (!workspaceExtension.isValid) { + throw new Error(`Cannot install the extension as it is invalid. ${workspaceExtension.validations.join(', ')}`); + } + + await this.checkForWorkspaceTrust(extension.manifest); + + const existingLocations = this.getWorkspaceExtensionsLocations(); + const workspaceExtensions = await this.getInstalledWorkspaceExtensions(); + + const existingExtensionIndex = workspaceExtensions.findIndex(e => areSameExtensions(e.identifier, workspaceExtension.identifier)); + if (existingExtensionIndex !== -1) { + existingLocations.splice(existingExtensionIndex, 1); + } + existingLocations.push(extension.location); + + this.saveWorkspaceExtensionsLocations(existingLocations); + + // log + this.logService.info(`Successfully installed the extension ${workspaceExtension.identifier.id} from ${extension.location.toString()} in the workspace`); + this.telemetryService.publicLog2<{}, { + owner: 'sandy081'; + comment: 'Install workspace extension'; + }>('workspaceextension:install'); + + this._onDidInstallExtensions.fire([{ + identifier: workspaceExtension.identifier, + source: extension.location, + operation: existingExtensionIndex !== -1 ? InstallOperation.Update : InstallOperation.Install, + applicationScoped: false, + profileLocation: this.userDataProfileService.currentProfile.extensionsResource, + local: workspaceExtension, + workspaceScoped: true + }]); + return workspaceExtension; + } catch (error) { + this._onDidInstallExtensions.fire([{ + identifier: extension.identifier, + source: extension.location, + operation: InstallOperation.Install, + applicationScoped: false, + profileLocation: this.userDataProfileService.currentProfile.extensionsResource, + error, + workspaceScoped: true + }]); + throw error; + } + } + + private async uninstallExtensionFromWorkspace(extension: ILocalExtension): Promise { + if (!extension.isWorkspaceScoped) { + throw new Error('The extension is not a workspace extension'); + } + + this.logService.info(`Uninstalling the workspace extension ${extension.identifier.id} from ${extension.location.toString()}`); + const server = this.getWorkspaceExtensionsServer(); + this._onUninstallExtension.fire({ + identifier: extension.identifier, + server, + applicationScoped: false, + workspaceScoped: true + }); + + try { + const existingLocations = this.getWorkspaceExtensionsLocations(); + const index = existingLocations.findIndex(existingLocation => this.uriIdentityService.extUri.isEqual(extension.location, existingLocation)); + if (index !== -1) { + existingLocations.splice(index, 1); + this.saveWorkspaceExtensionsLocations(existingLocations); + } + + this.logService.info(`Successfully uninstalled the workspace extension ${extension.identifier.id} from ${extension.location.toString()}`); + this.telemetryService.publicLog2<{}, { + owner: 'sandy081'; + comment: 'Uninstall workspace extension'; + }>('workspaceextension:uninstall'); + this._onDidUninstallExtension.fire({ + identifier: extension.identifier, + server, + applicationScoped: false, + workspaceScoped: true + }); + } catch (error) { + this._onDidUninstallExtension.fire({ + identifier: extension.identifier, + server, + error, + applicationScoped: false, + workspaceScoped: true + }); + throw error; + } + } + + private getWorkspaceExtensionsLocations(): URI[] { + const locations: URI[] = []; + try { + const parsed = JSON.parse(this.storageService.get(ExtensionManagementService.WORKSPACE_EXTENSIONS_KEY, StorageScope.WORKSPACE, '[]')); + if (Array.isArray(locations)) { + for (const location of parsed) { + if (isString(location)) { + if (this.workspaceService.getWorkbenchState() === WorkbenchState.FOLDER) { + locations.push(this.workspaceService.getWorkspace().folders[0].toResource(location)); + } else { + this.logService.warn(`Invalid value for 'extensions' in workspace storage: ${location}`); + } + } else { + locations.push(URI.revive(location)); + } + } + } else { + this.logService.warn(`Invalid value for 'extensions' in workspace storage: ${locations}`); + } + } catch (error) { + this.logService.warn(`Error parsing workspace extensions locations: ${getErrorMessage(error)}`); + } + return locations; + } + + private saveWorkspaceExtensionsLocations(locations: URI[]): void { + if (this.workspaceService.getWorkbenchState() === WorkbenchState.FOLDER) { + this.storageService.store(ExtensionManagementService.WORKSPACE_EXTENSIONS_KEY, + JSON.stringify(coalesce(locations + .map(location => this.uriIdentityService.extUri.relativePath(this.workspaceService.getWorkspace().folders[0].uri, location)))), + StorageScope.WORKSPACE, StorageTarget.MACHINE); + } else { + this.storageService.store(ExtensionManagementService.WORKSPACE_EXTENSIONS_KEY, JSON.stringify(locations), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + } + private async validateAndGetExtensionManagementServersToInstall(gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); @@ -453,9 +708,25 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } private getServer(extension: ILocalExtension): IExtensionManagementServer | null { + if (extension.isWorkspaceScoped) { + return this.getWorkspaceExtensionsServer(); + } return this.extensionManagementServerService.getExtensionManagementServer(extension); } + private getWorkspaceExtensionsServer(): IExtensionManagementServer { + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.extensionManagementServerService.remoteExtensionManagementServer; + } + if (this.extensionManagementServerService.localExtensionManagementServer) { + return this.extensionManagementServerService.localExtensionManagementServer; + } + if (this.extensionManagementServerService.webExtensionManagementServer) { + return this.extensionManagementServerService.webExtensionManagementServer; + } + throw new Error('No extension server found'); + } + protected async checkForWorkspaceTrust(manifest: IExtensionManifest): Promise { if (this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(manifest) === false) { const trustState = await this.workspaceTrustRequestService.requestWorkspaceTrust({ @@ -582,6 +853,45 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.resolve(); } + private async scanWorkspaceExtension(location: URI): Promise { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { includeInvalid: true }); + return scannedExtension ? this.toLocalWorkspaceExtension(scannedExtension) : null; + } + + private async toLocalWorkspaceExtension(extension: IScannedExtension): Promise { + const stat = await this.fileService.resolve(extension.location); + let readmeUrl: URI | undefined; + let changelogUrl: URI | undefined; + if (stat.children) { + readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; + changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; + } + return { + identifier: extension.identifier, + type: extension.type, + isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin, + location: extension.location, + manifest: extension.manifest, + targetPlatform: extension.targetPlatform, + validations: extension.validations, + isValid: extension.isValid, + readmeUrl, + changelogUrl, + publisherDisplayName: extension.metadata?.publisherDisplayName || null, + publisherId: extension.metadata?.publisherId || null, + isApplicationScoped: !!extension.metadata?.isApplicationScoped, + isMachineScoped: !!extension.metadata?.isMachineScoped, + isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion, + hasPreReleaseVersion: !!extension.metadata?.hasPreReleaseVersion, + preRelease: !!extension.metadata?.preRelease, + installedTimestamp: extension.metadata?.installedTimestamp, + updated: !!extension.metadata?.updated, + pinned: !!extension.metadata?.pinned, + isWorkspaceScoped: true, + source: 'resource' + }; + } + registerParticipant() { throw new Error('Not Supported'); } installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { throw new Error('Not Supported'); } } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 80ff4f879d828..e4f0b773a151d 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -235,6 +235,8 @@ function toLocalExtension(extension: IExtension): ILocalExtension { targetPlatform: TargetPlatform.WEB, updated: !!metadata.updated, pinned: !!metadata?.pinned, + isWorkspaceScoped: false, + source: metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'resource') }; } @@ -289,6 +291,7 @@ class InstallExtensionTask extends AbstractExtensionTask implem metadata.preRelease = isBoolean(this.options.preRelease) ? this.options.preRelease : this.options.installPreReleaseVersion || this.extension.properties.isPreReleaseVersion || metadata.preRelease; + metadata.source = URI.isUri(this.extension) ? 'resource' : 'gallery'; } metadata.pinned = this.options.installGivenVersion ? true : (this.options.pinned ?? metadata.pinned); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index 2e04440e43afd..50d1768eb6a56 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -23,6 +23,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class ExtensionManagementService extends BaseExtensionManagementService { @@ -41,8 +46,32 @@ export class ExtensionManagementService extends BaseExtensionManagementService { @IFileService fileService: IFileService, @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, + @IExtensionsScannerService extensionsScannerService: IExtensionsScannerService, + @IStorageService storageService: IStorageService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(extensionManagementServerService, extensionGalleryService, userDataProfileService, configurationService, productService, downloadService, userDataSyncEnablementService, dialogService, workspaceTrustRequestService, extensionManifestPropertiesService, fileService, logService, instantiationService); + super( + extensionManagementServerService, + extensionGalleryService, + userDataProfileService, + configurationService, + productService, + downloadService, + userDataSyncEnablementService, + dialogService, + workspaceTrustRequestService, + extensionManifestPropertiesService, + fileService, + logService, + instantiationService, + workspaceService, + extensionsScannerService, + storageService, + uriIdentityService, + telemetryService + ); } protected override async installVSIXInServer(vsix: URI, server: IExtensionManagementServer, options: InstallOptions | undefined): Promise { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 24908288aa99e..0829aec6ed876 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -34,7 +34,7 @@ import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService import { TestContextService, TestProductService, TestWorkspaceTrustEnablementService, TestWorkspaceTrustManagementService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; -import { NullLogService } from 'vs/platform/log/common/log'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -140,6 +140,7 @@ suite('ExtensionEnablementService Test', () => { getInstalled: () => Promise.resolve(installed) }, }, null, null)); + instantiationService.stub(ILogService, NullLogService); instantiationService.stub(IWorkbenchExtensionManagementService, disposableStore.add(instantiationService.createInstance(ExtensionManagementService))); testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); }); diff --git a/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts index 85d635eda894e..72e398b463148 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/extensionRecommendations.ts @@ -6,6 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStringDictionary } from 'vs/base/common/collections'; import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; export const enum ExtensionRecommendationReason { Workspace, @@ -35,7 +36,7 @@ export interface IExtensionRecommendationsService { getFileBasedRecommendations(): string[]; getExeBasedRecommendations(exe?: string): Promise<{ important: string[]; others: string[] }>; getConfigBasedRecommendations(): Promise<{ important: string[]; others: string[] }>; - getWorkspaceRecommendations(): Promise; + getWorkspaceRecommendations(): Promise>; getKeymapRecommendations(): string[]; getLanguageRecommendations(): string[]; getRemoteRecommendations(): string[]; diff --git a/src/vs/workbench/services/extensions/browser/extensionsScannerService.ts b/src/vs/workbench/services/extensions/browser/extensionsScannerService.ts new file mode 100644 index 0000000000000..993d8a09fe186 --- /dev/null +++ b/src/vs/workbench/services/extensions/browser/extensionsScannerService.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; +import { AbstractExtensionsScannerService, IExtensionsScannerService, Translations, } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +export class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { + + constructor( + @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IProductService productService: IProductService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super( + uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'systemExtensions'), + uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'userExtensions'), + uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'userExtensions', 'control.json'), + userDataProfileService.currentProfile, + userDataProfilesService, extensionsProfileScannerService, fileService, logService, environmentService, productService, uriIdentityService, instantiationService); + } + + protected async getTranslations(): Promise { + return {}; + } + +} + +registerSingleton(IExtensionsScannerService, ExtensionsScannerService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 4d82026746187..9a9b1bbbd92fa 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -6,9 +6,9 @@ import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { IExtensionDescription, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription, ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IExtensionsScannerService, IScannedExtension, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IExtensionsScannerService, IScannedExtension, toExtensionDescription as toExtensionDescriptionFromScannedExtension } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ILogService } from 'vs/platform/log/common/log'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; @@ -17,6 +17,8 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { timeout } from 'vs/base/common/async'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { getErrorMessage } from 'vs/base/common/errors'; +import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; export class CachedExtensionScanner { @@ -29,6 +31,7 @@ export class CachedExtensionScanner { @IHostService private readonly _hostService: IHostService, @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, + @IWorkbenchExtensionManagementService private readonly _extensionManagementService: IWorkbenchExtensionManagementService, @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise((resolve, reject) => { @@ -39,7 +42,7 @@ export class CachedExtensionScanner { public async scanSingleExtension(extensionPath: string, isBuiltin: boolean): Promise { const scannedExtension = await this._extensionsScannerService.scanExistingExtension(URI.file(path.resolve(extensionPath)), isBuiltin ? ExtensionType.System : ExtensionType.User, { language: platform.language }); - return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; + return scannedExtension ? toExtensionDescriptionFromScannedExtension(scannedExtension, false) : null; } public async startScanningExtensions(): Promise { @@ -56,10 +59,13 @@ export class CachedExtensionScanner { const language = platform.language; const result = await Promise.allSettled([ this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), - this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true })]); + this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true }), + this._extensionManagementService.getInstalledWorkspaceExtensions() + ]); let scannedSystemExtensions: IScannedExtension[] = [], scannedUserExtensions: IScannedExtension[] = [], + workspaceExtensions: IExtension[] = [], scannedDevelopedExtensions: IScannedExtension[] = [], hasErrors = false; @@ -77,16 +83,24 @@ export class CachedExtensionScanner { this._logService.error(`Error scanning user extensions:`, getErrorMessage(result[1].reason)); } + if (result[2].status === 'fulfilled') { + workspaceExtensions = result[2].value; + } else { + hasErrors = true; + this._logService.error(`Error scanning workspace extensions:`, getErrorMessage(result[2].reason)); + } + try { scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment({ language }, [...scannedSystemExtensions, ...scannedUserExtensions]); } catch (error) { this._logService.error(error); } - const system = scannedSystemExtensions.map(e => toExtensionDescription(e, false)); - const user = scannedUserExtensions.map(e => toExtensionDescription(e, false)); - const development = scannedDevelopedExtensions.map(e => toExtensionDescription(e, true)); - const r = dedupExtensions(system, user, development, this._logService); + const system = scannedSystemExtensions.map(e => toExtensionDescriptionFromScannedExtension(e, false)); + const userGlobal = scannedUserExtensions.map(e => toExtensionDescriptionFromScannedExtension(e, false)); + const userWorkspace = workspaceExtensions.map(e => toExtensionDescription(e, false)); + const development = scannedDevelopedExtensions.map(e => toExtensionDescriptionFromScannedExtension(e, true)); + const r = dedupExtensions(system, [...userGlobal, ...userWorkspace], development, this._logService); if (!hasErrors) { const disposable = this._extensionsScannerService.onDidChangeCache(() => { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index c37caab7092d1..fd003921c470e 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -160,7 +160,7 @@ import { ILayoutOffsetInfo } from 'vs/platform/layout/browser/layoutService'; import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { EnablementState, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { EnablementState, IResourceExtension, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILocalExtension, IGalleryExtension, InstallOptions, IExtensionIdentifier, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata, InstallExtensionResult, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Codicon } from 'vs/base/common/codicons'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; @@ -2148,6 +2148,10 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens toggleAppliationScope(): Promise { throw new Error('Not Supported'); } installExtensionsFromProfile(): Promise { throw new Error('Not Supported'); } whenProfileChanged(from: IUserDataProfile, to: IUserDataProfile): Promise { throw new Error('Not Supported'); } + getInstalledWorkspaceExtensions(): Promise { throw new Error('Method not implemented.'); } + installResourceExtension(): Promise { throw new Error('Method not implemented.'); } + getAllExtensions(location: URI): Promise { throw new Error('Method not implemented.'); } + getExtension(location: URI): Promise { throw new Error('Method not implemented.'); } } export class TestUserDataProfileService implements IUserDataProfileService { diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index bb6d118d46053..3dd96d43b7b2d 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -85,15 +85,13 @@ import 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService import 'vs/workbench/services/extensions/electron-sandbox/nativeExtensionService'; import 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService'; import 'vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService'; +import 'vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService'; -import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; -import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; -import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; registerSingleton(IUserDataInitializationService, new SyncDescriptor(UserDataInitializationService, [[]], true)); -registerSingleton(IExtensionsProfileScannerService, ExtensionsProfileScannerService, InstantiationType.Delayed); //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index a57ce4d7f351e..e4a0a7e3e7240 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -39,6 +39,8 @@ import 'vs/workbench/services/search/browser/searchService'; import 'vs/workbench/services/textfile/browser/browserTextFileService'; import 'vs/workbench/services/keybinding/browser/keyboardLayoutService'; import 'vs/workbench/services/extensions/browser/extensionService'; +import 'vs/workbench/services/extensionManagement/browser/extensionsProfileScannerService'; +import 'vs/workbench/services/extensions/browser/extensionsScannerService'; import 'vs/workbench/services/extensionManagement/browser/webExtensionsScannerService'; import 'vs/workbench/services/extensionManagement/common/extensionManagementServerService'; import 'vs/workbench/services/telemetry/browser/telemetryService';