tunnelView.ts 47.4 KB
Newer Older
A
Alex Ross 已提交
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  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!./media/tunnelView';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
9
import { IViewDescriptor, IEditableData, IViewsService, IViewDescriptorService } from 'vs/workbench/common/views';
10
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
A
Alex Ross 已提交
11
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
12
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
13
import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
A
Alex Ross 已提交
14
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
15
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
A
Alex Ross 已提交
16
import { IOpenerService } from 'vs/platform/opener/common/opener';
A
Alex Ross 已提交
17
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
A
Alex Ross 已提交
18 19 20
import { ICommandService, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { Event, Emitter } from 'vs/base/common/event';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
21
import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree';
A
Alex Ross 已提交
22
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
23
import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
24
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
A
Alex Ross 已提交
25
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
J
João Moreno 已提交
26
import { ActionRunner, IAction } from 'vs/base/common/actions';
27
import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction, ILocalizedString, SubmenuItemAction, Action2, registerAction2 } from 'vs/platform/actions/common/actions';
28
import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
A
Alex Ross 已提交
29
import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, mapHasAddressLocalhostOrAllInterfaces, TUNNEL_VIEW_ID, parseAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
A
Alex Ross 已提交
30
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
31
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
32 33 34
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { once } from 'vs/base/common/functional';
35
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
36 37
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
38
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane';
A
Alex Ross 已提交
39
import { URI } from 'vs/base/common/uri';
A
Alex Ross 已提交
40
import { RemoteTunnel } from 'vs/platform/remote/common/tunnel';
S
Sandeep Somavarapu 已提交
41
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
42
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
43
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
44
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
45
import { Codicon } from 'vs/base/common/codicons';
A
Alex Ross 已提交
46 47

export const forwardedPortsViewEnabled = new RawContextKey<boolean>('forwardedPortsViewEnabled', false);
48
export const PORT_AUTO_FORWARD_SETTING = 'remote.autoForwardPorts';
A
Alex Ross 已提交
49 50 51 52 53 54 55 56 57 58 59 60 61 62

class TunnelTreeVirtualDelegate implements IListVirtualDelegate<ITunnelItem> {
	getHeight(element: ITunnelItem): number {
		return 22;
	}

	getTemplateId(element: ITunnelItem): string {
		return 'tunnelItemTemplate';
	}
}

export interface ITunnelViewModel {
	onForwardedPortsChanged: Event<void>;
	readonly forwarded: TunnelItem[];
A
Alex Ross 已提交
63
	readonly detected: TunnelItem[];
64
	readonly candidates: TunnelItem[];
65
	readonly input: TunnelItem;
A
Alex Ross 已提交
66
	groupsAndForwarded(): Promise<ITunnelGroup[]>;
A
Alex Ross 已提交
67 68 69 70 71 72
}

export class TunnelViewModel extends Disposable implements ITunnelViewModel {
	private _onForwardedPortsChanged: Emitter<void> = new Emitter();
	public onForwardedPortsChanged: Event<void> = this._onForwardedPortsChanged.event;
	private model: TunnelModel;
73
	private _input: TunnelItem;
74
	private _candidates: Map<string, { host: string, port: number, detail: string }> = new Map();
A
Alex Ross 已提交
75 76

	constructor(
77 78
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
		@IConfigurationService private readonly configurationService: IConfigurationService) {
A
Alex Ross 已提交
79 80 81 82 83
		super();
		this.model = remoteExplorerService.tunnelModel;
		this._register(this.model.onForwardPort(() => this._onForwardedPortsChanged.fire()));
		this._register(this.model.onClosePort(() => this._onForwardedPortsChanged.fire()));
		this._register(this.model.onPortName(() => this._onForwardedPortsChanged.fire()));
84
		this._register(this.model.onCandidatesChanged(() => this._onForwardedPortsChanged.fire()));
85 86 87 88 89
		this._input = {
			label: nls.localize('remote.tunnelsView.add', "Forward a Port..."),
			tunnelType: TunnelType.Add,
			remoteHost: 'localhost',
			remotePort: 0,
90 91
			description: '',
			iconClasses: undefined
92
		};
A
Alex Ross 已提交
93 94
	}

A
Alex Ross 已提交
95 96
	async groupsAndForwarded(): Promise<ITunnelGroup[]> {
		const groups: (ITunnelGroup | TunnelItem)[] = [];
97 98
		this._candidates = new Map();
		(await this.model.candidates).forEach(candidate => {
A
Alex Ross 已提交
99
			this._candidates.set(makeAddress(candidate.host, candidate.port), candidate);
100
		});
101
		if ((this.model.forwarded.size > 0) || this.remoteExplorerService.getEditableData(undefined)) {
A
Alex Ross 已提交
102
			groups.push(...this.forwarded);
A
Alex Ross 已提交
103
		}
A
Alex Ross 已提交
104
		if (this.model.detected.size > 0) {
A
Alex Ross 已提交
105
			groups.push({
106
				label: nls.localize('remote.tunnelsView.detected', "Existing Tunnels"),
A
Alex Ross 已提交
107 108
				tunnelType: TunnelType.Detected,
				items: this.detected
A
Alex Ross 已提交
109 110
			});
		}
111 112 113 114 115 116 117 118 119
		if (!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) {
			const candidates = await this.candidates;
			if (candidates.length > 0) {
				groups.push({
					label: nls.localize('remote.tunnelsView.candidates', "Not Forwarded"),
					tunnelType: TunnelType.Candidate,
					items: candidates
				});
			}
A
Alex Ross 已提交
120
		}
121 122
		if (groups.length === 0) {
			groups.push(this._input);
123
		}
A
Alex Ross 已提交
124 125 126
		return groups;
	}

127
	private addProcessInfoFromCandidate(tunnelItem: ITunnelItem) {
A
Alex Ross 已提交
128
		const key = makeAddress(tunnelItem.remoteHost, tunnelItem.remotePort);
129 130 131 132 133
		if (this._candidates.has(key)) {
			tunnelItem.description = this._candidates.get(key)!.detail;
		}
	}

A
Alex Ross 已提交
134
	get forwarded(): TunnelItem[] {
135
		const forwarded = Array.from(this.model.forwarded.values()).map(tunnel => {
136 137 138
			const tunnelItem = TunnelItem.createFromTunnel(tunnel);
			this.addProcessInfoFromCandidate(tunnelItem);
			return tunnelItem;
A
Alex Ross 已提交
139 140 141 142 143 144
		}).sort((a: TunnelItem, b: TunnelItem) => {
			if (a.remotePort === b.remotePort) {
				return a.remoteHost < b.remoteHost ? -1 : 1;
			} else {
				return a.remotePort < b.remotePort ? -1 : 1;
			}
A
Alex Ross 已提交
145
		});
146 147 148 149
		if (this.remoteExplorerService.getEditableData(undefined)) {
			forwarded.push(this._input);
		}
		return forwarded;
A
Alex Ross 已提交
150 151
	}

A
Alex Ross 已提交
152 153
	get detected(): TunnelItem[] {
		return Array.from(this.model.detected.values()).map(tunnel => {
154 155 156
			const tunnelItem = TunnelItem.createFromTunnel(tunnel, TunnelType.Detected, false);
			this.addProcessInfoFromCandidate(tunnelItem);
			return tunnelItem;
A
Alex Ross 已提交
157 158 159
		});
	}

160 161 162
	get candidates(): TunnelItem[] {
		const candidates: TunnelItem[] = [];
		this._candidates.forEach(value => {
A
Alex Ross 已提交
163 164
			if (!mapHasAddressLocalhostOrAllInterfaces(this.model.forwarded, value.host, value.port) &&
				!mapHasAddressLocalhostOrAllInterfaces(this.model.detected, value.host, value.port)) {
165
				candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, undefined, false, undefined, value.detail));
166
			}
A
Alex Ross 已提交
167
		});
