Skip to content

Commit 1602ab8

Browse files
authored
Unify remote menu and start entry items for desktop (microsoft#185908)
* Unify remote menu and start entry items for desktop * Clean up code * Move remoteStartEntry to be web workbench contrib
1 parent 511849e commit 1602ab8

File tree

6 files changed

+215
-373
lines changed

6 files changed

+215
-373
lines changed

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

Lines changed: 164 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
77
import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme';
88
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
99
import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from 'vs/workbench/services/remote/common/remoteAgentService';
10-
import { RunOnceScheduler } from 'vs/base/common/async';
10+
import { RunOnceScheduler, retry } from 'vs/base/common/async';
1111
import { Event } from 'vs/base/common/event';
1212
import { Disposable, dispose } from 'vs/base/common/lifecycle';
1313
import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions';
@@ -18,12 +18,12 @@ import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/c
1818
import { ICommandService } from 'vs/platform/commands/common/commands';
1919
import { Schemas } from 'vs/base/common/network';
2020
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
21-
import { QuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
21+
import { QuickPickItem, IQuickInputService, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput';
2222
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
2323
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
2424
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
2525
import { IHostService } from 'vs/workbench/services/host/browser/host';
26-
import { PlatformToString, isWeb, platform } from 'vs/base/common/platform';
26+
import { PlatformName, PlatformToString, isWeb, platform } from 'vs/base/common/platform';
2727
import { once } from 'vs/base/common/functional';
2828
import { truncate } from 'vs/base/common/strings';
2929
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@@ -32,7 +32,7 @@ import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtua
3232
import { getCodiconAriaLabel } from 'vs/base/common/iconLabels';
3333
import { ILogService } from 'vs/platform/log/common/log';
3434
import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions';
35-
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
35+
import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
3636
import { IExtensionsViewPaneContainer, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
3737
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
3838
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
@@ -46,6 +46,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
4646
import { IProductService } from 'vs/platform/product/common/productService';
4747
import { DomEmitter } from 'vs/base/browser/event';
4848
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
49+
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
50+
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
51+
import { ThemeIcon } from 'vs/base/common/themables';
52+
import { infoIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
53+
import { IOpenerService } from 'vs/platform/opener/common/opener';
54+
import { URI } from 'vs/base/common/uri';
4955

5056
export const STATUS_BAR_OFFLINE_BACKGROUND = registerColor('statusBar.offlineBackground', {
5157
dark: '#6c1717',
@@ -62,6 +68,19 @@ export const STATUS_BAR_OFFLINE_FOREGROUND = registerColor('statusBar.offlineFor
6268
}, nls.localize('statusBarOfflineForeground', "Status bar foreground color when the workbench is offline. The status bar is shown in the bottom of the window"));
6369

6470
type ActionGroup = [string, Array<MenuItemAction | SubmenuItemAction>];
71+
72+
interface RemoteExtensionMetadata {
73+
id: string;
74+
installed: boolean;
75+
dependencies: string[];
76+
isPlatformCompatible: boolean;
77+
helpLink: string;
78+
startConnectLabel: string;
79+
startCommand: string;
80+
priority: number;
81+
supportedPlatforms?: PlatformName[];
82+
}
83+
6584
export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution {
6685

6786
private static readonly REMOTE_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu';
@@ -93,7 +112,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
93112
private measureNetworkConnectionLatencyScheduler: RunOnceScheduler | undefined = undefined;
94113

95114
private loggedInvalidGroupNames: { [group: string]: boolean } = Object.create(null);
96-
115+
private readonly remoteExtensionMetadata: RemoteExtensionMetadata[];
116+
private _isInitialized: boolean = false;
97117
constructor(
98118
@IStatusbarService private readonly statusbarService: IStatusbarService,
99119
@IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,
@@ -111,9 +131,29 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
111131
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
112132
@ITelemetryService private readonly telemetryService: ITelemetryService,
113133
@IProductService private readonly productService: IProductService,
134+
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
135+
@IOpenerService private readonly openerService: IOpenerService,
114136
) {
115137
super();
116138

139+
const remoteExtensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips };
140+
this.remoteExtensionMetadata = Object.values(remoteExtensionTips).filter(value => value.startEntry !== undefined).map(value => {
141+
return {
142+
id: value.extensionId,
143+
installed: false,
144+
friendlyName: value.friendlyName,
145+
isPlatformCompatible: false,
146+
dependencies: [],
147+
helpLink: value.startEntry?.helpLink ?? '',
148+
startConnectLabel: value.startEntry?.startConnectLabel ?? '',
149+
startCommand: value.startEntry?.startCommand ?? '',
150+
priority: value.startEntry?.priority ?? 10,
151+
supportedPlatforms: value.supportedPlatforms
152+
};
153+
});
154+
155+
this.remoteExtensionMetadata.sort((ext1, ext2) => ext1.priority - ext2.priority);
156+
117157
// Set initial connection state
118158
if (this.remoteAuthority) {
119159
this.connectionState = 'initializing';
@@ -127,6 +167,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
127167

128168
this.updateWhenInstalledExtensionsRegistered();
129169
this.updateRemoteStatusIndicator();
170+
this.initializeRemoteMetadata();
130171
}
131172

132173
private registerActions(): void {
@@ -253,6 +294,53 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
253294
this._register(new DomEmitter(window, 'offline')).event
254295
)(() => this.setNetworkState(navigator.onLine ? 'online' : 'offline')));
255296
}
297+
298+
this._register(this.extensionService.onDidChangeExtensions(async (result) => {
299+
for (const ext of result.added) {
300+
const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, ext.identifier));
301+
if (index > -1) {
302+
this.remoteExtensionMetadata[index].installed = true;
303+
}
304+
}
305+
}));
306+
307+
this._register(this.extensionManagementService.onDidUninstallExtension(async (result) => {
308+
const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, result.identifier.id));
309+
if (index > -1) {
310+
this.remoteExtensionMetadata[index].installed = false;
311+
}
312+
}));
313+
}
314+
315+
private async initializeRemoteMetadata(): Promise<void> {
316+
317+
if (this._isInitialized) {
318+
return;
319+
}
320+
321+
const currentPlatform = PlatformToString(platform);
322+
for (let i = 0; i < this.remoteExtensionMetadata.length; i++) {
323+
const extensionId = this.remoteExtensionMetadata[i].id;
324+
const supportedPlatforms = this.remoteExtensionMetadata[i].supportedPlatforms;
325+
// Update compatibility
326+
const token = new CancellationTokenSource();
327+
const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: extensionId }], token.token))[0];
328+
if (!await this.extensionManagementService.canInstall(galleryExtension)) {
329+
this.remoteExtensionMetadata[i].isPlatformCompatible = false;
330+
}
331+
else if (supportedPlatforms && !supportedPlatforms.includes(currentPlatform)) {
332+
this.remoteExtensionMetadata[i].isPlatformCompatible = false;
333+
}
334+
else {
335+
this.remoteExtensionMetadata[i].isPlatformCompatible = true;
336+
this.remoteExtensionMetadata[i].dependencies = galleryExtension.properties.extensionPack ?? [];
337+
}
338+
339+
// Check if installed and enabled
340+
this.remoteExtensionMetadata[i].installed = (await this.extensionManagementService.getInstalled()).find(value => ExtensionIdentifier.equals(value.identifier.id, extensionId)) ? true : false;
341+
}
342+
343+
this._isInitialized = true;
256344
}
257345

