Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify remote menu and start entry items for desktop #185908

Merged
merged 3 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 164 additions & 37 deletions src/vs/workbench/contrib/remote/browser/remoteIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from 'vs/workbench/services/remote/common/remoteAgentService';
import { RunOnceScheduler } from 'vs/base/common/async';
import { RunOnceScheduler, retry } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions';
Expand All @@ -18,12 +18,12 @@ import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/c
import { ICommandService } from 'vs/platform/commands/common/commands';
import { Schemas } from 'vs/base/common/network';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { QuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { QuickPickItem, IQuickInputService, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput';
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { PlatformToString, isWeb, platform } from 'vs/base/common/platform';
import { PlatformName, PlatformToString, isWeb, platform } from 'vs/base/common/platform';
import { once } from 'vs/base/common/functional';
import { truncate } from 'vs/base/common/strings';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
Expand All @@ -32,7 +32,7 @@ import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtua
import { getCodiconAriaLabel } from 'vs/base/common/iconLabels';
import { ILogService } from 'vs/platform/log/common/log';
import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions';
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionsViewPaneContainer, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
Expand All @@ -46,6 +46,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IProductService } from 'vs/platform/product/common/productService';
import { DomEmitter } from 'vs/base/browser/event';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { ThemeIcon } from 'vs/base/common/themables';
import { infoIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { URI } from 'vs/base/common/uri';

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

type ActionGroup = [string, Array<MenuItemAction | SubmenuItemAction>];

interface RemoteExtensionMetadata {
id: string;
installed: boolean;
dependencies: string[];
isPlatformCompatible: boolean;
helpLink: string;
startConnectLabel: string;
startCommand: string;
priority: number;
supportedPlatforms?: PlatformName[];
}

export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution {

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

private loggedInvalidGroupNames: { [group: string]: boolean } = Object.create(null);

private readonly remoteExtensionMetadata: RemoteExtensionMetadata[];
private _isInitialized: boolean = false;
constructor(
@IStatusbarService private readonly statusbarService: IStatusbarService,
@IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,
Expand All @@ -111,9 +131,29 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IProductService private readonly productService: IProductService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IOpenerService private readonly openerService: IOpenerService,
) {
super();

const remoteExtensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips };
this.remoteExtensionMetadata = Object.values(remoteExtensionTips).filter(value => value.startEntry !== undefined).map(value => {
return {
id: value.extensionId,
installed: false,
friendlyName: value.friendlyName,
isPlatformCompatible: false,
dependencies: [],
helpLink: value.startEntry?.helpLink ?? '',
startConnectLabel: value.startEntry?.startConnectLabel ?? '',
startCommand: value.startEntry?.startCommand ?? '',
priority: value.startEntry?.priority ?? 10,
supportedPlatforms: value.supportedPlatforms
};
});

this.remoteExtensionMetadata.sort((ext1, ext2) => ext1.priority - ext2.priority);

// Set initial connection state
if (this.remoteAuthority) {
this.connectionState = 'initializing';
Expand All @@ -127,6 +167,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr

this.updateWhenInstalledExtensionsRegistered();
this.updateRemoteStatusIndicator();
this.initializeRemoteMetadata();
}

private registerActions(): void {
Expand Down Expand Up @@ -253,6 +294,53 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
this._register(new DomEmitter(window, 'offline')).event
)(() => this.setNetworkState(navigator.onLine ? 'online' : 'offline')));
}

this._register(this.extensionService.onDidChangeExtensions(async (result) => {
for (const ext of result.added) {
const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, ext.identifier));
if (index > -1) {
this.remoteExtensionMetadata[index].installed = true;
}
}
}));

this._register(this.extensionManagementService.onDidUninstallExtension(async (result) => {
const index = this.remoteExtensionMetadata.findIndex(value => ExtensionIdentifier.equals(value.id, result.identifier.id));
if (index > -1) {
this.remoteExtensionMetadata[index].installed = false;
}
}));
}

private async initializeRemoteMetadata(): Promise<void> {

if (this._isInitialized) {
return;
}

const currentPlatform = PlatformToString(platform);
for (let i = 0; i < this.remoteExtensionMetadata.length; i++) {
const extensionId = this.remoteExtensionMetadata[i].id;
const supportedPlatforms = this.remoteExtensionMetadata[i].supportedPlatforms;
// Update compatibility
const token = new CancellationTokenSource();
const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: extensionId }], token.token))[0];
if (!await this.extensionManagementService.canInstall(galleryExtension)) {
this.remoteExtensionMetadata[i].isPlatformCompatible = false;
}
else if (supportedPlatforms && !supportedPlatforms.includes(currentPlatform)) {
this.remoteExtensionMetadata[i].isPlatformCompatible = false;
}
else {
this.remoteExtensionMetadata[i].isPlatformCompatible = true;
this.remoteExtensionMetadata[i].dependencies = galleryExtension.properties.extensionPack ?? [];
}