168
		return candidates;
A
Alex Ross 已提交
169 170
	}

171
	get input(): TunnelItem {
172 173 174
		return this._input;
	}

A
Alex Ross 已提交
175 176 177 178 179 180 181 182 183
	dispose() {
		super.dispose();
	}
}

interface ITunnelTemplateData {
	elementDisposable: IDisposable;
	container: HTMLElement;
	iconLabel: IconLabel;
184
	icon: HTMLElement;
A
Alex Ross 已提交
185 186 187 188 189 190 191
	actionBar: ActionBar;
}

class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGroup | ITunnelItem, ITunnelItem, ITunnelTemplateData> {
	static readonly ITEM_HEIGHT = 22;
	static readonly TREE_TEMPLATE_ID = 'tunnelItemTemplate';

A
Alex Ross 已提交
192
	private inputDone?: (success: boolean, finishEditing: boolean) => void;
A
Alex Ross 已提交
193 194 195 196 197 198
	private _actionRunner: ActionRunner | undefined;

	constructor(
		private readonly viewId: string,
		@IMenuService private readonly menuService: IMenuService,
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
199 200 201 202
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
A
Alex Ross 已提交
203 204 205 206 207 208 209 210 211 212 213 214 215
	) {
		super();
	}

	set actionRunner(actionRunner: ActionRunner) {
		this._actionRunner = actionRunner;
	}

	get templateId(): string {
		return TunnelTreeRenderer.TREE_TEMPLATE_ID;
	}

	renderTemplate(container: HTMLElement): ITunnelTemplateData {
216
		container.classList.add('custom-view-tree-node-item');
217
		const icon = dom.append(container, dom.$('.custom-view-tree-node-item-icon'));
A
Alex Ross 已提交
218 219 220 221 222 223
		const iconLabel = new IconLabel(container, { supportHighlights: true });
		// dom.addClass(iconLabel.element, 'tunnel-view-label');
		const actionsContainer = dom.append(iconLabel.element, dom.$('.actions'));
		const actionBar = new ActionBar(actionsContainer, {
			actionViewItemProvider: (action: IAction) => {
				if (action instanceof MenuItemAction) {
224
					return this.instantiationService.createInstance(MenuEntryActionViewItem, action);
225 226
				} else if (action instanceof SubmenuItemAction) {
					return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action);
A
Alex Ross 已提交
227 228 229 230 231 232
				}

				return undefined;
			}
		});

233
		return { icon, iconLabel, actionBar, container, elementDisposable: Disposable.None };
A
Alex Ross 已提交
234 235 236
	}

	private isTunnelItem(item: ITunnelGroup | ITunnelItem): item is ITunnelItem {
237
		return !!((<ITunnelItem>item).remotePort);
A
Alex Ross 已提交
238 239 240 241 242
	}

	renderElement(element: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
		templateData.elementDisposable.dispose();
		const node = element.element;
243

A
Alex Ross 已提交
244 245
		// reset
		templateData.actionBar.clear();
246 247 248
		templateData.icon.className = 'custom-view-tree-node-item-icon';
		templateData.icon.hidden = true;

A
Alex Ross 已提交
249
		let editableData: IEditableData | undefined;
A
Alex Ross 已提交
250
		if (this.isTunnelItem(node)) {
251
			editableData = this.remoteExplorerService.getEditableData(node);
252 253
			if (editableData) {
				templateData.iconLabel.element.style.display = 'none';
A
Alex Ross 已提交
254
				this.renderInputBox(templateData.container, editableData);
255 256 257
			} else {
				templateData.iconLabel.element.style.display = 'flex';
				this.renderTunnel(node, templateData);
A
Alex Ross 已提交
258
			}
259
		} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined))) {
A
Alex Ross 已提交
260 261
			templateData.iconLabel.element.style.display = 'none';
			this.renderInputBox(templateData.container, editableData);
A
Alex Ross 已提交
262
		} else {
A
Alex Ross 已提交
263
			templateData.iconLabel.element.style.display = 'flex';
A
Alex Ross 已提交
264 265 266 267
			templateData.iconLabel.setLabel(node.label);
		}
	}

268
	private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) {
269 270
		const label = node.label + (node.description ? (' - ' + node.description) : '');
		templateData.iconLabel.setLabel(node.label, node.description, { title: label, extraClasses: ['tunnel-view-label'] });
271

272
		templateData.actionBar.context = node;
273
		const contextKeyService = this._register(this.contextKeyService.createScoped());
274 275 276
		contextKeyService.createKey('view', this.viewId);
		contextKeyService.createKey('tunnelType', node.tunnelType);
		contextKeyService.createKey('tunnelCloseable', node.closeable);
277 278 279
		const disposableStore = new DisposableStore();
		templateData.elementDisposable = disposableStore;
		const menu = disposableStore.add(this.menuService.createMenu(MenuId.TunnelInline, contextKeyService));
280
		const actions: IAction[] = [];
281
		disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
282 283 284 285 286 287
		if (actions) {
			templateData.actionBar.push(actions, { icon: true, label: false });
			if (this._actionRunner) {
				templateData.actionBar.actionRunner = this._actionRunner;
			}
		}
288 289 290 291 292 293 294
		if (node.iconClasses) {
			templateData.icon.className = `custom-view-tree-node-item-icon ${node.iconClasses}`;
			templateData.icon.hidden = false;
		} else {
			templateData.icon.className = 'custom-view-tree-node-item-icon';
			templateData.icon.hidden = true;
		}
A
Alex Ross 已提交
295 296 297

		menu.dispose();
		contextKeyService.dispose();
298 299
	}

