Skip to content

Commit 4b9463a

Browse files
authored
Use extensionEnablement service and check for canInstall (microsoft#178094)
1 parent 2c68ba8 commit 4b9463a

File tree

1 file changed

+83
-89
lines changed

1 file changed

+83
-89
lines changed

src/vs/workbench/contrib/remote/browser/remoteStartEntry.ts

Lines changed: 83 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ import { once } from 'vs/base/common/functional';
1212
import { IProductService } from 'vs/platform/product/common/productService';
1313
import { Action2, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
1414
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
15-
import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionManagementService, isTargetPlatformCompatible } from 'vs/platform/extensionManagement/common/extensionManagement';
15+
import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
1616
import { retry } from 'vs/base/common/async';
1717
import { Registry } from 'vs/platform/registry/common/platform';
1818
import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
1919
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
2020
import { CancellationToken } from 'vs/base/common/cancellation';
2121
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2222
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
23-
import { TargetPlatform } from 'vs/platform/extensions/common/extensions';
23+
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
24+
import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
2425

2526
const STATUSBAR_REMOTEINDICATOR_CONTRIBUTION = 'statusBar/remoteIndicator';
2627

@@ -36,28 +37,33 @@ type RemoteStartActionEvent = {
3637
remoteExtensionId?: string;
3738
};
3839

40+
interface RemoteCommand {
41+
command: string;
42+
commandContext: string | undefined;
43+
}
44+
3945
interface RemoteExtensionMetadata {
4046
id: string;
4147
friendlyName: string;
42-
remoteCommands: string[];
48+
remoteCommands: RemoteCommand[];
4349
installed: boolean;
44-
dependenciesStr: string;
45-
isPlatformCompatible: boolean | undefined;
50+
dependencies: string[];
51+
isPlatformCompatible: boolean;
4652
}
4753

4854
export class RemoteStartEntry extends Disposable implements IWorkbenchContribution {
4955

5056
private static readonly REMOTE_START_ENTRY_ACTIONS_COMMAND_ID = 'workbench.action.remote.showStartEntryActions';
5157
private readonly remoteExtensionMetadata: RemoteExtensionMetadata[];
5258
private _isInitialized: boolean = false;
53-
private targetPlatform: TargetPlatform = TargetPlatform.UNKNOWN;
5459

5560
constructor(
5661
@IQuickInputService private readonly quickInputService: IQuickInputService,
5762
@ICommandService private readonly commandService: ICommandService,
5863
@IProductService private readonly productService: IProductService,
5964
@IExtensionService private readonly extensionService: IExtensionService,
6065
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
66+
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
6167
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
6268
@ITelemetryService private readonly telemetryService: ITelemetryService,
6369
@IContextKeyService private readonly contextKeyService: IContextKeyService) {
@@ -66,7 +72,7 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi
6672
registerConfiguration(this.productService.quality !== 'stable');
6773
const remoteExtensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips };
6874
this.remoteExtensionMetadata = Object.values(remoteExtensionTips).filter(value => value.showInStartEntry === true).map(value => {
69-
return { id: value.extensionId, installed: false, friendlyName: value.friendlyName, remoteCommands: [], isPlatformCompatible: undefined, dependenciesStr: '' };
75+
return { id: value.extensionId, installed: false, friendlyName: value.friendlyName, remoteCommands: [], isPlatformCompatible: false, dependencies: [] };
7076
});
7177

7278
this.registerActions();
@@ -95,66 +101,69 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi
95101
}
96102

97103
private registerListeners(): void {
98-
99104
this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => {
100105
for (const ext of result) {
101-
await this.updateInstallStatus(ext.identifier.id, true);
106+
const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, ext.identifier.id));
107+
if (index > -1) {
108+
this.remoteExtensionMetadata[index].installed = true;
109+
this.remoteExtensionMetadata[index].remoteCommands = await this.getRemoteCommands(ext.identifier.id);
110+
}
102111
}
103112
}));
104113

105114
this._register(this.extensionManagementService.onDidUninstallExtension(async (result) => {
106-
await this.updateInstallStatus(result.identifier.id, false);
115+
const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, result.identifier.id));
116+
if (index > -1) {
117+
this.remoteExtensionMetadata[index].installed = false;
118+
}
107119
}));
108-
}
109120

121+
this._register(this.extensionEnablementService.onEnablementChanged(async (result) => {
122+
for (const ext of result) {
123+
const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, ext.identifier.id));
124+
if (index > -1) {
125+
// update remote commands for extension if we never fetched it when it was disabled.
126+
if (this.extensionEnablementService.isEnabled(ext) && this.remoteExtensionMetadata[index].remoteCommands.length === 0) {
127+
this.remoteExtensionMetadata[index].remoteCommands = await this.getRemoteCommands(ext.identifier.id);
128+
}
129+
}
130+
}
131+
}));
132+
}
110133