// Check if installed and enabled
this.remoteExtensionMetadata[i].installed = (await this.extensionManagementService.getInstalled()).find(value => ExtensionIdentifier.equals(value.identifier.id, extensionId)) ? true : false;
}

this._isInitialized = true;
}

private updateVirtualWorkspaceLocation() {
Expand Down Expand Up @@ -547,6 +635,32 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
return markdownTooltip;
}

private async installAndRunStartCommand(metadata: RemoteExtensionMetadata) {
const extensionId = metadata.id;
const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: extensionId }], CancellationToken.None))[0];

await this.extensionManagementService.installFromGallery(galleryExtension, {
isMachineScoped: false,
donotIncludePackAndDependencies: false,
context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true }
});

await retry(async () => {
const ext = await this.extensionService.getExtension(metadata.id);
if (!ext) {
throw Error('Failed to find installed remote extension');
}
return ext;
}, 300, 10);
this.commandService.executeCommand(metadata.startCommand);

this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: 'remoteInstallAndRun',
detail: extensionId,
from: 'remote indicator'
});
}

private showRemoteMenu() {
const getCategoryLabel = (action: MenuItemAction) => {
if (action.item.category) {
Expand Down Expand Up @@ -642,19 +756,31 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
}
}

if (this.extensionGalleryService.isEnabled() && this.hasAdditionalRemoteExtensions()) {
items.push({
id: RemoteStatusIndicator.INSTALL_REMOTE_EXTENSIONS_ID,
label: nls.localize('installRemotes', "Install Additional Remote Extensions..."),

alwaysShow: true
});
}

if (items.length === entriesBeforeConfig) {
items.pop(); // remove the separator again
}

if (this.extensionGalleryService.isEnabled()) {

const notInstalledItems: QuickPickItem[] = [];
for (const metadata of this.remoteExtensionMetadata) {
if (!metadata.installed && metadata.isPlatformCompatible) {
// Create Install QuickPick with a help link
const label = metadata.startConnectLabel;
const buttons: IQuickInputButton[] = [{
iconClass: ThemeIcon.asClassName(infoIcon),
tooltip: nls.localize('remote.startActions.help', "Learn More")
}];
notInstalledItems.push({ type: 'item', id: metadata.id, label: label, buttons: buttons });
}
}

items.push({
type: 'separator', label: nls.localize('remote.startActions.install', 'Install')
});
items.push(...notInstalledItems);
}

return items;
};

Expand All @@ -663,20 +789,36 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
quickPick.items = computeItems();
quickPick.sortByLabel = false;
quickPick.canSelectMany = false;
once(quickPick.onDidAccept)((_ => {
once(quickPick.onDidAccept)((async _ => {
const selectedItems = quickPick.selectedItems;
if (selectedItems.length === 1) {
const commandId = selectedItems[0].id!;
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: commandId,
from: 'remote indicator'
});
this.commandService.executeCommand(commandId);
const remoteExtension = this.remoteExtensionMetadata.find(value => ExtensionIdentifier.equals(value.id, commandId));
if (remoteExtension) {
quickPick.items = [];
quickPick.busy = true;
quickPick.placeholder = nls.localize('remote.startActions.installingExtension', 'Installing extension... ');
quickPick.hide();
await this.installAndRunStartCommand(remoteExtension);
}
else {
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: commandId,
from: 'remote indicator'
});
this.commandService.executeCommand(commandId);
}
quickPick.hide();
}

quickPick.hide();
}));

once(quickPick.onDidTriggerItemButton)(async (e) => {
const remoteExtension = this.remoteExtensionMetadata.find(value => ExtensionIdentifier.equals(value.id, e.item.id));
if (remoteExtension) {
await this.openerService.open(URI.parse(remoteExtension.helpLink));
}
});

// refresh the items when actions change
const legacyItemUpdater = this.legacyIndicatorMenu.onDidChange(() => quickPick.items = computeItems());
quickPick.onDidHide(legacyItemUpdater.dispose);
Expand All @@ -687,21 +829,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
quickPick.show();
}

private hasAdditionalRemoteExtensions() {
const extensionTips = { ...this.productService.remoteExtensionTips, ...this.productService.virtualWorkspaceExtensionTips };
const currentPlatform = PlatformToString(platform);
for (const extension of Object.values(extensionTips)) {
const { extensionId: recommendedExtensionId, supportedPlatforms } = extension;
if (!supportedPlatforms || supportedPlatforms.includes(currentPlatform)) {
// if this recommended extension isn't already installed, return early
if (!this.extensionService.extensions.some((extension) => extension.id?.toLowerCase() === recommendedExtensionId.toLowerCase())) {
return true;
}
}
}
return false;
}

private hasRemoteMenuCommands(ignoreInstallAdditional: boolean): boolean {
if (this.remoteAuthority !== undefined || this.virtualWorkspaceLocation !== undefined) {
if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { RemoteStartEntry } from 'vs/workbench/contrib/remote/browser/remoteStartEntry';

Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(RemoteStartEntry, LifecyclePhase.Restored);
Loading