/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./remoteViewlet'; import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { FilterViewPaneContainer } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { VIEWLET_ID, VIEW_CONTAINER } from 'vs/workbench/contrib/remote/common/remote.contribution'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptor, IViewsRegistry, Extensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction, Viewlet } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { IProgress, IProgressStep, IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ReconnectionWaitEvent, PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; import Severity from 'vs/base/common/severity'; import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { SwitchRemoteViewItem, SwitchRemoteAction } from 'vs/workbench/contrib/remote/browser/explorerViewItems'; import { Action, IActionViewItem, IAction } from 'vs/base/common/actions'; import { isStringArray } from 'vs/base/common/types'; import { IRemoteExplorerService, HelpInformation } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { startsWith } from 'vs/base/common/strings'; import { TunnelPanelDescriptor, TunnelViewModel } from 'vs/workbench/contrib/remote/browser/tunnelView'; import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; class HelpModel { items: IHelpItem[] | undefined; constructor( openerService: IOpenerService, quickInputService: IQuickInputService, commandService: ICommandService, remoteExplorerService: IRemoteExplorerService, environmentService: IWorkbenchEnvironmentService ) { let helpItems: IHelpItem[] = []; const getStarted = remoteExplorerService.helpInformation.filter(info => info.getStarted); if (getStarted.length) { helpItems.push(new HelpItem( ['getStarted'], nls.localize('remote.help.getStarted', "$(star) Get Started"), getStarted.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.getStarted!, remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName })), quickInputService, environmentService, openerService, remoteExplorerService )); } const documentation = remoteExplorerService.helpInformation.filter(info => info.documentation); if (documentation.length) { helpItems.push(new HelpItem( ['documentation'], nls.localize('remote.help.documentation', "$(book) Read Documentation"), documentation.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.documentation!, remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName })), quickInputService, environmentService, openerService, remoteExplorerService )); } const feedback = remoteExplorerService.helpInformation.filter(info => info.feedback); if (feedback.length) { helpItems.push(new HelpItem( ['feedback'], nls.localize('remote.help.feedback', "$(twitter) Provide Feedback"), feedback.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.feedback!, remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName })), quickInputService, environmentService, openerService, remoteExplorerService )); } const issues = remoteExplorerService.helpInformation.filter(info => info.issues); if (issues.length) { helpItems.push(new HelpItem( ['issues'], nls.localize('remote.help.issues', "$(issues) Review Issues"), issues.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.issues!, remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName })), quickInputService, environmentService, openerService, remoteExplorerService )); } if (helpItems.length) { helpItems.push(new IssueReporterItem( ['issueReporter'], nls.localize('remote.help.report', "$(comment) Report Issue"), remoteExplorerService.helpInformation.map(info => ({ extensionDescription: info.extensionDescription, remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName })), quickInputService, environmentService, commandService, remoteExplorerService )); } if (helpItems.length) { this.items = helpItems; } } } interface IHelpItem extends IQuickPickItem { label: string; handleClick(): Promise; } abstract class HelpItemBase implements IHelpItem { constructor( public iconClasses: string[], public label: string, public values: { extensionDescription: IExtensionDescription, url?: string, remoteAuthority: string[] | undefined }[], private quickInputService: IQuickInputService, private environmentService: IWorkbenchEnvironmentService, private remoteExplorerService: IRemoteExplorerService ) { iconClasses.push('remote-help-tree-node-item-icon'); } async handleClick() { const remoteAuthority = this.environmentService.configuration.remoteAuthority; if (remoteAuthority && startsWith(remoteAuthority, this.remoteExplorerService.targetType)) { for (let value of this.values) { if (value.remoteAuthority) { for (let authority of value.remoteAuthority) { if (startsWith(remoteAuthority, authority)) { await this.takeAction(value.extensionDescription, value.url); return; } } } } } if (this.values.length > 1) { let actions = this.values.map(value => { return { label: value.extensionDescription.displayName || value.extensionDescription.identifier.value, description: value.url, extensionDescription: value.extensionDescription }; }); const action = await this.quickInputService.pick(actions, { placeHolder: nls.localize('pickRemoteExtension', "Select url to open") }); if (action) { await this.takeAction(action.extensionDescription, action.description); } } else { await this.takeAction(this.values[0].extensionDescription, this.values[0].url); } } protected abstract takeAction(extensionDescription: IExtensionDescription, url?: string): Promise; } class HelpItem extends HelpItemBase { constructor( iconClasses: string[], label: string, values: { extensionDescription: IExtensionDescription; url: string, remoteAuthority: string[] | undefined }[], quickInputService: IQuickInputService, environmentService: IWorkbenchEnvironmentService, private openerService: IOpenerService, remoteExplorerService: IRemoteExplorerService ) { super(iconClasses, label, values, quickInputService, environmentService, remoteExplorerService); } protected async takeAction(extensionDescription: IExtensionDescription, url: string): Promise { await this.openerService.open(URI.parse(url)); } } class IssueReporterItem extends HelpItemBase { constructor( iconClasses: string[], label: string, values: { extensionDescription: IExtensionDescription; remoteAuthority: string[] | undefined }[], quickInputService: IQuickInputService, environmentService: IWorkbenchEnvironmentService, private commandService: ICommandService, remoteExplorerService: IRemoteExplorerService ) { super(iconClasses, label, values, quickInputService, environmentService, remoteExplorerService); } protected async takeAction(extensionDescription: IExtensionDescription): Promise { await this.commandService.executeCommand('workbench.action.openIssueReporter', [extensionDescription.identifier.value]); } } class HelpAction extends Action { static readonly ID = 'remote.explorer.help'; static readonly LABEL = nls.localize('remote.explorer.help', "Help, Documentation, and Feedback"); private helpModel: HelpModel; constructor(id: string, label: string, @IOpenerService private readonly openerService: IOpenerService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ICommandService private readonly commandService: ICommandService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(id, label, 'codicon codicon-question'); this.helpModel = new HelpModel(openerService, quickInputService, commandService, remoteExplorerService, environmentService); } async run(event?: any): Promise { if (!this.helpModel.items) { this.helpModel = new HelpModel(this.openerService, this.quickInputService, this.commandService, this.remoteExplorerService, this.environmentService); } if (this.helpModel.items) { const selection = await this.quickInputService.pick(this.helpModel.items, { placeHolder: nls.localize('remote.explorer.helpPlaceholder', "Help and Feedback") }); if (selection) { return selection.handleClick(); } } } } export class RemoteViewlet extends Viewlet { constructor( @ITelemetryService telemetryService: ITelemetryService, @IStorageService protected storageService: IStorageService, @IInstantiationService protected instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IContextMenuService protected contextMenuService: IContextMenuService, @IExtensionService protected extensionService: IExtensionService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService, @IConfigurationService protected configurationService: IConfigurationService ) { super(VIEWLET_ID, instantiationService.createInstance(RemoteViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); } } export class RemoteViewPaneContainer extends FilterViewPaneContainer { private actions: IAction[] | undefined; private tunnelPanelDescriptor: TunnelPanelDescriptor | undefined; private static contextKeyName: string = 'forwardedPortsViewEnabled'; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IStorageService storageService: IStorageService, @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(VIEWLET_ID, remoteExplorerService.onDidChangeTargetType, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); this.contextKeyService.createKey(RemoteViewPaneContainer.contextKeyName, false); } protected getFilterOn(viewDescriptor: IViewDescriptor): string | undefined { return isStringArray(viewDescriptor.remoteAuthority) ? viewDescriptor.remoteAuthority[0] : viewDescriptor.remoteAuthority; } public getActionViewItem(action: Action): IActionViewItem | undefined { if (action.id === SwitchRemoteAction.ID) { return this.instantiationService.createInstance(SwitchRemoteViewItem, action, SwitchRemoteViewItem.createOptionItems(Registry.as(Extensions.ViewsRegistry).getViews(VIEW_CONTAINER), this.contextKeyService)); } return super.getActionViewItem(action); } public getActions(): IAction[] { if (!this.actions) { this.actions = [ this.instantiationService.createInstance(SwitchRemoteAction, SwitchRemoteAction.ID, SwitchRemoteAction.LABEL), this.instantiationService.createInstance(HelpAction, HelpAction.ID, HelpAction.LABEL) ]; this.actions.forEach(a => { this._register(a); }); } return this.actions; } getTitle(): string { const title = nls.localize('remote.explorer', "Remote Explorer"); return title; } onDidAddViews(added: IAddedViewDescriptorRef[]): ViewPane[] { // Call to super MUST be first, since registering the additional view will cause this to be called again. const panels: ViewPane[] = super.onDidAddViews(added); // This context key is set to false in the constructor, but is expected to be changed by resolver extensions to enable the forwarded ports view. const viewEnabled: boolean = !!this.contextKeyService.getContextKeyValue(RemoteViewPaneContainer.contextKeyName); if (this.environmentService.configuration.remoteAuthority && !this.tunnelPanelDescriptor && viewEnabled) { this.tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService), this.environmentService); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViews([this.tunnelPanelDescriptor!], VIEW_CONTAINER); } return panels; } } Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( RemoteViewlet, VIEWLET_ID, nls.localize('remote.explorer', "Remote Explorer"), 'codicon-remote-explorer', 4 )); class OpenRemoteViewletAction extends ShowViewletAction { static readonly ID = VIEWLET_ID; static readonly LABEL = nls.localize('toggleRemoteViewlet', "Show Remote Explorer"); constructor(id: string, label: string, @IViewletService viewletService: IViewletService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService) { super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); } } // Register Action to Open Viewlet Registry.as(WorkbenchActionExtensions.WorkbenchActions).registerWorkbenchAction( SyncActionDescriptor.create(OpenRemoteViewletAction, VIEWLET_ID, nls.localize('toggleRemoteViewlet', "Show Remote Explorer"), { primary: 0 }), 'View: Show Remote Explorer', nls.localize('view', "View") ); class ProgressReporter { private _currentProgress: IProgress | null = null; private lastReport: string | null = null; constructor(currentProgress: IProgress | null) { this._currentProgress = currentProgress; } set currentProgress(progress: IProgress) { this._currentProgress = progress; } report(message?: string) { if (message) { this.lastReport = message; } if (this.lastReport && this._currentProgress) { this._currentProgress.report({ message: this.lastReport }); } } } class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IProgressService progressService: IProgressService, @IDialogService dialogService: IDialogService, @ICommandService commandService: ICommandService, @IContextKeyService contextKeyService: IContextKeyService ) { const connection = remoteAgentService.getConnection(); if (connection) { let currentProgressPromiseResolve: (() => void) | null = null; let progressReporter: ProgressReporter | null = null; let lastLocation: ProgressLocation | null = null; let currentTimer: ReconnectionTimer | null = null; let reconnectWaitEvent: ReconnectionWaitEvent | null = null; let disposableListener: IDisposable | null = null; function showProgress(location: ProgressLocation, buttons: { label: string, callback: () => void }[]) { if (currentProgressPromiseResolve) { currentProgressPromiseResolve(); } const promise = new Promise((resolve) => currentProgressPromiseResolve = resolve); lastLocation = location; if (location === ProgressLocation.Dialog) { // Show dialog progressService!.withProgress( { location: ProgressLocation.Dialog, buttons: buttons.map(button => button.label) }, (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, (choice?) => { // Handle choice from dialog if (buttons[choice]) { buttons[choice].callback(); } else { showProgress(ProgressLocation.Notification, buttons); } progressReporter!.report(); }); } else { // Show notification progressService!.withProgress( { location: ProgressLocation.Notification, buttons: buttons.map(button => button.label) }, (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, (choice?) => { // Handle choice from dialog if (buttons[choice]) { buttons[choice].callback(); } else { hideProgress(); } }); } } function hideProgress() { if (currentProgressPromiseResolve) { currentProgressPromiseResolve(); } currentProgressPromiseResolve = null; } const reconnectButton = { label: nls.localize('reconnectNow', "Reconnect Now"), callback: () => { if (reconnectWaitEvent) { reconnectWaitEvent.skipWait(); } } }; const reloadButton = { label: nls.localize('reloadWindow', "Reload Window"), callback: () => { commandService.executeCommand(ReloadWindowAction.ID); } }; connection.onDidStateChange((e) => { if (currentTimer) { currentTimer.dispose(); currentTimer = null; } if (disposableListener) { disposableListener.dispose(); disposableListener = null; } switch (e.type) { case PersistentConnectionEventType.ConnectionLost: if (!currentProgressPromiseResolve) { progressReporter = new ProgressReporter(null); showProgress(ProgressLocation.Dialog, [reconnectButton, reloadButton]); } progressReporter!.report(nls.localize('connectionLost', "Connection Lost")); break; case PersistentConnectionEventType.ReconnectionWait: hideProgress(); reconnectWaitEvent = e; showProgress(lastLocation || ProgressLocation.Notification, [reconnectButton, reloadButton]); currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds); break; case PersistentConnectionEventType.ReconnectionRunning: hideProgress(); showProgress(lastLocation || ProgressLocation.Notification, [reloadButton]); progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect...")); // Register to listen for quick input is opened disposableListener = contextKeyService.onDidChangeContext((contextKeyChangeEvent) => { const reconnectInteraction = new Set(['inQuickOpen']); if (contextKeyChangeEvent.affectsSome(reconnectInteraction)) { // Need to move from dialog if being shown and user needs to type in a prompt if (lastLocation === ProgressLocation.Dialog && progressReporter !== null) { hideProgress(); showProgress(ProgressLocation.Notification, [reloadButton]); progressReporter.report(); } } }); break; case PersistentConnectionEventType.ReconnectionPermanentFailure: hideProgress(); progressReporter = null; dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1 }).then(result => { // Reload the window if (result.choice === 0) { commandService.executeCommand(ReloadWindowAction.ID); } }); break; case PersistentConnectionEventType.ConnectionGain: hideProgress(); progressReporter = null; break; } }); } } } class ReconnectionTimer implements IDisposable { private readonly _progressReporter: ProgressReporter; private readonly _completionTime: number; private readonly _token: any; constructor(progressReporter: ProgressReporter, completionTime: number) { this._progressReporter = progressReporter; this._completionTime = completionTime; this._token = setInterval(() => this._render(), 1000); this._render(); } public dispose(): void { clearInterval(this._token); } private _render() { const remainingTimeMs = this._completionTime - Date.now(); if (remainingTimeMs < 0) { return; } const remainingTime = Math.ceil(remainingTimeMs / 1000); if (remainingTime === 1) { this._progressReporter.report(nls.localize('reconnectionWaitOne', "Attempting to reconnect in {0} second...", remainingTime)); } else { this._progressReporter.report(nls.localize('reconnectionWaitMany', "Attempting to reconnect in {0} seconds...", remainingTime)); } } } const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually);