diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index c54fe1b2a34..2c453505e19 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -173,7 +173,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem this.scanUserExtensions(userScanOptions), ]); const development = await this.scanExtensionsUnderDevelopment(systemScanOptions, [...system, ...user]); - return this.dedupExtensions([...system, ...user, ...development], await this.getTargetPlatform(), true); + return this.dedupExtensions(system, user, development, await this.getTargetPlatform(), true); } async scanSystemExtensions(scanOptions: ScanOptions): Promise { @@ -181,7 +181,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem promises.push(this.scanDefaultSystemExtensions(!!scanOptions.useCache, scanOptions.language)); promises.push(this.scanDevSystemExtensions(scanOptions.language, !!scanOptions.checkControlFile)); const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises); - return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], scanOptions, false); + return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], ExtensionType.System, scanOptions, false); } async scanUserExtensions(scanOptions: ScanOptions): Promise { @@ -189,7 +189,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const extensionsScannerInput = await this.createExtensionScannerInput(scanOptions.profileLocation ?? this.userExtensionsLocation, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions = await extensionsScanner.scanExtensions(extensionsScannerInput); - extensions = await this.applyScanOptions(extensions, scanOptions, true); + extensions = await this.applyScanOptions(extensions, ExtensionType.User, scanOptions, true); this.logService.trace('Scanned user extensions:', extensions.length); return extensions; } @@ -208,7 +208,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem }); }))) .flat(); - return this.applyScanOptions(extensions, scanOptions, true); + return this.applyScanOptions(extensions, 'development', scanOptions, true); } return []; } @@ -228,7 +228,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); - return this.applyScanOptions(extensions, scanOptions, true); + return this.applyScanOptions(extensions, extensionType, scanOptions, true); } async scanMetadata(extensionLocation: URI): Promise { @@ -252,9 +252,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem await this.fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest, null, '\t'))); } - private async applyScanOptions(extensions: IRelaxedScannedExtension[], scanOptions: ScanOptions, pickLatest: boolean): Promise { + private async applyScanOptions(extensions: IRelaxedScannedExtension[], type: ExtensionType | 'development', scanOptions: ScanOptions, pickLatest: boolean): Promise { if (!scanOptions.includeAllVersions) { - extensions = this.dedupExtensions(extensions, await this.getTargetPlatform(), pickLatest); + extensions = this.dedupExtensions(type === ExtensionType.System ? extensions : undefined, type === ExtensionType.User ? extensions : undefined, type === 'development' ? extensions : undefined, await this.getTargetPlatform(), pickLatest); } if (!scanOptions.includeInvalid) { extensions = extensions.filter(extension => extension.isValid); @@ -272,33 +272,61 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem }); } - private dedupExtensions(extensions: IRelaxedScannedExtension[], targetPlatform: TargetPlatform, pickLatest: boolean): IRelaxedScannedExtension[] { - const result = new Map(); - for (const extension of extensions) { - const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); - const existing = result.get(extensionKey); - if (existing) { - if (existing.isValid && !extension.isValid) { - continue; + private dedupExtensions(system: IScannedExtension[] | undefined, user: IScannedExtension[] | undefined, development: IScannedExtension[] | undefined, targetPlatform: TargetPlatform, pickLatest: boolean): IScannedExtension[] { + const pick = (existing: IScannedExtension, extension: IScannedExtension): boolean => { + if (existing.isValid && !extension.isValid) { + return false; + } + if (existing.isValid === extension.isValid) { + if (pickLatest && semver.gt(existing.manifest.version, extension.manifest.version)) { + this.logService.debug(`Skipping extension ${extension.location.path} with lower version ${extension.manifest.version} in favour of ${existing.location.path} with version ${existing.manifest.version}`); + return false; } - if (existing.isValid === extension.isValid) { - if (pickLatest && semver.gt(existing.manifest.version, extension.manifest.version)) { - this.logService.debug(`Skipping extension ${extension.location.path} with lower version ${extension.manifest.version}.`); - continue; + if (semver.eq(existing.manifest.version, extension.manifest.version)) { + if (existing.type === ExtensionType.System) { + this.logService.debug(`Skipping extension ${extension.location.path} in favour of system extension ${existing.location.path} with same version`); + return false; } - if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) { + if (existing.targetPlatform === targetPlatform) { this.logService.debug(`Skipping extension ${extension.location.path} from different target platform ${extension.targetPlatform}`); - continue; + return false; } } - if (existing.type === ExtensionType.System) { - this.logService.debug(`Overwriting system extension ${existing.location.path} with ${extension.location.path}.`); - } else { - this.logService.warn(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`); - } + } + if (existing.type === ExtensionType.System) { + this.logService.debug(`Overwriting system extension ${existing.location.path} with ${extension.location.path}.`); + } else { + this.logService.warn(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`); + } + return true; + }; + const result = new Map(); + system?.forEach((extension) => { + const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); + const existing = result.get(extensionKey); + if (!existing || pick(existing, extension)) { + result.set(extensionKey, extension); + } + }); + user?.forEach((extension) => { + const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); + const existing = result.get(extensionKey); + if (!existing && system && extension.type === ExtensionType.System) { + this.logService.debug(`Skipping obsolete system extension ${extension.location.path}.`); + return; + } + if (!existing || pick(existing, extension)) { + result.set(extensionKey, extension); + } + }); + development?.forEach(extension => { + const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); + const existing = result.get(extensionKey); + if (!existing || pick(existing, extension)) { + result.set(extensionKey, extension); } result.set(extensionKey, extension); - } + }); return [...result.values()]; } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 1006bd05439..a15de91f394 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -432,23 +432,33 @@ class ExtensionsScanner extends Disposable { } private async removeOutdatedExtensions(): Promise { + const systemExtensions = await this.extensionsScannerService.scanSystemExtensions({}); // System extensions const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions const toRemove: IScannedExtension[] = []; // Outdated extensions const targetPlatform = await this.extensionsScannerService.getTargetPlatform(); const byExtension = groupByExtension(extensions, e => e.identifier); - toRemove.push(...byExtension.map(p => p.sort((a, b) => { - const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); - if (vcompare !== 0) { - return vcompare; + for (const extensions of byExtension) { + if (extensions.length > 1) { + toRemove.push(...extensions.sort((a, b) => { + const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); + if (vcompare !== 0) { + return vcompare; + } + if (a.targetPlatform === targetPlatform) { + return -1; + } + return 1; + }).slice(1)); } - if (a.targetPlatform === targetPlatform) { - return -1; + if (extensions[0].type === ExtensionType.System) { + const systemExtension = systemExtensions.find(e => areSameExtensions(e.identifier, extensions[0].identifier)); + if (!systemExtension || semver.gte(systemExtension.manifest.version, extensions[0].manifest.version)) { + toRemove.push(extensions[0]); + } } - return 1; - }).slice(1)).flat()); - + } await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); } diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index c1755f15dff..0a65c761e81 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -24,8 +24,8 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio const extension = result.get(extensionKey); if (extension) { if (extension.isBuiltin) { - if (semver.gt(extension.version, userExtension.version)) { - logService.warn(`Skipping extension ${userExtension.extensionLocation.path} with lower version ${userExtension.version}.`); + if (semver.gte(extension.version, userExtension.version)) { + logService.warn(`Skipping extension ${userExtension.extensionLocation.path} in favour of the builtin extension ${extension.extensionLocation.path}.`); return; } // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning @@ -33,6 +33,9 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio } else { logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); } + } else if (userExtension.isBuiltin) { + logService.warn(`Skipping obsolete builtin extension ${userExtension.extensionLocation.path}`); + return; } result.set(extensionKey, userExtension); }); diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 69a2d8f5d53..a38a74255db 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -43,15 +43,14 @@ export class CachedExtensionScanner { public async startScanningExtensions(): Promise { try { - const { system, user, development } = await this._scanInstalledExtensions(); - const r = dedupExtensions(system, user, development, this._logService); - this._scannedExtensionsResolve(r); + const extensions = await this._scanInstalledExtensions(); + this._scannedExtensionsResolve(extensions); } catch (err) { this._scannedExtensionsReject(err); } } - private async _scanInstalledExtensions(): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { + private async _scanInstalledExtensions(): Promise { try { const language = platform.language; const [scannedSystemExtensions, scannedUserExtensions] = await Promise.all([ @@ -61,6 +60,7 @@ export class CachedExtensionScanner { 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 disposable = this._extensionsScannerService.onDidChangeCache(() => { disposable.dispose(); this._notificationService.prompt( @@ -73,11 +73,11 @@ export class CachedExtensionScanner { ); }); timeout(5000).then(() => disposable.dispose()); - return { system, user, development }; + return r; } catch (err) { this._logService.error(`Error scanning installed extensions:`); this._logService.error(err); - return { system: [], user: [], development: [] }; + return []; } }