Repository: microsoft/vscode (TypeScript)
Date: 2023-08-22 10:08:40+00:00
Task type: Bug Fix

Issue:
Installing extension from VSIX after installing from location deletes original directory
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes/No

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
- VS Code Version: 1.80.1 
- OS Version: Mac OS 13.4.1

Steps to Reproduce:

1. Set up extension
2. Use `Developer: Install Extension From Location` to install the extension from the root path of the extension (the one with package.json etc)
3. Package the extension into vsix
4. Use `Developer: Install Extension From VSIX` and select the newly created vsix (this file can be anywhere)
5. The extension directory gets deleted



Issue discussion:
This is not fixed completely after reverting the fix (https://github.com/microsoft/vscode/pull/190074). 

Hence reopened it.

Solution patch:
diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
--- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
+++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
@@ -124,22 +124,24 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable
 		const extensionsToRemove: IScannedProfileExtension[] = [];
 		const extensionsToAdd: IScannedProfileExtension[] = [];
 		try {
-			await this.withProfileExtensions(profileLocation, profileExtensions => {
+			await this.withProfileExtensions(profileLocation, existingExtensions => {
 				const result: IScannedProfileExtension[] = [];
-				for (const extension of profileExtensions) {
-					if (extensions.some(([e]) => areSameExtensions(e.identifier, extension.identifier) && e.manifest.version !== extension.version)) {
+				for (const existing of existingExtensions) {
+					if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) {
 						// Remove the existing extension with different version
-						extensionsToRemove.push(extension);
+						extensionsToRemove.push(existing);
 					} else {
-						result.push(extension);
+						result.push(existing);
 					}
 				}
 				for (const [extension, metadata] of extensions) {
-					if (!result.some(e => areSameExtensions(e.identifier, extension.identifier) && e.version === extension.manifest.version)) {
-						// Add only if the same version of the extension is not already added
-						const extensionToAdd = { identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata };
+					const index = result.findIndex(e => areSameExtensions(e.identifier, extension.identifier) && e.version === extension.manifest.version);
+					const extensionToAdd = { identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata };
+					if (index === -1) {
 						extensionsToAdd.push(extensionToAdd);
 						result.push(extensionToAdd);
+					} else {
+						result.splice(index, 1, extensionToAdd);
 					}
 				}
 				if (extensionsToAdd.length) {
@@ -189,7 +191,6 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable
 
 	async removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise<void> {
 		const extensionsToRemove: IScannedProfileExtension[] = [];
-		this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation });
 		try {
 			await this.withProfileExtensions(profileLocation, profileExtensions => {
 				const result: IScannedProfileExtension[] = [];


Test patch:
diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
--- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
+++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
@@ -4,13 +4,14 @@
  *--------------------------------------------------------------------------------------------*/
 
 import * as assert from 'assert';
+import * as sinon from 'sinon';
 import { VSBuffer } from 'vs/base/common/buffer';
 import { DisposableStore } from 'vs/base/common/lifecycle';
 import { joinPath } from 'vs/base/common/resources';
 import { URI } from 'vs/base/common/uri';
 import { IEnvironmentService } from 'vs/platform/environment/common/environment';
-import { AbstractExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
-import { ExtensionType, IExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
+import { AbstractExtensionsProfileScannerService, ProfileExtensionsEvent } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
+import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
 import { FileService } from 'vs/platform/files/common/fileService';
 import { IFileService } from 'vs/platform/files/common/files';
 import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
@@ -47,6 +48,10 @@ suite('ExtensionsProfileScannerService', () => {
 		instantiationService.stub(IUserDataProfilesService, userDataProfilesService);
 	});
 
+	teardown(() => disposables.clear());
+
+	suiteTeardown(() => sinon.restore());
+
 	test('write extensions located in the same extensions folder', async () => {
 		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
 
@@ -442,7 +447,247 @@ suite('ExtensionsProfileScannerService', () => {
 		assert.deepStrictEqual(manifestContent, [{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version }]);
 	});
 
-	function aExtension(id: string, location: URI, e?: Partial<IExtension>): IExtension {
+	test('add extension trigger events', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+		const target1 = sinon.stub();
+		const target2 = sinon.stub();
+		testObject.onAddExtensions(target1);
+		testObject.onDidAddExtensions(target2);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+
+		assert.ok(target1.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].identifier, extension.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].version, extension.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].location.toString(), extension.location.toString());
+
+		assert.ok(target2.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].identifier, extension.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].version, extension.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].location.toString(), extension.location.toString());
+	});
+
+	test('remove extension trigger events', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+		const target1 = sinon.stub();
+		const target2 = sinon.stub();
+		testObject.onRemoveExtensions(target1);
+		testObject.onDidRemoveExtensions(target2);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+		await testObject.removeExtensionFromProfile(extension, extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.length, 0);
+
+		assert.ok(target1.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].identifier, extension.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].version, extension.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].location.toString(), extension.location.toString());
+
+		assert.ok(target2.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].identifier, extension.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].version, extension.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].location.toString(), extension.location.toString());
+	});
+
+	test('add extension with same id but different version', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+
+		const extension1 = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension1, undefined]], extensionsManifest);
+
+		const target1 = sinon.stub();
+		const target2 = sinon.stub();
+		const target3 = sinon.stub();
+		const target4 = sinon.stub();
+		testObject.onAddExtensions(target1);
+		testObject.onRemoveExtensions(target2);
+		testObject.onDidAddExtensions(target3);
+		testObject.onDidRemoveExtensions(target4);
+		const extension2 = aExtension('pub.a', joinPath(ROOT, 'pub.a-2.0.0'), undefined, { version: '2.0.0' });
+		await testObject.addExtensionsToProfile([[extension2, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: undefined }]);
+
+		assert.ok(target1.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].identifier, extension2.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].version, extension2.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString());
+
+		assert.ok(target2.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].identifier, extension1.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].version, extension1.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString());
+
+		assert.ok(target3.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].identifier, extension2.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].version, extension2.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString());
+
+		assert.ok(target4.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].identifier, extension1.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].version, extension1.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString());
+	});
+
+	test('add same extension', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+
+		const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const target1 = sinon.stub();
+		const target2 = sinon.stub();
+		const target3 = sinon.stub();
+		const target4 = sinon.stub();
+		testObject.onAddExtensions(target1);
+		testObject.onRemoveExtensions(target2);
+		testObject.onDidAddExtensions(target3);
+		testObject.onDidRemoveExtensions(target4);
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+		assert.ok(target1.notCalled);
+		assert.ok(target2.notCalled);
+		assert.ok(target3.notCalled);
+		assert.ok(target4.notCalled);
+	});
+
+	test('add same extension with different metadata', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+
+		const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const target1 = sinon.stub();
+		const target2 = sinon.stub();
+		const target3 = sinon.stub();
+		const target4 = sinon.stub();
+		testObject.onAddExtensions(target1);
+		testObject.onRemoveExtensions(target2);
+		testObject.onDidAddExtensions(target3);
+		testObject.onDidRemoveExtensions(target4);
+		await testObject.addExtensionsToProfile([[extension, { isApplicationScoped: true }]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON(), metadata: a.metadata })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: { isApplicationScoped: true } }]);
+		assert.ok(target1.notCalled);
+		assert.ok(target2.notCalled);
+		assert.ok(target3.notCalled);
+		assert.ok(target4.notCalled);
+	});
+
+	test('add extension with different version and metadata', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+
+		const extension1 = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension1, undefined]], extensionsManifest);
+		const extension2 = aExtension('pub.a', joinPath(ROOT, 'pub.a-2.0.0'), undefined, { version: '2.0.0' });
+
+		const target1 = sinon.stub();
+		const target2 = sinon.stub();
+		const target3 = sinon.stub();
+		const target4 = sinon.stub();
+		testObject.onAddExtensions(target1);
+		testObject.onRemoveExtensions(target2);
+		testObject.onDidAddExtensions(target3);
+		testObject.onDidRemoveExtensions(target4);
+		await testObject.addExtensionsToProfile([[extension2, { isApplicationScoped: true }]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON(), metadata: a.metadata })), [{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: { isApplicationScoped: true } }]);
+
+		assert.ok(target1.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].identifier, extension2.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].version, extension2.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString());
+
+		assert.ok(target2.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].identifier, extension1.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].version, extension1.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString());
+
+		assert.ok(target3.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].identifier, extension2.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].version, extension2.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target1.args[0][0])).extensions[0].location.toString(), extension2.location.toString());
+
+		assert.ok(target4.calledOnce);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString());
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions.length, 1);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].identifier, extension1.identifier);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].version, extension1.manifest.version);
+		assert.deepStrictEqual((<ProfileExtensionsEvent>(target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString());
+	});
+
+	test('add extension with same id and version located in the different folder', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+
+		let extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const target1 = sinon.stub();
+		const target2 = sinon.stub();
+		const target3 = sinon.stub();
+		const target4 = sinon.stub();
+		testObject.onAddExtensions(target1);
+		testObject.onRemoveExtensions(target2);
+		testObject.onDidAddExtensions(target3);
+		testObject.onDidRemoveExtensions(target4);
+		extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+		assert.ok(target1.notCalled);
+		assert.ok(target2.notCalled);
+		assert.ok(target3.notCalled);
+		assert.ok(target4.notCalled);
+	});
+
+	function aExtension(id: string, location: URI, e?: Partial<IExtension>, manifest?: Partial<IExtensionManifest>): IExtension {
 		return {
 			identifier: { id },
 			location,
@@ -454,6 +699,7 @@ suite('ExtensionsProfileScannerService', () => {
 				publisher: 'publisher',
 				version: '1.0.0',
 				engines: { vscode: '1.0.0' },
+				...manifest,
 			},
 			isValid: true,
 			validations: [],