A
Alex Ross 已提交
300
	private renderInputBox(container: HTMLElement, editableData: IEditableData): IDisposable {
A
Alex Ross 已提交
301 302 303 304 305
		// Required for FireFox. The blur event doesn't fire on FireFox when you just mash the "+" button to forward a port.
		if (this.inputDone) {
			this.inputDone(false, false);
			this.inputDone = undefined;
		}
306 307 308 309 310
		const value = editableData.startingValue || '';
		const inputBox = new InputBox(container, this.contextViewService, {
			ariaLabel: nls.localize('remote.tunnelsView.input', "Press Enter to confirm or Escape to cancel."),
			validationOptions: {
				validation: (value) => {
311 312
					const message = editableData.validationMessage(value);
					if (!message || message.severity !== Severity.Error) {
313 314 315 316
						return null;
					}

					return {
317
						content: message.content,
318 319 320 321 322 323 324 325 326 327 328
						formatContent: true,
						type: MessageType.ERROR
					};
				}
			},
			placeholder: editableData.placeholder || ''
		});
		const styler = attachInputBoxStyler(inputBox, this.themeService);

		inputBox.value = value;
		inputBox.focus();
329
		inputBox.select({ start: 0, end: editableData.startingValue ? editableData.startingValue.length : 0 });
330 331

		const done = once((success: boolean, finishEditing: boolean) => {
A
Alex Ross 已提交
332 333 334
			if (this.inputDone) {
				this.inputDone = undefined;
			}
335
			inputBox.element.style.display = 'none';
A
Alex Ross 已提交
336
			const inputValue = inputBox.value;
337 338
			dispose(toDispose);
			if (finishEditing) {
A
Alex Ross 已提交
339
				editableData.onFinish(inputValue, success);
340 341
			}
		});
A
Alex Ross 已提交
342
		this.inputDone = done;
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365

		const toDispose = [
			inputBox,
			dom.addStandardDisposableListener(inputBox.inputElement, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
				if (e.equals(KeyCode.Enter)) {
					if (inputBox.validate()) {
						done(true, true);
					}
				} else if (e.equals(KeyCode.Escape)) {
					done(false, true);
				}
			}),
			dom.addDisposableListener(inputBox.inputElement, dom.EventType.BLUR, () => {
				done(inputBox.isInputValid(), true);
			}),
			styler
		];

		return toDisposable(() => {
			done(false, false);
		});
	}

A
Alex Ross 已提交
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
	disposeElement(resource: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
		templateData.elementDisposable.dispose();
	}

	disposeTemplate(templateData: ITunnelTemplateData): void {
		templateData.actionBar.dispose();
		templateData.elementDisposable.dispose();
	}
}

class TunnelDataSource implements IAsyncDataSource<ITunnelViewModel, ITunnelItem | ITunnelGroup> {
	hasChildren(element: ITunnelViewModel | ITunnelItem | ITunnelGroup) {
		if (element instanceof TunnelViewModel) {
			return true;
		} else if (element instanceof TunnelItem) {
			return false;
		} else if ((<ITunnelGroup>element).items) {
			return true;
		}
		return false;
	}

388
	async getChildren(element: ITunnelViewModel | ITunnelItem | ITunnelGroup) {
A
Alex Ross 已提交
389
		if (element instanceof TunnelViewModel) {
A
Alex Ross 已提交
390
			return element.groupsAndForwarded();
A
Alex Ross 已提交
391 392 393 394 395 396 397 398 399 400 401 402
		} else if (element instanceof TunnelItem) {
			return [];
		} else if ((<ITunnelGroup>element).items) {
			return (<ITunnelGroup>element).items!;
		}
		return [];
	}
}

interface ITunnelGroup {
	tunnelType: TunnelType;
	label: string;
A
Alex Ross 已提交
403
	items?: ITunnelItem[] | Promise<ITunnelItem[]>;
A
Alex Ross 已提交
404 405 406
}

class TunnelItem implements ITunnelItem {
A
Alex Ross 已提交
407
	static createFromTunnel(tunnel: Tunnel, type: TunnelType = TunnelType.Forwarded, closeable?: boolean) {
408
		return new TunnelItem(type, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.localPort, closeable === undefined ? tunnel.closeable : closeable, tunnel.name, tunnel.description ?? tunnel.runningProcess, tunnel.source);
A
Alex Ross 已提交
409 410
	}

A
Alex Ross 已提交
411 412
	constructor(
		public tunnelType: TunnelType,
413 414
		public remoteHost: string,
		public remotePort: number,
A
Alex Ross 已提交
415
		public localAddress?: string,
416
		public localPort?: number,
A
Alex Ross 已提交
417 418 419
		public closeable?: boolean,
		public name?: string,
		private _description?: string,
420
		private source?: string
A
Alex Ross 已提交
421 422
	) { }
	get label(): string {
A
Alex Ross 已提交
423
		if (this.name) {
A
Alex Ross 已提交
424
			return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name);
A
Alex Ross 已提交
425
		} else if (this.localAddress) {
A
Alex Ross 已提交
426
			return nls.localize('remote.tunnelsView.forwardedPortLabel1', "{0} \u2192 {1}", this.remotePort, TunnelItem.compactLongAddress(this.localAddress));
A
Alex Ross 已提交
427
		} else {
A
Alex Ross 已提交
428
			return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}", this.remotePort);
A
Alex Ross 已提交
429 430 431
		}
	}

A
Alex Ross 已提交
432
	private static compactLongAddress(address: string): string {
433
		if (address.length < 16) {
A
Alex Ross 已提交
434 435
			return address;
		}
436
		let displayAddress: string = address;
437
		try {
438
			if (!address.startsWith('http')) {
A
Alex Ross 已提交
439
				address = `http://${address}`;
440 441 442 443 444 445 446 447 448
			}
			const url = new URL(address);
			if (url && url.host) {
				const lastDotIndex = url.host.lastIndexOf('.');
				const secondLastDotIndex = lastDotIndex !== -1 ? url.host.substring(0, lastDotIndex).lastIndexOf('.') : -1;
				if (secondLastDotIndex !== -1) {
					displayAddress = `${url.protocol}//...${url.host.substring(secondLastDotIndex + 1)}`;
				}
			}
449 450 451
		} catch (e) {
			// Address isn't a valid url and can't be compacted.
		}
452
		return displayAddress;
A
Alex Ross 已提交
453 454
	}

455 456 457 458
	set description(description: string | undefined) {
		this._description = description;
	}