111134
private async _init(): Promise<void> {
112135
if (this._isInitialized) {
113136
return;
114137
}
115138

116-
this.targetPlatform = await this.extensionManagementService.getTargetPlatform();
117139
for (let i = 0; i < this.remoteExtensionMetadata.length; i++) {
118-
const installed = this.extensionService.extensions.some((e) => e.id?.toLowerCase() === this.remoteExtensionMetadata[i].id);
119-
if (installed) {
120-
await this.updateInstallStatus(this.remoteExtensionMetadata[i].id, true);
140+
const extensionId = this.remoteExtensionMetadata[i].id;
141+
142+
// Update compatibility
143+
const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: extensionId }], CancellationToken.None))[0];
144+
if (!await this.extensionManagementService.canInstall(galleryExtension)) {
145+
this.remoteExtensionMetadata[i].isPlatformCompatible = false;
121146
}
122147
else {
123-
await this.updateInstallStatus(this.remoteExtensionMetadata[i].id, false);
148+
this.remoteExtensionMetadata[i].isPlatformCompatible = true;
149+
this.remoteExtensionMetadata[i].dependencies = galleryExtension.properties.extensionPack ?? [];
124150
}
125-
}
126-
this._isInitialized = true;
127-
}
128151

129-
private async updateInstallStatus(extensionId: string, installed: boolean): Promise<RemoteExtensionMetadata | undefined> {
130-
const index = this.remoteExtensionMetadata.findIndex(value => value.id === extensionId);
131-
if (index > -1) {
152+
// Check if installed and enabled
153+
const installed = (await this.extensionManagementService.getInstalled()).find(value => ExtensionIdentifier.equals(value.identifier.id, extensionId));
132154
if (installed) {
133-
const commands = await this.getRemoteCommands(extensionId);
134-
if (commands) {
135-
this.remoteExtensionMetadata[index].remoteCommands = commands;
136-
this.remoteExtensionMetadata[index].isPlatformCompatible = true;
137-
} else {
138-
this.remoteExtensionMetadata[index].isPlatformCompatible = false;
139-
}
140-
this.remoteExtensionMetadata[index].installed = true;
141-
} else if (!installed) {
142-
const dependenciesStr = await this.getDependenciesFromGallery(this.remoteExtensionMetadata[index].id);
143-
if (dependenciesStr !== undefined) {
144-
this.remoteExtensionMetadata[index].dependenciesStr = dependenciesStr;
145-
this.remoteExtensionMetadata[index].isPlatformCompatible = true;
146-
}
147-
else {
148-
this.remoteExtensionMetadata[index].isPlatformCompatible = false;
155+
this.remoteExtensionMetadata[i].installed = true;
156+
if (this.extensionEnablementService.isEnabled(installed)) {
157+
// update commands if enabled
158+
this.remoteExtensionMetadata[i].remoteCommands = await this.getRemoteCommands(extensionId);
149159
}
150-
this.remoteExtensionMetadata[index].installed = false;
151160
}
152-
return this.remoteExtensionMetadata[index];
153161
}
154-
return undefined;
162+
163+
this._isInitialized = true;
155164
}
156165

157-
private async getRemoteCommands(remoteExtensionId: string): Promise<string[] | undefined> {
166+
private async getRemoteCommands(remoteExtensionId: string): Promise<RemoteCommand[]> {
158167

159168
const extension = await retry(async () => {
160169
const ext = await this.extensionService.getExtension(remoteExtensionId);
@@ -165,22 +174,21 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi
165174
}, 300, 10);
166175

167176
const menus = extension?.contributes?.menus;
168-
if (menus) {
169-
const commands: string[] = [];
170-
for (const contextMenu in menus) {
171-
// The remote start entry pulls the first command from the statusBar/remoteIndicator menu contribution
172-
if (contextMenu === STATUSBAR_REMOTEINDICATOR_CONTRIBUTION) {
173-
for (const command of menus[contextMenu]) {
174-
const expression = ContextKeyExpr.deserialize(command.when);
175-
if (this.contextKeyService.contextMatchesRules(expression)) {
176-
commands.push(command.command);
177-
}
178-
}
177+
if (!menus) {
178+
throw Error('Failed to find remoteIndicator menu');
179+
}
180+
181+
const commands: RemoteCommand[] = [];
182+
for (const contextMenu in menus) {
183+
// The remote start entry pulls the first command from the statusBar/remoteIndicator menu contribution
184+
if (contextMenu === STATUSBAR_REMOTEINDICATOR_CONTRIBUTION) {
185+
for (const command of menus[contextMenu]) {
186+
commands.push({ command: command.command, commandContext: command.when });
179187
}
180188
}
181-
return commands;
182189
}
183-
return undefined;
190+
191+
return commands;
184192
}
185193

186194
private async showRemoteStartActions() {
@@ -193,26 +201,31 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi
193201
if (metadata.installed && metadata.remoteCommands) {
194202
installedItems.push({ type: 'separator', label: metadata.friendlyName });
195203
for (const command of metadata.remoteCommands) {
196-
const commandAction = MenuRegistry.getCommand(command);
204+
205+
const expression = ContextKeyExpr.deserialize(command.commandContext);
206+
if (!this.contextKeyService.contextMatchesRules(expression)) {
207+
continue;
208+
}
209+
210+
const commandAction = MenuRegistry.getCommand(command.command);
197211
const label = typeof commandAction?.title === 'string' ? commandAction.title : commandAction?.title?.value;
198212
if (label) {
199213
installedItems.push({
200214
type: 'item',
201215
label: label,
202-
id: command
216+
id: command.command
203217
});
204218
}
205219
}
206220
}
207221
else if (!metadata.installed && metadata.isPlatformCompatible) {
208222
const label = nls.localize('remote.startActions.connectTo', 'Connect to {0}... ', metadata.friendlyName);
209-
const tooltip = metadata.dependenciesStr ? nls.localize('remote.startActions.tooltip', 'Also installs dependencies - {0} ', metadata.dependenciesStr) : '';
210-
notInstalledItems.push({ type: 'item', id: metadata.id, label: label, tooltip: tooltip });
223+
notInstalledItems.push({ type: 'item', id: metadata.id, label: label });
211224
}
212225
}
213226

214227
installedItems.push({
215-
type: 'separator', label: nls.localize('remote.startActions.downloadAndInstall', 'Download and Install')
228+
type: 'separator', label: nls.localize('remote.startActions.install', 'Install')
216229
});
217230
return installedItems.concat(notInstalledItems);
218231
};
@@ -223,16 +236,15 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi
223236
quickPick.sortByLabel = false;
224237
quickPick.canSelectMany = false;
225238
quickPick.ignoreFocusOut = false;
226-
quickPick.busy = false;
227239
once(quickPick.onDidAccept)(async () => {
228240

229241
const selectedItems = quickPick.selectedItems;
230242
if (selectedItems.length === 1) {
231243
const selectedItem = selectedItems[0].id!;
232-
233-
const remoteExtension = this.remoteExtensionMetadata.find(value => value.id === selectedItem);
244+
quickPick.busy = true;
245+
const remoteExtension = this.remoteExtensionMetadata.find(value => ExtensionIdentifier.equals(value.id, selectedItem));
234246
if (remoteExtension) {
235-
quickPick.busy = true;
247+
236248
quickPick.placeholder = nls.localize('remote.startActions.installingExtension', 'Installing extension... ');
237249

238250
const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: selectedItem }], CancellationToken.None))[0];
@@ -243,14 +255,15 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi
243255
});
244256

245257
this.telemetryService.publicLog2<RemoteStartActionEvent, RemoteStartActionClassification>('remoteStartList.ActionExecuted', { command: 'workbench.extensions.installExtension', remoteExtensionId: selectedItem });
258+
const commands = await this.getRemoteCommands(selectedItem);
246259

247-
quickPick.busy = false;
260+
await this.extensionService.activateByEvent(`onCommand:${commands[0]}`);
248261

249-
const metadata = await this.updateInstallStatus(selectedItem, true);
250-
if (metadata) {
251-
this.telemetryService.publicLog2<RemoteStartActionEvent, RemoteStartActionClassification>('remoteStartList.ActionExecuted', { command: metadata?.remoteCommands[0], remoteExtensionId: metadata?.id });
252-
this.commandService.executeCommand(metadata?.remoteCommands[0]);
253-
}
262+
const command = commands[0].command;
263+
this.commandService.executeCommand(command);
264+
265+
this.telemetryService.publicLog2<RemoteStartActionEvent, RemoteStartActionClassification>('remoteStartList.ActionExecuted', { command: command, remoteExtensionId: selectedItem });
266+
quickPick.busy = false;
254267
}
255268
else {
256269
this.commandService.executeCommand(selectedItem);
@@ -260,25 +273,6 @@ export class RemoteStartEntry extends Disposable implements IWorkbenchContributi
260273
});
261274
quickPick.show();
262275
}
263-
264-
private async getDependenciesFromGallery(extensionId: string): Promise<string | undefined> {
265-
266-
const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: extensionId }], CancellationToken.None))[0];
267-
const canInstall = galleryExtension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, galleryExtension.allTargetPlatforms, this.targetPlatform));
268-
if (!canInstall) {
269-
return undefined;
270-
}
271-
272-
const friendlyNames: string[] = [];
273-
if (galleryExtension.properties.extensionPack) {
274-
for (const extensionPackItem of galleryExtension.properties.extensionPack) {
275-
const extensionPack = (await this.extensionGalleryService.getExtensions([{ id: extensionPackItem }], CancellationToken.None))[0];
276-
friendlyNames.push(extensionPack.displayName);
277-
}
278-
}
279-
280-
return friendlyNames.join(', ');
281-
}
282276
}
283277

284278
function registerConfiguration(enabled: boolean): void {

0 commit comments

Comments
 (0)