258346
private updateVirtualWorkspaceLocation() {
@@ -547,6 +635,32 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
547635
return markdownTooltip;
548636
}
549637

638+
private async installAndRunStartCommand(metadata: RemoteExtensionMetadata) {
639+
const extensionId = metadata.id;
640+
const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: extensionId }], CancellationToken.None))[0];
641+
642+
await this.extensionManagementService.installFromGallery(galleryExtension, {
643+
isMachineScoped: false,
644+
donotIncludePackAndDependencies: false,
645+
context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true }
646+
});
647+
648+
await retry(async () => {
649+
const ext = await this.extensionService.getExtension(metadata.id);
650+
if (!ext) {
651+
throw Error('Failed to find installed remote extension');
652+
}
653+
return ext;
654+
}, 300, 10);
655+
this.commandService.executeCommand(metadata.startCommand);
656+
657+
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
658+
id: 'remoteInstallAndRun',
659+
detail: extensionId,
660+
from: 'remote indicator'
661+
});
662+
}
663+
550664
private showRemoteMenu() {
551665
const getCategoryLabel = (action: MenuItemAction) => {
552666
if (action.item.category) {
@@ -642,19 +756,31 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
642756
}
643757
}
644758

645-
if (this.extensionGalleryService.isEnabled() && this.hasAdditionalRemoteExtensions()) {
646-
items.push({
647-
id: RemoteStatusIndicator.INSTALL_REMOTE_EXTENSIONS_ID,
648-
label: nls.localize('installRemotes', "Install Additional Remote Extensions..."),
649-
650-
alwaysShow: true
651-
});
652-
}
653-
654759
if (items.length === entriesBeforeConfig) {
655760
items.pop(); // remove the separator again
656761
}
657762

763+
if (this.extensionGalleryService.isEnabled()) {
764+
765+
const notInstalledItems: QuickPickItem[] = [];
766+
for (const metadata of this.remoteExtensionMetadata) {
767+
if (!metadata.installed && metadata.isPlatformCompatible) {
768+
// Create Install QuickPick with a help link
769+
const label = metadata.startConnectLabel;
770+
const buttons: IQuickInputButton[] = [{
771+
iconClass: ThemeIcon.asClassName(infoIcon),
772+
tooltip: nls.localize('remote.startActions.help', "Learn More")
773+
}];
774+
notInstalledItems.push({ type: 'item', id: metadata.id, label: label, buttons: buttons });
775+
}
776+
}
777+
778+
items.push({
779+
type: 'separator', label: nls.localize('remote.startActions.install', 'Install')
780+
});
781+
items.push(...notInstalledItems);
782+
}
783+
658784
return items;
659785
};
660786