A
Alex Ross 已提交
459
	get description(): string | undefined {
460 461 462 463 464 465
		const description: string[] = [];

		if (this.name && this.localAddress) {
			description.push(nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} \u2192 {1}", this.remotePort, TunnelItem.compactLongAddress(this.localAddress)));
		}

A
Alex Ross 已提交
466
		if (this._description) {
467 468 469 470 471 472 473 474 475
			description.push(this._description);
		}

		if (this.source) {
			description.push(this.source);
		}

		if (description.length > 0) {
			return description.join('  \u2022  ');
A
Alex Ross 已提交
476
		}
477

A
Alex Ross 已提交
478
		return undefined;
A
Alex Ross 已提交
479
	}
480 481 482 483

	get iconClasses(): string | undefined {
		return this.tunnelType === TunnelType.Detected || this.tunnelType === TunnelType.Forwarded ? Codicon.plug.classNames : undefined;
	}
A
Alex Ross 已提交
484 485 486 487
}

export const TunnelTypeContextKey = new RawContextKey<TunnelType>('tunnelType', TunnelType.Add);
export const TunnelCloseableContextKey = new RawContextKey<boolean>('tunnelCloseable', false);
488 489 490
const TunnelViewFocusContextKey = new RawContextKey<boolean>('tunnelViewFocus', false);
const TunnelViewSelectionKeyName = 'tunnelViewSelection';
const TunnelViewSelectionContextKey = new RawContextKey<ITunnelItem | undefined>(TunnelViewSelectionKeyName, undefined);
491
const PortChangableContextKey = new RawContextKey<boolean>('portChangable', false);
A
Alex Ross 已提交
492

M
Matt Bierner 已提交
493 494
class TunnelDataTree extends WorkbenchAsyncDataTree<any, any, any> { }

S
SteVen Batten 已提交
495
export class TunnelPanel extends ViewPane {
A
Alex Ross 已提交
496
	static readonly ID = TUNNEL_VIEW_ID;
A
Alex Ross 已提交
497
	static readonly TITLE = nls.localize('remote.tunnel', "Ports");
M
Matt Bierner 已提交
498
	private tree!: TunnelDataTree;
A
Alex Ross 已提交
499 500
	private tunnelTypeContext: IContextKey<TunnelType>;
	private tunnelCloseableContext: IContextKey<boolean>;
501 502
	private tunnelViewFocusContext: IContextKey<boolean>;
	private tunnelViewSelectionContext: IContextKey<ITunnelItem | undefined>;
503
	private portChangableContextKey: IContextKey<boolean>;
A
Alex Ross 已提交
504
	private isEditing: boolean = false;
A
Alex Ross 已提交
505 506 507 508 509
	private titleActions: IAction[] = [];
	private readonly titleActionsDisposable = this._register(new MutableDisposable());

	constructor(
		protected viewModel: ITunnelViewModel,
S
SteVen Batten 已提交
510
		options: IViewPaneOptions,
A
Alex Ross 已提交
511 512 513 514 515
		@IKeybindingService protected keybindingService: IKeybindingService,
		@IContextMenuService protected contextMenuService: IContextMenuService,
		@IContextKeyService protected contextKeyService: IContextKeyService,
		@IConfigurationService protected configurationService: IConfigurationService,
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
516
		@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
517
		@IOpenerService openerService: IOpenerService,
A
Alex Ross 已提交
518 519 520
		@IQuickInputService protected quickInputService: IQuickInputService,
		@ICommandService protected commandService: ICommandService,
		@IMenuService private readonly menuService: IMenuService,
521
		@IContextViewService private readonly contextViewService: IContextViewService,
522
		@IThemeService themeService: IThemeService,
523 524
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
		@ITelemetryService telemetryService: ITelemetryService,
A
Alex Ross 已提交
525
	) {
526
		super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
A
Alex Ross 已提交
527 528
		this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService);
		this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService);
529 530
		this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService);
		this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService);
531
		this.portChangableContextKey = PortChangableContextKey.bindTo(contextKeyService);
A
Alex Ross 已提交
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551

		const scopedContextKeyService = this._register(this.contextKeyService.createScoped());
		scopedContextKeyService.createKey('view', TunnelPanel.ID);

		const titleMenu = this._register(this.menuService.createMenu(MenuId.TunnelTitle, scopedContextKeyService));
		const updateActions = () => {
			this.titleActions = [];
			this.titleActionsDisposable.value = createAndFillInActionBarActions(titleMenu, undefined, this.titleActions);
			this.updateActions();
		};

		this._register(titleMenu.onDidChange(updateActions));
		updateActions();

		this._register(toDisposable(() => {
			this.titleActions = [];
		}));
	}

	protected renderBody(container: HTMLElement): void {
J
Joao Moreno 已提交
552 553
		super.renderBody(container);

A
Alex Ross 已提交
554 555
		const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view'));
		const treeContainer = dom.append(panelContainer, dom.$('.customview-tree'));
556
		treeContainer.classList.add('ports-view');
557
		treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');
A
Alex Ross 已提交
558

559
		const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
M
Matt Bierner 已提交
560
		this.tree = this.instantiationService.createInstance(TunnelDataTree,
A
Alex Ross 已提交
561 562 563 564 565 566 567 568 569 570 571 572 573 574
			'RemoteTunnels',
			treeContainer,
			new TunnelTreeVirtualDelegate(),
			[renderer],
			new TunnelDataSource(),
			{
				collapseByDefault: (e: ITunnelItem | ITunnelGroup): boolean => {
					return false;
				},
				keyboardNavigationLabelProvider: {
					getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => {
						return item.label;
					}
				},
575 576 577 578 579 580 581 582 583 584 585 586
				multipleSelectionSupport: false,
				accessibilityProvider: {
					getAriaLabel: (item: ITunnelItem | ITunnelGroup) => {
						if (item instanceof TunnelItem) {
							if (item.localAddress) {
								return nls.localize('remote.tunnel.ariaLabelForwarded', "Remote port {0}:{1} forwarded to local address {2}", item.remoteHost, item.remotePort, item.localAddress);
							} else {
								return nls.localize('remote.tunnel.ariaLabelCandidate', "Remote port {0}:{1} not forwarded", item.remoteHost, item.remotePort);
							}
						} else {
							return item.label;
						}
I
isidor 已提交
587 588
					},
					getWidgetAriaLabel: () => nls.localize('tunnelView', "Tunnel View")
J
João Moreno 已提交
589
				}
A
Alex Ross 已提交
590 591 592 593 594 595
			}
		);
		const actionRunner: ActionRunner = new ActionRunner();
		renderer.actionRunner = actionRunner;

		this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner)));
596
		this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
597 598 599
		this._register(this.tree.onDidChangeFocus(e => this.onFocusChanged(e.elements)));
		this._register(this.tree.onDidFocus(() => this.tunnelViewFocusContext.set(true)));
		this._register(this.tree.onDidBlur(() => this.tunnelViewFocusContext.set(false)));
