Repository: microsoft/vscode (TypeScript) — Bug Fix

Issue:
Cannot enable Jupyter extension when connected to remote machine with ssh
Issue Type: <b>Bug</b>

1. Connect to a remote machine with the ssh extension
2. install the jupyter extension on the remote machine (I also have it installed locally)
3. disable the extension, reload the window, then re-enable

Result:
Error: `Cannot change enablement of Jupyter Notebook Renderers extension because of its extension kind`
The extension has to be uninstalled and reinstalled to get it to load again

VS Code version: Code - Insiders 1.69.0-insider (173f4bea73601a31859372b409cac2c20fa91194, 2022-06-24T06:32:08.615Z)
OS version: Windows_NT x64 10.0.22000
Restricted Mode: No
Remote OS version: Linux x64 5.13.0-1021-azure

<details>
<summary>System Info</summary>

|Item|Value|
|---|---|
|CPUs|Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz (8 x 1498)|
|GPU Status|2d_canvas: enabled<br>canvas_oop_rasterization: disabled_off<br>direct_rendering_display_compositor: disabled_off_ok<br>gpu_compositing: enabled<br>multiple_raster_threads: enabled_on<br>opengl: enabled_on<br>rasterization: enabled<br>raw_draw: disabled_off_ok<br>skia_renderer: enabled_on<br>video_decode: enabled<br>video_encode: enabled<br>vulkan: disabled_off<br>webgl: enabled<br>webgl2: enabled|
|Load (avg)|undefined|
|Memory (System)|31.59GB (11.66GB free)|
|Process Argv|--folder-uri file:///c%3A/src/test/3 --crash-reporter-id 1202dc88-403d-4ebc-8393-af3551a8bd5b|
|Screen Reader|no|
|VM|0%|

|Item|Value|
|---|---|
|Remote|SSH: 20.57.139.129|
|OS|Linux x64 5.13.0-1021-azure|
|CPUs|Intel(R) Xeon(R) CPU E5-2673 v3 @ 2.40GHz (1 x 2394)|
|Memory (System)|3.34GB (0.19GB free)|
|VM|0%|
</details><details><summary>Extensions (10)</summary>

Extension|Author (truncated)|Version
---|---|---
Bookmarks|ale|13.3.0
tsl-problem-matcher|amo|0.6.2
vscode-language-pack-de|MS-|1.69.6220922
jupyter-keymap|ms-|1.0.0
remote-containers|ms-|0.240.0
remote-ssh|ms-|0.83.2022062315
remote-wsl|ms-|0.66.3
jupyter|ms-|2022.6.1001771053
jupyter-keymap|ms-|1.0.0
jupyter-renderers|ms-|1.0.8


</details><details>
<summary>A/B Experiments</summary>

```
vsliv695:30137379
vsins829:30139715
vsliv368cf:30146710
vsreu685:30147344
python383cf:30185419
vspor879:30202332
vspor708:30202333
vspor363:30204092
vstes627:30244334
vslsvsres303:30308271
pythonvspyl392:30422396
pythontb:30258533
pythonptprofiler:30281269
vshan820:30294714
pythondataviewer:30285072
vscod805cf:30301675
bridge0708:30335490
bridge0723:30353136
vsaa593cf:30376535
pythonvs932:30404738
wslgetstarted:30449409
vscscmwlcmt:30465136
cppdebug:30492333
pylanb8912:30496796
vsclangdf:30492506
841h4636:30516487

```

</details>

<!-- generated by issue reporter -->


Discussion:

Resolving patch:
diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts
--- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts
+++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts
@@ -147,7 +147,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
 			throw new Error(localize('cannot change enablement environment', "Cannot change enablement of {0} extension because it is enabled in environment", extension.manifest.displayName || extension.identifier.id));
 		}
 
