|
| 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 * as nls from 'vs/nls'; |
| 7 | +import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; |
| 8 | +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; |
| 9 | +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; |
| 10 | +import { Disposable } from 'vs/base/common/lifecycle'; |
| 11 | +import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; |
| 12 | +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; |
| 13 | +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; |
| 14 | +import { ILabelService } from 'vs/platform/label/common/label'; |
| 15 | +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; |
| 16 | +import { ICommandService } from 'vs/platform/commands/common/commands'; |
| 17 | +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; |
| 18 | +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; |
| 19 | +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; |
| 20 | +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; |
| 21 | +import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; |
| 22 | +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; |
| 23 | +import { IHostService } from 'vs/workbench/services/host/browser/host'; |
| 24 | +import { RemoteConnectionState, Deprecated_RemoteAuthorityContext } from 'vs/workbench/browser/contextkeys'; |
| 25 | +import { isWeb } from 'vs/base/common/platform'; |
| 26 | +import { once } from 'vs/base/common/functional'; |
| 27 | + |
| 28 | +const WINDOW_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; |
| 29 | +const CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; |
| 30 | +const SHOW_CLOSE_REMOTE_COMMAND_ID = !isWeb; // web does not have a "Close Remote" command |
| 31 | + |
| 32 | +export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenchContribution { |
| 33 | + |
| 34 | + private windowIndicatorEntry: IStatusbarEntryAccessor | undefined; |
| 35 | + private windowCommandMenu: IMenu; |
| 36 | + private hasWindowActions: boolean = false; |
| 37 | + private remoteAuthority: string | undefined; |
| 38 | + private connectionState: 'initializing' | 'connected' | 'disconnected' | undefined = undefined; |
| 39 | + |
| 40 | + constructor( |
| 41 | + @IStatusbarService private readonly statusbarService: IStatusbarService, |
| 42 | + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, |
| 43 | + @ILabelService private readonly labelService: ILabelService, |
| 44 | + @IContextKeyService private contextKeyService: IContextKeyService, |
| 45 | + @IMenuService private menuService: IMenuService, |
| 46 | + @IQuickInputService private readonly quickInputService: IQuickInputService, |
| 47 | + @ICommandService private readonly commandService: ICommandService, |
| 48 | + @IExtensionService extensionService: IExtensionService, |
| 49 | + @IRemoteAgentService remoteAgentService: IRemoteAgentService, |
| 50 | + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, |
| 51 | + @IHostService hostService: IHostService |
| 52 | + ) { |
| 53 | + super(); |
| 54 | + |
| 55 | + this.windowCommandMenu = this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService); |
| 56 | + this._register(this.windowCommandMenu); |
| 57 | + |
| 58 | + const category = nls.localize('remote.category', "Remote"); |
| 59 | + const that = this; |
| 60 | + registerAction2(class extends Action2 { |
| 61 | + constructor() { |
| 62 | + super({ |
| 63 | + id: WINDOW_ACTIONS_COMMAND_ID, |
| 64 | + category, |
| 65 | + title: { value: nls.localize('remote.showMenu', "Show Remote Menu"), original: 'Show Remote Menu' }, |
| 66 | + f1: true, |
| 67 | + }); |
| 68 | + } |
| 69 | + run = () => that.showIndicatorActions(that.windowCommandMenu); |
| 70 | + }); |
| 71 | + |
| 72 | + this.remoteAuthority = environmentService.configuration.remoteAuthority; |
| 73 | + Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(this.remoteAuthority || ''); |
| 74 | + |
| 75 | + if (this.remoteAuthority) { |
| 76 | + |
| 77 | + if (SHOW_CLOSE_REMOTE_COMMAND_ID) { |
| 78 | + registerAction2(class extends Action2 { |
| 79 | + constructor() { |
| 80 | + super({ |
| 81 | + id: CLOSE_REMOTE_COMMAND_ID, |
| 82 | + category, |
| 83 | + title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, |
| 84 | + f1: true |
| 85 | + }); |
| 86 | + } |
| 87 | + run = () => that.remoteAuthority && hostService.openWindow({ forceReuseWindow: true }); |
| 88 | + }); |
| 89 | + |
| 90 | + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { |
| 91 | + group: '6_close', |
| 92 | + command: { |
| 93 | + id: CLOSE_REMOTE_COMMAND_ID, |
| 94 | + title: nls.localize({ key: 'miCloseRemote', comment: ['&& denotes a mnemonic'] }, "Close Re&&mote Connection") |
| 95 | + }, |
| 96 | + order: 3.5 |
| 97 | + }); |
| 98 | + } |
| 99 | + |
| 100 | + // Pending entry until extensions are ready |
| 101 | + this.renderWindowIndicator('$(sync~spin) ' + nls.localize('host.open', "Opening Remote..."), undefined, WINDOW_ACTIONS_COMMAND_ID); |
| 102 | + this.connectionState = 'initializing'; |
| 103 | + RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); |
| 104 | + |
| 105 | + const connection = remoteAgentService.getConnection(); |
| 106 | + if (connection) { |
| 107 | + this._register(connection.onDidStateChange((e) => { |
| 108 | + switch (e.type) { |
| 109 | + case PersistentConnectionEventType.ConnectionLost: |
| 110 | + case PersistentConnectionEventType.ReconnectionPermanentFailure: |
| 111 | + case PersistentConnectionEventType.ReconnectionRunning: |
| 112 | + case PersistentConnectionEventType.ReconnectionWait: |
| 113 | + this.setDisconnected(true); |
| 114 | + break; |
| 115 | + case PersistentConnectionEventType.ConnectionGain: |
| 116 | + this.setDisconnected(false); |
| 117 | + break; |
| 118 | + } |
| 119 | + })); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + extensionService.whenInstalledExtensionsRegistered().then(_ => { |
| 124 | + if (this.remoteAuthority) { |
| 125 | + this._register(this.labelService.onDidChangeFormatters(e => this.updateWindowIndicator())); |
| 126 | + remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority).then(() => this.setDisconnected(false), () => this.setDisconnected(true)); |
| 127 | + } |
| 128 | + this._register(this.windowCommandMenu.onDidChange(e => this.updateWindowActions())); |
| 129 | + this.updateWindowIndicator(); |
| 130 | + }); |
| 131 | + } |
| 132 | + |
| 133 | + private setDisconnected(isDisconnected: boolean): void { |
| 134 | + const newState = isDisconnected ? 'disconnected' : 'connected'; |
| 135 | + if (this.connectionState !== newState) { |
| 136 | + this.connectionState = newState; |
| 137 | + RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); |
| 138 | + Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(isDisconnected ? `disconnected/${this.remoteAuthority!}` : this.remoteAuthority!); |
| 139 | + this.updateWindowIndicator(); |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + private updateWindowIndicator(): void { |
| 144 | + const windowActionCommand = (this.remoteAuthority || this.windowCommandMenu.getActions().length) ? WINDOW_ACTIONS_COMMAND_ID : undefined; |
| 145 | + if (this.remoteAuthority) { |
| 146 | + const hostLabel = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.remoteAuthority) || this.remoteAuthority; |
| 147 | + if (this.connectionState !== 'disconnected') { |
| 148 | + this.renderWindowIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel), windowActionCommand); |
| 149 | + } else { |
| 150 | + this.renderWindowIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from")} ${hostLabel}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel), windowActionCommand); |
| 151 | + } |
| 152 | + } else { |
| 153 | + if (windowActionCommand) { |
| 154 | + this.renderWindowIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a remote window"), windowActionCommand); |
| 155 | + } else if (this.windowIndicatorEntry) { |
| 156 | + this.windowIndicatorEntry.dispose(); |
| 157 | + this.windowIndicatorEntry = undefined; |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + private updateWindowActions() { |
| 163 | + const newHasWindowActions = this.windowCommandMenu.getActions().length > 0; |
| 164 | + if (newHasWindowActions !== this.hasWindowActions) { |
| 165 | + this.hasWindowActions = newHasWindowActions; |
| 166 | + this.updateWindowIndicator(); |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + private renderWindowIndicator(text: string, tooltip?: string, command?: string): void { |
| 171 | + const properties: IStatusbarEntry = { |
| 172 | + backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), text, tooltip, command |
| 173 | + }; |
| 174 | + if (this.windowIndicatorEntry) { |
| 175 | + this.windowIndicatorEntry.update(properties); |
| 176 | + } else { |
| 177 | + this.windowIndicatorEntry = this.statusbarService.addEntry(properties, 'status.host', nls.localize('status.host', "Remote Host"), StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + private showIndicatorActions(menu: IMenu) { |
| 182 | + |
| 183 | + const actions = menu.getActions(); |
| 184 | + |
| 185 | + const items: (IQuickPickItem | IQuickPickSeparator)[] = []; |
| 186 | + for (let actionGroup of actions) { |
| 187 | + if (items.length) { |
| 188 | + items.push({ type: 'separator' }); |
| 189 | + } |
| 190 | + for (let action of actionGroup[1]) { |
| 191 | + if (action instanceof MenuItemAction) { |
| 192 | + let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; |
| 193 | + if (action.item.category) { |
| 194 | + const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value; |
| 195 | + label = nls.localize('cat.title', "{0}: {1}", category, label); |
| 196 | + } |
| 197 | + items.push({ |
| 198 | + type: 'item', |
| 199 | + id: action.item.id, |
| 200 | + label |
| 201 | + }); |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + if (SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { |
| 207 | + if (items.length) { |
| 208 | + items.push({ type: 'separator' }); |
| 209 | + } |
| 210 | + items.push({ |
| 211 | + type: 'item', |
| 212 | + id: CLOSE_REMOTE_COMMAND_ID, |
| 213 | + label: nls.localize('closeRemote.title', 'Close Remote Connection') |
| 214 | + }); |
| 215 | + } |
| 216 | + |
| 217 | + const quickPick = this.quickInputService.createQuickPick(); |
| 218 | + quickPick.items = items; |
| 219 | + quickPick.canSelectMany = false; |
| 220 | + once(quickPick.onDidAccept)((_ => { |
| 221 | + const selectedItems = quickPick.selectedItems; |
| 222 | + if (selectedItems.length === 1) { |
| 223 | + this.commandService.executeCommand(selectedItems[0].id!); |
| 224 | + } |
| 225 | + quickPick.hide(); |
| 226 | + })); |
| 227 | + quickPick.show(); |
| 228 | + } |
| 229 | +} |
0 commit comments