A
Alex Ross 已提交
600 601 602

		this.tree.setInput(this.viewModel);
		this._register(this.viewModel.onForwardedPortsChanged(() => {
A
Alex Ross 已提交
603
			this._onDidChangeViewWelcomeState.fire();
A
Alex Ross 已提交
604 605 606
			this.tree.updateChildren(undefined, true);
		}));

607
		this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(e => {
608
			if (e.element && (e.element.tunnelType === TunnelType.Add)) {
609
				this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
A
Alex Ross 已提交
610 611
			}
		}));
612 613

		this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
A
Alex Ross 已提交
614 615
			this.isEditing = !!this.remoteExplorerService.getEditableData(e);
			this._onDidChangeViewWelcomeState.fire();
616

A
Alex Ross 已提交
617
			if (!this.isEditing) {
618
				treeContainer.classList.remove('highlight');
619 620 621 622
			}

			await this.tree.updateChildren(undefined, false);

A
Alex Ross 已提交
623
			if (this.isEditing) {
624
				treeContainer.classList.add('highlight');
A
Alex Ross 已提交
625 626 627 628
				if (!e) {
					// When we are in editing mode for a new forward, rather than updating an existing one we need to reveal the input box since it might be out of view.
					this.tree.reveal(this.viewModel.input);
				}
629 630 631 632
			} else {
				this.tree.domFocus();
			}
		}));
A
Alex Ross 已提交
633 634 635
	}

	private get contributedContextMenu(): IMenu {
636
		const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService));
A
Alex Ross 已提交
637 638 639
		return contributedContextMenu;
	}

A
Alex Ross 已提交
640
	shouldShowWelcome(): boolean {
641 642
		return (this.viewModel.forwarded.length === 0) && (this.viewModel.candidates.length === 0) &&
			(this.viewModel.detected.length === 0) && !this.isEditing;
A
Alex Ross 已提交
643 644
	}

A
Alex Ross 已提交
645 646 647 648 649
	focus(): void {
		super.focus();
		this.tree.domFocus();
	}

650 651 652 653
	private onFocusChanged(elements: ITunnelItem[]) {
		const item = elements && elements.length ? elements[0] : undefined;
		if (item) {
			this.tunnelViewSelectionContext.set(item);
654
			this.tunnelTypeContext.set(item.tunnelType);
655
			this.tunnelCloseableContext.set(!!item.closeable);
656
			this.portChangableContextKey.set(!!item.localPort);
657
		} else {
658
			this.tunnelTypeContext.reset();
659 660
			this.tunnelViewSelectionContext.reset();
			this.tunnelCloseableContext.reset();
661
			this.portChangableContextKey.reset();
662 663 664
		}
	}

A
Alex Ross 已提交
665
	private onContextMenu(treeEvent: ITreeContextMenuEvent<ITunnelItem | ITunnelGroup>, actionRunner: ActionRunner): void {
666
		if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) {
A
Alex Ross 已提交
667 668 669 670 671 672 673 674
			return;
		}
		const node: ITunnelItem | null = treeEvent.element;
		const event: UIEvent = treeEvent.browserEvent;

		event.preventDefault();
		event.stopPropagation();

675 676 677 678
		if (node) {
			this.tree!.setFocus([node]);
			this.tunnelTypeContext.set(node.tunnelType);
			this.tunnelCloseableContext.set(!!node.closeable);
679
			this.portChangableContextKey.set(!!node.localPort);
680 681
		} else {
			this.tunnelTypeContext.set(TunnelType.Add);
682
			this.tunnelCloseableContext.set(false);
683
			this.portChangableContextKey.set(false);
684
		}
A
Alex Ross 已提交
685 686

		const actions: IAction[] = [];
687
		this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions));
A
Alex Ross 已提交
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708

		this.contextMenuService.showContextMenu({
			getAnchor: () => treeEvent.anchor,
			getActions: () => actions,
			getActionViewItem: (action) => {
				const keybinding = this.keybindingService.lookupKeybinding(action.id);
				if (keybinding) {
					return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });
				}
				return undefined;
			},
			onHide: (wasCancelled?: boolean) => {
				if (wasCancelled) {
					this.tree!.domFocus();
				}
			},
			getActionsContext: () => node,
			actionRunner
		});
	}

709 710 711 712 713 714
	private onMouseDblClick(e: ITreeMouseEvent<ITunnelGroup | ITunnelItem | null>): void {
		if (!e.element) {
			this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
		}
	}

A
Alex Ross 已提交
715
	protected layoutBody(height: number, width: number): void {
J
João Moreno 已提交
716
		super.layoutBody(height, width);
A
Alex Ross 已提交
717 718 719 720 721 722 723
		this.tree.layout(height, width);
	}
}

export class TunnelPanelDescriptor implements IViewDescriptor {
	readonly id = TunnelPanel.ID;
	readonly name = TunnelPanel.TITLE;
S
Sandeep Somavarapu 已提交
724
	readonly ctorDescriptor: SyncDescriptor<TunnelPanel>;
A
Alex Ross 已提交
725 726 727
	readonly canToggleVisibility = true;
	readonly hideByDefault = false;
	readonly workspace = true;
728
	// group is not actually used for views that are not extension contributed. Use order instead.
A
Alex Ross 已提交
729
	readonly group = 'details@0';
730 731
	// -500 comes from the remote explorer viewOrderDelegate
	readonly order = -500;
A
Alex Ross 已提交
732
	readonly remoteAuthority?: string | string[];
A
Alex Ross 已提交
733
	readonly canMoveView = true;
734
	readonly containerIcon = Codicon.plug;
A
Alex Ross 已提交
735 736

	constructor(viewModel: ITunnelViewModel, environmentService: IWorkbenchEnvironmentService) {
S
Sandeep Somavarapu 已提交
737
		this.ctorDescriptor = new SyncDescriptor(TunnelPanel, [viewModel]);
738
		this.remoteAuthority = environmentService.remoteAuthority ? environmentService.remoteAuthority.split('+')[0] : undefined;
A
Alex Ross 已提交
739 740 741
	}
}

J
jeanp413 已提交
742 743 744 745 746 747 748 749 750 751 752
function validationMessage(validationString: string | null): { content: string, severity: Severity } | null {
	if (!validationString) {
		return null;
	}

	return {
		content: validationString,
		severity: Severity.Error
	};
}

753 754 755
namespace LabelTunnelAction {
	export const ID = 'remote.tunnel.label';
	export const LABEL = nls.localize('remote.tunnel.label', "Set Label");
A
Alex Ross 已提交
756 757 758

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
759 760
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
			if (context instanceof TunnelItem) {
A
Alex Ross 已提交
761
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
762
				remoteExplorerService.setEditable(context, {
763
					onFinish: async (value, success) => {
764
						if (success) {
765
							remoteExplorerService.tunnelModel.name(context.remoteHost, context.remotePort, value);
766
						}
767
						remoteExplorerService.setEditable(context, null);
768 769
					},
					validationMessage: () => null,
770
					placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
771
					startingValue: context.name
772
				});
A
Alex Ross 已提交
773 774 775 776 777 778
			}
			return;
		};
	}
}