@@ -663,20 +789,36 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
663789
quickPick.items = computeItems();
664790
quickPick.sortByLabel = false;
665791
quickPick.canSelectMany = false;
666-
once(quickPick.onDidAccept)((_ => {
792+
once(quickPick.onDidAccept)((async _ => {
667793
const selectedItems = quickPick.selectedItems;
668794
if (selectedItems.length === 1) {
669795
const commandId = selectedItems[0].id!;
670-
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
671-
id: commandId,
672-
from: 'remote indicator'
673-
});
674-
this.commandService.executeCommand(commandId);
796+
const remoteExtension = this.remoteExtensionMetadata.find(value => ExtensionIdentifier.equals(value.id, commandId));
797+
if (remoteExtension) {
798+
quickPick.items = [];
799+
quickPick.busy = true;
800+
quickPick.placeholder = nls.localize('remote.startActions.installingExtension', 'Installing extension... ');
801+
quickPick.hide();
802+
await this.installAndRunStartCommand(remoteExtension);
803+
}
804+
else {
805+
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
806+
id: commandId,
807+
from: 'remote indicator'
808+
});
809+
this.commandService.executeCommand(commandId);
810+
}
811+
quickPick.hide();
675812
}
676-
677-
quickPick.hide();
678813
}));
679814

815+
once(quickPick.onDidTriggerItemButton)(async (e) => {
816+
const remoteExtension = this.remoteExtensionMetadata.find(value => ExtensionIdentifier.equals(value.id, e.item.id));
817+
if (remoteExtension) {
818+
await this.openerService.open(URI.parse(remoteExtension.helpLink));
819+
}
820+
});
821+
680822
// refresh the items when actions change
681823
const legacyItemUpdater = this.legacyIndicatorMenu.onDidChange(() => quickPick.items = computeItems());
682824
quickPick.onDidHide(legacyItemUpdater.dispose);
@@ -687,21 +829,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
687829
quickPick.show();
688830
}
689831

690-
private hasAdditionalRemoteExtensions() {
691-
const extensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips };
692-
const currentPlatform = PlatformToString(platform);
693-
for (const extension of Object.values(extensionTips)) {
694-
const { extensionId: recommendedExtensionId, supportedPlatforms } = extension;
695-
if (!supportedPlatforms || supportedPlatforms.includes(currentPlatform)) {
696-
// if this recommended extension isn't already installed, return early
697-
if (!this.extensionService.extensions.some((extension) => extension.id?.toLowerCase() === recommendedExtensionId.toLowerCase())) {
698-
return true;
699-
}
700-
}
701-
}
702-
return false;
703-
}
704-
705832
private hasRemoteMenuCommands(ignoreInstallAdditional: boolean): boolean {
706833
if (this.remoteAuthority !== undefined || this.virtualWorkspaceLocation !== undefined) {
707834
if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Registry } from 'vs/platform/registry/common/platform';
7+
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
8+
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
9+
import { RemoteStartEntry } from 'vs/workbench/contrib/remote/browser/remoteStartEntry';
10+
11+
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
12+
.registerWorkbenchContribution(RemoteStartEntry, LifecyclePhase.Restored);

0 commit comments

Comments
 (0)