diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 06e2d09320f..2dc4800d78c 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -17,9 +17,9 @@ 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 { ForwardedPortsView, VIEWLET_ID } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; +import { AutomaticPortForwarding, ForwardedPortsView, VIEWLET_ID } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; +import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, IViewDescriptorService } 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'; @@ -37,14 +37,13 @@ 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 { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +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, MakeAddress, mapHasTunnelLocalhostOrAllInterfaces } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { forwardedPortsViewEnabled, OpenPortInBrowserAction } from 'vs/workbench/contrib/remote/browser/tunnelView'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; @@ -56,9 +55,6 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { RemoteStatusIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator'; import { inQuickPickContextKeyValue } from 'vs/workbench/browser/quickaccess'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; -import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification'; -import { UrlFinder } from 'vs/workbench/contrib/remote/browser/urlFinder'; -import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; export interface HelpInformation { extensionDescription: IExtensionDescription; @@ -827,91 +823,6 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { } } -class AutomaticPortForwarding extends Disposable implements IWorkbenchContribution { - private contextServiceListener?: IDisposable; - private urlFinder?: UrlFinder; - private static AUTO_FORWARD_SETTING = 'remote.autoForwardPorts'; - - constructor( - @ITerminalService private readonly terminalService: ITerminalService, - @INotificationService private readonly notificationService: INotificationService, - @IOpenerService private readonly openerService: IOpenerService, - @IViewsService private readonly viewsService: IViewsService, - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(); - this._register(configurationService.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) { - this.tryStartStopUrlFinder(); - } - })); - - if (this.environmentService.remoteAuthority) { - this.contextServiceListener = this._register(this.contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(new Set(forwardedPortsViewEnabled.keys()))) { - this.tryStartStopUrlFinder(); - } - })); - this.tryStartStopUrlFinder(); - } - } - - private tryStartStopUrlFinder() { - if (this.configurationService.getValue(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) { - this.startUrlFinder(); - } else { - this.stopUrlFinder(); - } - } - - private startUrlFinder() { - if (!this.urlFinder && !forwardedPortsViewEnabled.getValue(this.contextKeyService)) { - return; - } - if (this.contextServiceListener) { - this.contextServiceListener.dispose(); - } - this.urlFinder = this._register(new UrlFinder(this.terminalService)); - this._register(this.urlFinder.onDidMatchLocalUrl(async (localUrl) => { - if (mapHasTunnelLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, localUrl.host, localUrl.port)) { - return; - } - const forwarded = await this.remoteExplorerService.forward(localUrl); - if (forwarded) { - const address = MakeAddress(forwarded.tunnelRemoteHost, forwarded.tunnelRemotePort); - const message = nls.localize('remote.tunnelsView.automaticForward', "{0} from the remote has been forwarded to {1} locally.", - address, forwarded.localAddress); - const browserChoice: IPromptChoice = { - label: OpenPortInBrowserAction.LABEL, - run: () => OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address) - }; - const showChoice: IPromptChoice = { - label: nls.localize('remote.tunnelsView.showView', "Show Forwarded Ports"), - run: () => { - const remoteAuthority = this.environmentService.remoteAuthority; - const explorerType: string[] | undefined = remoteAuthority ? [remoteAuthority.split('+')[0]] : undefined; - if (explorerType) { - this.remoteExplorerService.targetType = explorerType; - } - this.viewsService.openViewContainer(VIEWLET_ID); - } - }; - this.notificationService.prompt(Severity.Info, message, [browserChoice, showChoice], { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } }); - } - })); - } - - private stopUrlFinder() { - if (this.urlFinder) { - this.urlFinder.dispose(); - this.urlFinder = undefined; - } - } -} - const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteStatusIndicator, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 012ada7de18..97c52340794 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -5,33 +5,42 @@ import * as nls from 'vs/nls'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { Extensions, IViewDescriptorService, IViewsRegistry } from 'vs/workbench/common/views'; +import { Extensions, IViewDescriptorService, IViewsRegistry, IViewsService } from 'vs/workbench/common/views'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IRemoteExplorerService, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { forwardedPortsViewEnabled, ForwardPortAction, TunnelPanelDescriptor, TunnelViewModel } from 'vs/workbench/contrib/remote/browser/tunnelView'; +import { IRemoteExplorerService, MakeAddress, mapHasTunnelLocalhostOrAllInterfaces, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, TunnelPanelDescriptor, TunnelViewModel } from 'vs/workbench/contrib/remote/browser/tunnelView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; +import { UrlFinder } from 'vs/workbench/contrib/remote/browser/urlFinder'; +import Severity from 'vs/base/common/severity'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; export const VIEWLET_ID = 'workbench.view.remote'; export class ForwardedPortsView extends Disposable implements IWorkbenchContribution { private contextKeyListener?: IDisposable; private _activityBadge?: IDisposable; + private entryAccessor: IStatusbarEntryAccessor | undefined; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, - @IActivityService private readonly activityService: IActivityService + @IActivityService private readonly activityService: IActivityService, + @IStatusbarService private readonly statusbarService: IStatusbarService ) { super(); this._register(Registry.as(Extensions.ViewsRegistry).registerViewWelcomeContent(TUNNEL_VIEW_ID, { content: `Forwarded ports allow you to access your running applications locally.\n[Forward a Port](command:${ForwardPortAction.INLINE_ID})`, })); + this.enableBadgeAndStatusBar(); this.enableForwardedPortsView(); - this.enableBadge(); } private enableForwardedPortsView() { @@ -57,12 +66,19 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu } } - private enableBadge() { - this._register(this.remoteExplorerService.tunnelModel.onForwardPort(() => this.updateActivityBadge())); - this._register(this.remoteExplorerService.tunnelModel.onClosePort(() => this.updateActivityBadge())); + private enableBadgeAndStatusBar() { + this._register(this.remoteExplorerService.tunnelModel.onForwardPort(() => { + this.updateActivityBadge(); + this.updateStatusBar(); + })); + this._register(this.remoteExplorerService.tunnelModel.onClosePort(() => { + this.updateActivityBadge(); + this.updateStatusBar(); + })); const disposable = Registry.as(Extensions.ViewsRegistry).onViewsRegistered(e => { if (e.find(view => view.views.find(viewDescriptor => viewDescriptor.id === TUNNEL_VIEW_ID))) { this.updateActivityBadge(); + this.updateStatusBar(); disposable.dispose(); } }); @@ -81,4 +97,108 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu } } } + + private updateStatusBar() { + if (!this.entryAccessor && this.remoteExplorerService.tunnelModel.forwarded.size > 0) { + this._register(this.entryAccessor = this.statusbarService.addEntry(this.entry, 'status.forwardedPorts', nls.localize('status.forwardedPorts', "Forwarded Ports"), StatusbarAlignment.LEFT, 40)); + } else if (this.entryAccessor && this.remoteExplorerService.tunnelModel.forwarded.size === 0) { + this.entryAccessor.dispose(); + this.entryAccessor = undefined; + } + } + + private get entry(): IStatusbarEntry { + return { + text: '$(radio-tower) ' + nls.localize('remote.forwardedPorts.statusbarText', "Application running"), + ariaLabel: nls.localize('remote.forwardedPorts.statusbarAria', "Application running and available locally through port forwarding"), + tooltip: nls.localize('remote.forwardedPorts.statusbarTooltip', "Application available locally (port forwarded)"), + command: `${TUNNEL_VIEW_ID}.focus` + }; + } +} + + +export class AutomaticPortForwarding extends Disposable implements IWorkbenchContribution { + private contextServiceListener?: IDisposable; + private urlFinder?: UrlFinder; + private static AUTO_FORWARD_SETTING = 'remote.autoForwardPorts'; + + constructor( + @ITerminalService private readonly terminalService: ITerminalService, + @INotificationService private readonly notificationService: INotificationService, + @IOpenerService private readonly openerService: IOpenerService, + @IViewsService private readonly viewsService: IViewsService, + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + this._register(configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) { + this.tryStartStopUrlFinder(); + } + })); + + if (this.environmentService.remoteAuthority) { + this.contextServiceListener = this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(new Set(forwardedPortsViewEnabled.keys()))) { + this.tryStartStopUrlFinder(); + } + })); + this.tryStartStopUrlFinder(); + } + } + + private tryStartStopUrlFinder() { + if (this.configurationService.getValue(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) { + this.startUrlFinder(); + } else { + this.stopUrlFinder(); + } + } + + private startUrlFinder() { + if (!this.urlFinder && !forwardedPortsViewEnabled.getValue(this.contextKeyService)) { + return; + } + if (this.contextServiceListener) { + this.contextServiceListener.dispose(); + } + this.urlFinder = this._register(new UrlFinder(this.terminalService)); + this._register(this.urlFinder.onDidMatchLocalUrl(async (localUrl) => { + if (mapHasTunnelLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, localUrl.host, localUrl.port)) { + return; + } + const forwarded = await this.remoteExplorerService.forward(localUrl); + if (forwarded) { + const address = MakeAddress(forwarded.tunnelRemoteHost, forwarded.tunnelRemotePort); + const message = nls.localize('remote.tunnelsView.automaticForward', "{0} from the remote has been forwarded to {1} locally.", + address, forwarded.localAddress); + const browserChoice: IPromptChoice = { + label: OpenPortInBrowserAction.LABEL, + run: () => OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address) + }; + const showChoice: IPromptChoice = { + label: nls.localize('remote.tunnelsView.showView', "Show Forwarded Ports"), + run: () => { + const remoteAuthority = this.environmentService.remoteAuthority; + const explorerType: string[] | undefined = remoteAuthority ? [remoteAuthority.split('+')[0]] : undefined; + if (explorerType) { + this.remoteExplorerService.targetType = explorerType; + } + this.viewsService.openViewContainer(VIEWLET_ID); + } + }; + this.notificationService.prompt(Severity.Info, message, [browserChoice, showChoice], { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } }); + } + })); + } + + private stopUrlFinder() { + if (this.urlFinder) { + this.urlFinder.dispose(); + this.urlFinder = undefined; + } + } }