779 780 781
const invalidPortString: string = nls.localize('remote.tunnelsView.portNumberValid', "Forwarded port is invalid.");
const maxPortNumber: number = 65536;
const invalidPortNumberString: string = nls.localize('remote.tunnelsView.portNumberToHigh', "Port number must be \u2265 0 and < {0}.", maxPortNumber);
782

A
Alex Ross 已提交
783
export namespace ForwardPortAction {
784 785
	export const INLINE_ID = 'remote.tunnel.forwardInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.forwardCommandPalette';
786
	export const LABEL: ILocalizedString = { value: nls.localize('remote.tunnel.forward', "Forward a Port"), original: 'Forward a Port' };
787
	export const TREEITEM_LABEL = nls.localize('remote.tunnel.forwardItem', "Forward Port");
A
Alex Ross 已提交
788
	const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000).");
A
Alex Ross 已提交
789

A
Alex Ross 已提交
790
	function validateInput(value: string): string | null {
A
Alex Ross 已提交
791
		const parsed = parseAddress(value);
792
		if (!parsed) {
793
			return invalidPortString;
794 795
		} else if (parsed.port >= maxPortNumber) {
			return invalidPortNumberString;
A
Alex Ross 已提交
796 797 798 799
		}
		return null;
	}

A
Alex Ross 已提交
800 801
	function error(notificationService: INotificationService, tunnel: RemoteTunnel | void, host: string, port: number) {
		if (!tunnel) {
802
			notificationService.warn(nls.localize('remote.tunnel.forwardError', "Unable to forward {0}:{1}. The host may not be available or that remote port may already be forwarded", host, port));
A
Alex Ross 已提交
803 804 805
		}
	}

806
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
807 808
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
809
			const notificationService = accessor.get(INotificationService);
A
Alex Ross 已提交
810
			if (arg instanceof TunnelItem) {
A
Alex Ross 已提交
811
				remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }).then(tunnel => error(notificationService, tunnel, arg.remoteHost, arg.remotePort));
812 813
			} else {
				remoteExplorerService.setEditable(undefined, {
814
					onFinish: async (value, success) => {
815
						let parsed: { host: string, port: number } | undefined;
A
Alex Ross 已提交
816
						if (success && (parsed = parseAddress(value))) {
A
Alex Ross 已提交
817
							remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
818
						}
819
						remoteExplorerService.setEditable(undefined, null);
A
Alex Ross 已提交
820
					},
J
jeanp413 已提交
821
					validationMessage: (value) => validationMessage(validateInput(value)),
A
Alex Ross 已提交
822
					placeholder: forwardPrompt
A
Alex Ross 已提交
823
				});
824 825 826 827 828 829 830
			}
		};
	}

	export function commandPaletteHandler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
831
			const notificationService = accessor.get(INotificationService);
832 833 834 835 836 837 838 839
			const viewsService = accessor.get(IViewsService);
			const quickInputService = accessor.get(IQuickInputService);
			await viewsService.openView(TunnelPanel.ID, true);
			const value = await quickInputService.input({
				prompt: forwardPrompt,
				validateInput: (value) => Promise.resolve(validateInput(value))
			});
			let parsed: { host: string, port: number } | undefined;
A
Alex Ross 已提交
840
			if (value && (parsed = parseAddress(value))) {
A
Alex Ross 已提交
841
				remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
842 843 844 845 846
			}
		};
	}
}

A
Alex Ross 已提交
847
interface QuickPickTunnel extends IQuickPickItem {
A
Alex Ross 已提交
848
	tunnel?: ITunnelItem
A
Alex Ross 已提交
849 850
}

A
Alex Ross 已提交
851 852 853 854 855 856 857 858
function makeTunnelPicks(tunnels: Tunnel[]): QuickPickInput<QuickPickTunnel>[] {
	const picks: QuickPickInput<QuickPickTunnel>[] = tunnels.map(forwarded => {
		const item = TunnelItem.createFromTunnel(forwarded);
		return {
			label: item.label,
			description: item.description,
			tunnel: item
		};
A
Alex Ross 已提交
859 860 861
	});
	if (picks.length === 0) {
		picks.push({
862
			label: nls.localize('remote.tunnel.closeNoPorts', "No ports currently forwarded. Try running the {0} command", ForwardPortAction.LABEL.value)
A
Alex Ross 已提交
863 864 865 866 867
		});
	}
	return picks;
}

A
Alex Ross 已提交
868
namespace ClosePortAction {
A
Alex Ross 已提交
869 870
	export const INLINE_ID = 'remote.tunnel.closeInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.closeCommandPalette';
871
	export const LABEL: ILocalizedString = { value: nls.localize('remote.tunnel.close', "Stop Forwarding Port"), original: 'Stop Forwarding Port' };
A
Alex Ross 已提交
872

A
Alex Ross 已提交
873
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
874
		return async (accessor, arg) => {
875
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
876
			if (context instanceof TunnelItem) {
A
Alex Ross 已提交
877
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
878
				await remoteExplorerService.close({ host: context.remoteHost, port: context.remotePort });
A
Alex Ross 已提交
879 880 881
			}
		};
	}
A
Alex Ross 已提交
882 883 884 885 886 887 888

	export function commandPaletteHandler(): ICommandHandler {
		return async (accessor) => {
			const quickInputService = accessor.get(IQuickInputService);
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
			const commandService = accessor.get(ICommandService);

A
Alex Ross 已提交
889
			const picks: QuickPickInput<QuickPickTunnel>[] = makeTunnelPicks(Array.from(remoteExplorerService.tunnelModel.forwarded.values()).filter(tunnel => tunnel.closeable));
A
Alex Ross 已提交
890 891 892 893 894 895 896 897
			const result = await quickInputService.pick(picks, { placeHolder: nls.localize('remote.tunnel.closePlaceholder', "Choose a port to stop forwarding") });
			if (result && result.tunnel) {
				await remoteExplorerService.close({ host: result.tunnel.remoteHost, port: result.tunnel.remotePort });
			} else if (result) {
				await commandService.executeCommand(ForwardPortAction.COMMANDPALETTE_ID);
			}
		};
	}
A
Alex Ross 已提交
898 899
}