-		switch (this.getEnablementState(extension)) {
+		this.throwErrorIfEnablementStateCannotBeChanged(extension, this.getEnablementState(extension), donotCheckDependencies);
+	}
+
+	private throwErrorIfEnablementStateCannotBeChanged(extension: IExtension, enablementStateOfExtension: EnablementState, donotCheckDependencies?: boolean): void {
+		switch (enablementStateOfExtension) {
 			case EnablementState.DisabledByEnvironment:
 				throw new Error(localize('cannot change disablement environment', "Cannot change enablement of {0} extension because it is disabled in environment", extension.manifest.displayName || extension.identifier.id));
 			case EnablementState.DisabledByVirtualWorkspace:
@@ -219,30 +223,60 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
 	}
 
 	private getExtensionsToEnableRecursively(extensions: IExtension[], allExtensions: ReadonlyArray<IExtension>, enablementState: EnablementState, options: { dependencies: boolean; pack: boolean }, checked: IExtension[] = []): IExtension[] {
+		if (!options.dependencies && !options.pack) {
+			return [];
+		}
+
 		const toCheck = extensions.filter(e => checked.indexOf(e) === -1);
-		if (toCheck.length) {
-			for (const extension of toCheck) {
-				checked.push(extension);
+		if (!toCheck.length) {
+			return [];
+		}
+
+		for (const extension of toCheck) {
+			checked.push(extension);
+		}
+
+		const extensionsToDisable: IExtension[] = [];
+		for (const extension of allExtensions) {
+			// Extension is already checked
+			if (checked.some(e => areSameExtensions(e.identifier, extension.identifier))) {
+				continue;
 			}
-			const extensionsToDisable = allExtensions.filter(i => {
-				if (checked.indexOf(i) !== -1) {
-					return false;
+
+			const enablementStateOfExtension = this.getEnablementState(extension);
+			// Extension enablement state is same as the end enablement state
+			if (enablementStateOfExtension === enablementState) {
+				continue;
+			}
+
+			// Check if the extension is a dependency or in extension pack
+			if (extensions.some(e =>
+				(options.dependencies && e.manifest.extensionDependencies?.some(id => areSameExtensions({ id }, extension.identifier)))
+				|| (options.pack && e.manifest.extensionPack?.some(id => areSameExtensions({ id }, extension.identifier))))) {
+
+				const index = extensionsToDisable.findIndex(e => areSameExtensions(e.identifier, extension.identifier));
+
+				// Extension is not aded to the disablement list so add it
+				if (index === -1) {
+					extensionsToDisable.push(extension);
 				}
-				if (this.getEnablementState(i) === enablementState) {
-					return false;
+
+				// Extension is there already in the disablement list.
+				else {
+					try {
+						// Replace only if the enablement state can be changed
+						this.throwErrorIfEnablementStateCannotBeChanged(extension, enablementStateOfExtension, true);
+						extensionsToDisable.splice(index, 1, extension);
+					} catch (error) { /*Do not add*/ }
 				}
-				return (options.dependencies || options.pack)
-					&& extensions.some(extension =>
-						(options.dependencies && extension.manifest.extensionDependencies?.some(id => areSameExtensions({ id }, i.identifier)))
-						|| (options.pack && extension.manifest.extensionPack?.some(id => areSameExtensions({ id }, i.identifier)))
-					);
-			});
-			if (extensionsToDisable.length) {
-				extensionsToDisable.push(...this.getExtensionsToEnableRecursively(extensionsToDisable, allExtensions, enablementState, options, checked));
 			}
-			return extensionsToDisable;
 		}
-		return [];
+
+		if (extensionsToDisable.length) {
+			extensionsToDisable.push(...this.getExtensionsToEnableRecursively(extensionsToDisable, allExtensions, enablementState, options, checked));
+		}
+
+		return extensionsToDisable;
 	}
 
 	private _setUserEnablementState(extension: IExtension, newState: EnablementState): Promise<boolean> {


Test patch:
diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts
--- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts
+++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts
@@ -624,6 +624,38 @@ suite('ExtensionEnablementService Test', () => {
 		assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByVirtualWorkspace);
 	});
 
+	test('test enable a remote workspace extension and local ui extension that is a dependency of remote', async () => {
+		instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), null));
+		const localUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`) });
+		const remoteUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) });
+		const target = aLocalExtension2('pub.b', { main: 'main.js', extensionDependencies: ['pub.a'] }, { location: URI.file(`pub.b`).with({ scheme: 'vscode-remote' }) });
+		testObject = new TestExtensionEnablementService(instantiationService);
+
+		installed.push(localUIExtension, remoteUIExtension, target);
+		await testObject.setEnablement([target, localUIExtension], EnablementState.DisabledGlobally);
+		await testObject.setEnablement([target, localUIExtension], EnablementState.EnabledGlobally);
+		assert.ok(testObject.isEnabled(target));
+		assert.ok(testObject.isEnabled(localUIExtension));
+		assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally);
+		assert.strictEqual(testObject.getEnablementState(localUIExtension), EnablementState.EnabledGlobally);
+	});
+
+	test('test enable a remote workspace extension also enables its dependency in local', async () => {
+		instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), null));
+		const localUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`) });
+		const remoteUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) });
+		const target = aLocalExtension2('pub.b', { main: 'main.js', extensionDependencies: ['pub.a'] }, { location: URI.file(`pub.b`).with({ scheme: 'vscode-remote' }) });
+		testObject = new TestExtensionEnablementService(instantiationService);
+
+		installed.push(localUIExtension, remoteUIExtension, target);
+		await testObject.setEnablement([target, localUIExtension], EnablementState.DisabledGlobally);
+		await testObject.setEnablement([target], EnablementState.EnabledGlobally);
+		assert.ok(testObject.isEnabled(target));
+		assert.ok(testObject.isEnabled(localUIExtension));
+		assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally);
+		assert.strictEqual(testObject.getEnablementState(localUIExtension), EnablementState.EnabledGlobally);
+	});
+
 	test('test canChangeEnablement return false when extension is disabled in virtual workspace', () => {
 		const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } });
 		instantiationService.stub(IWorkspaceContextService, 'getWorkspace', <IWorkspace>{ folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] });