900
export namespace OpenPortInBrowserAction {
A
Alex Ross 已提交
901 902 903 904 905
	export const ID = 'remote.tunnel.open';
	export const LABEL = nls.localize('remote.tunnel.open', "Open in Browser");

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
A
Alex Ross 已提交
906
			let key: string | undefined;
A
Alex Ross 已提交
907
			if (arg instanceof TunnelItem) {
A
Alex Ross 已提交
908 909 910 911 912
				key = makeAddress(arg.remoteHost, arg.remotePort);
			} else if (arg.tunnelRemoteHost && arg.tunnelRemotePort) {
				key = makeAddress(arg.tunnelRemoteHost, arg.tunnelRemotePort);
			}
			if (key) {
A
Alex Ross 已提交
913 914
				const model = accessor.get(IRemoteExplorerService).tunnelModel;
				const openerService = accessor.get(IOpenerService);
915
				return run(model, openerService, key);
A
Alex Ross 已提交
916 917 918
			}
		};
	}
919 920 921 922 923

	export function run(model: TunnelModel, openerService: IOpenerService, key: string) {
		const tunnel = model.forwarded.get(key) || model.detected.get(key);
		let address: string | undefined;
		if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) {
924
			if (!address.startsWith('http')) {
A
Alex Ross 已提交
925
				address = `http://${address}`;
926 927
			}
			return openerService.open(URI.parse(address));
928 929 930
		}
		return Promise.resolve();
	}
A
Alex Ross 已提交
931 932
}

A
Alex Ross 已提交
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956
namespace OpenPortInBrowserCommandPaletteAction {
	export const ID = 'remote.tunnel.openCommandPalette';
	export const LABEL = nls.localize('remote.tunnel.openCommandPalette', "Open Port in Browser");

	interface QuickPickTunnel extends IQuickPickItem {
		tunnel?: TunnelItem;
	}

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			const model = accessor.get(IRemoteExplorerService).tunnelModel;
			const quickPickService = accessor.get(IQuickInputService);
			const openerService = accessor.get(IOpenerService);
			const commandService = accessor.get(ICommandService);
			const options: QuickPickTunnel[] = [...model.forwarded, ...model.detected].map(value => {
				const tunnelItem = TunnelItem.createFromTunnel(value[1]);
				return {
					label: tunnelItem.label,
					description: tunnelItem.description,
					tunnel: tunnelItem
				};
			});
			if (options.length === 0) {
				options.push({
957 958 959 960 961
					label: nls.localize('remote.tunnel.openCommandPaletteNone', "No ports currently forwarded. Open the Ports view to get started.")
				});
			} else {
				options.push({
					label: nls.localize('remote.tunnel.openCommandPaletteView', "Open the Ports view...")
A
Alex Ross 已提交
962 963 964 965 966 967 968 969 970 971 972 973
				});
			}
			const picked = await quickPickService.pick<QuickPickTunnel>(options, { placeHolder: nls.localize('remote.tunnel.openCommandPalettePick', "Choose the port to open") });
			if (picked && picked.tunnel) {
				return OpenPortInBrowserAction.run(model, openerService, makeAddress(picked.tunnel.remoteHost, picked.tunnel.remotePort));
			} else if (picked) {
				return commandService.executeCommand(`${TUNNEL_VIEW_ID}.focus`);
			}
		};
	}
}

A
Alex Ross 已提交
974
namespace CopyAddressAction {
A
Alex Ross 已提交
975 976 977 978 979 980 981 982 983 984 985
	export const INLINE_ID = 'remote.tunnel.copyAddressInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.copyAddressCommandPalette';
	export const INLINE_LABEL = nls.localize('remote.tunnel.copyAddressInline', "Copy Address");
	export const COMMANDPALETTE_LABEL = nls.localize('remote.tunnel.copyAddressCommandPalette', "Copy Forwarded Port Address");

	async function copyAddress(remoteExplorerService: IRemoteExplorerService, clipboardService: IClipboardService, tunnelItem: ITunnelItem) {
		const address = remoteExplorerService.tunnelModel.address(tunnelItem.remoteHost, tunnelItem.remotePort);
		if (address) {
			await clipboardService.writeText(address.toString());
		}
	}
A
Alex Ross 已提交
986

A
Alex Ross 已提交
987
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
988
		return async (accessor, arg) => {
989 990 991
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
			if (context instanceof TunnelItem) {
				return copyAddress(accessor.get(IRemoteExplorerService), accessor.get(IClipboardService), context);
A
Alex Ross 已提交
992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
			}
		};
	}

	export function commandPaletteHandler(): ICommandHandler {
		return async (accessor, arg) => {
			const quickInputService = accessor.get(IQuickInputService);
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
			const commandService = accessor.get(ICommandService);
			const clipboardService = accessor.get(IClipboardService);

			const tunnels = Array.from(remoteExplorerService.tunnelModel.forwarded.values()).concat(Array.from(remoteExplorerService.tunnelModel.detected.values()));
			const result = await quickInputService.pick(makeTunnelPicks(tunnels), { placeHolder: nls.localize('remote.tunnel.copyAddressPlaceholdter', "Choose a forwarded port") });
			if (result && result.tunnel) {
				await copyAddress(remoteExplorerService, clipboardService, result.tunnel);
			} else if (result) {
				await commandService.executeCommand(ForwardPortAction.COMMANDPALETTE_ID);
A
Alex Ross 已提交
1009 1010 1011 1012 1013
			}
		};
	}
}

1014 1015 1016 1017 1018 1019 1020
namespace ChangeLocalPortAction {
	export const ID = 'remote.tunnel.changeLocalPort';
	export const LABEL = nls.localize('remote.tunnel.changeLocalPort', "Change Local Port");

	function validateInput(value: string): string | null {
		if (!value.match(/^[0-9]+$/)) {
			return invalidPortString;
1021 1022
		} else if (Number(value) >= maxPortNumber) {
			return invalidPortNumberString;
1023 1024 1025 1026 1027 1028 1029
		}
		return null;
	}

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
1030
			const notificationService = accessor.get(INotificationService);
1031 1032 1033 1034 1035 1036 1037
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
			if (context instanceof TunnelItem) {
				remoteExplorerService.setEditable(context, {
					onFinish: async (value, success) => {
						remoteExplorerService.setEditable(context, null);
						if (success) {
							await remoteExplorerService.close({ host: context.remoteHost, port: context.remotePort });
1038
							const numberValue = Number(value);
1039
							const newForward = await remoteExplorerService.forward({ host: context.remoteHost, port: context.remotePort }, numberValue, context.name);
1040
							if (newForward && newForward.tunnelLocalPort !== numberValue) {
1041
								notificationService.warn(nls.localize('remote.tunnel.changeLocalPortNumber', "The local port {0} is not available. Port number {1} has been used instead", value, newForward.tunnelLocalPort ?? newForward.localAddress));
1042
							}
1043 1044
						}
					},
J
jeanp413 已提交
1045
					validationMessage: (value) => validationMessage(validateInput(value)),
1046 1047 1048 1049 1050 1051 1052
					placeholder: nls.localize('remote.tunnelsView.changePort', "New local port")
				});
			}
		};
	}
}

1053 1054
const tunnelViewCommandsWeightBonus = 10; // give our commands a little bit more weight over other default list/tree commands

1055 1056
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: LabelTunnelAction.ID,
1057
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
1058 1059 1060 1061 1062 1063 1064
	when: ContextKeyExpr.and(TunnelViewFocusContextKey, TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)),
	primary: KeyCode.F2,
	mac: {
		primary: KeyCode.Enter
	},
	handler: LabelTunnelAction.handler()
});
1065 1066
CommandsRegistry.registerCommand(ForwardPortAction.INLINE_ID, ForwardPortAction.inlineHandler());
CommandsRegistry.registerCommand(ForwardPortAction.COMMANDPALETTE_ID, ForwardPortAction.commandPaletteHandler());
1067 1068
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: ClosePortAction.INLINE_ID,
1069
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
1070 1071 1072 1073 1074 1075 1076 1077
	when: ContextKeyExpr.and(TunnelCloseableContextKey, TunnelViewFocusContextKey),
	primary: KeyCode.Delete,
	mac: {
		primary: KeyMod.CtrlCmd | KeyCode.Backspace
	},
	handler: ClosePortAction.inlineHandler()
});

A
Alex Ross 已提交
1078
CommandsRegistry.registerCommand(ClosePortAction.COMMANDPALETTE_ID, ClosePortAction.commandPaletteHandler());
A
Alex Ross 已提交
1079
CommandsRegistry.registerCommand(OpenPortInBrowserAction.ID, OpenPortInBrowserAction.handler());
A
Alex Ross 已提交
1080
CommandsRegistry.registerCommand(OpenPortInBrowserCommandPaletteAction.ID, OpenPortInBrowserCommandPaletteAction.handler());
1081 1082
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: CopyAddressAction.INLINE_ID,
1083
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
1084 1085 1086 1087
	when: ContextKeyExpr.or(ContextKeyExpr.and(TunnelViewFocusContextKey, TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)), ContextKeyExpr.and(TunnelViewFocusContextKey, TunnelTypeContextKey.isEqualTo(TunnelType.Detected))),
	primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
	handler: CopyAddressAction.inlineHandler()
});
A
Alex Ross 已提交
1088
CommandsRegistry.registerCommand(CopyAddressAction.COMMANDPALETTE_ID, CopyAddressAction.commandPaletteHandler());
1089
CommandsRegistry.registerCommand(ChangeLocalPortAction.ID, ChangeLocalPortAction.handler());
A
Alex Ross 已提交
1090

A
Alex Ross 已提交
1091 1092 1093 1094 1095 1096 1097
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
		id: ClosePortAction.COMMANDPALETTE_ID,
		title: ClosePortAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
1098 1099
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
1100
		id: ForwardPortAction.COMMANDPALETTE_ID,
1101 1102 1103 1104
		title: ForwardPortAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
A
Alex Ross 已提交
1105 1106 1107 1108 1109 1110 1111
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
		id: CopyAddressAction.COMMANDPALETTE_ID,
		title: CopyAddressAction.COMMANDPALETTE_LABEL
	},
	when: forwardedPortsViewEnabled
}));
A
Alex Ross 已提交
1112 1113 1114 1115 1116 1117 1118
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
		id: OpenPortInBrowserCommandPaletteAction.ID,
		title: OpenPortInBrowserCommandPaletteAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131
registerAction2(class extends Action2 {
	constructor() {
		super({
			id: ForwardPortAction.INLINE_ID,
			title: ForwardPortAction.LABEL,
			icon: Codicon.plus,
			menu: [{
				id: MenuId.ViewTitle,
				group: 'navigation',
				order: 0,
				when: ContextKeyEqualsExpr.create('view', TUNNEL_VIEW_ID),
			}]
		});
A
Alex Ross 已提交
1132
	}
1133 1134 1135 1136 1137
	run(accessor: ServicesAccessor, ...args: any[]) {
		return ForwardPortAction.inlineHandler()(accessor, args);
	}
});

A
Alex Ross 已提交
1138 1139 1140 1141
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 0,
	command: {
A
Alex Ross 已提交
1142 1143
		id: CopyAddressAction.INLINE_ID,
		title: CopyAddressAction.INLINE_LABEL,
A
Alex Ross 已提交
1144
	},
A
Alex Ross 已提交
1145
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
1146 1147 1148 1149 1150 1151 1152 1153
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
	},
A
Alex Ross 已提交
1154
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
1155 1156 1157 1158 1159
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 2,
	command: {
1160 1161
		id: LabelTunnelAction.ID,
		title: LabelTunnelAction.LABEL,
A
Alex Ross 已提交
1162 1163 1164
	},
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)
}));
1165 1166 1167 1168 1169 1170 1171
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '1_manage',
	order: 0,
	command: {
		id: ChangeLocalPortAction.ID,
		title: ChangeLocalPortAction.LABEL,
	},
1172
	when: ContextKeyExpr.and(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), PortChangableContextKey)
1173
}));
A
Alex Ross 已提交
1174 1175 1176 1177
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
1178
		id: ForwardPortAction.INLINE_ID,
1179
		title: ForwardPortAction.TREEITEM_LABEL,
A
Alex Ross 已提交
1180
	},
1181
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Candidate), TunnelTypeContextKey.isEqualTo(TunnelType.Add))
A
Alex Ross 已提交
1182 1183
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
1184 1185
	group: '1_manage',
	order: 1,
A
Alex Ross 已提交
1186
	command: {
A
Alex Ross 已提交
1187
		id: ClosePortAction.INLINE_ID,
A
Alex Ross 已提交
1188 1189 1190 1191 1192 1193 1194 1195 1196 1197
		title: ClosePortAction.LABEL,
	},
	when: TunnelCloseableContextKey
}));

MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
1198
		icon: Codicon.globe
A
Alex Ross 已提交
1199
	},
A
Alex Ross 已提交
1200
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
1201 1202 1203 1204
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
1205
		id: ForwardPortAction.INLINE_ID,
1206
		title: ForwardPortAction.TREEITEM_LABEL,
1207
		icon: Codicon.plus
A
Alex Ross 已提交
1208
	},
A
Alex Ross 已提交
1209
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate)
A
Alex Ross 已提交
1210 1211 1212 1213
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 2,
	command: {
A
Alex Ross 已提交
1214
		id: ClosePortAction.INLINE_ID,
A
Alex Ross 已提交
1215
		title: ClosePortAction.LABEL,
1216
		icon: Codicon.x
A
Alex Ross 已提交
1217 1218 1219
	},
	when: TunnelCloseableContextKey
}));