tunnelView.ts 43.8 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';
A
Alex Ross 已提交
13 14 15 16
import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
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 28
import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction, ILocalizedString, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
A
Alex Ross 已提交
29
import { IRemoteExplorerService, TunnelModel, MakeAddress, TunnelType, ITunnelItem, Tunnel } 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';
S
SteVen Batten 已提交
38
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer';
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';
A
Alex Ross 已提交
45 46

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

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 已提交
61
	readonly detected: TunnelItem[];
62
	readonly candidates: TunnelItem[];
63
	readonly input: TunnelItem;
A
Alex Ross 已提交
64
	groups(): Promise<ITunnelGroup[]>;
A
Alex Ross 已提交
65 66 67 68 69 70
}

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

	constructor(
75
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService) {
A
Alex Ross 已提交
76 77 78 79 80
		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()));
81
		this._register(this.model.onCandidatesChanged(() => this._onForwardedPortsChanged.fire()));
82 83 84 85 86 87 88
		this._input = {
			label: nls.localize('remote.tunnelsView.add', "Forward a Port..."),
			tunnelType: TunnelType.Add,
			remoteHost: 'localhost',
			remotePort: 0,
			description: ''
		};
A
Alex Ross 已提交
89 90
	}

A
Alex Ross 已提交
91
	async groups(): Promise<ITunnelGroup[]> {
A
Alex Ross 已提交
92
		const groups: ITunnelGroup[] = [];
93 94 95 96
		this._candidates = new Map();
		(await this.model.candidates).forEach(candidate => {
			this._candidates.set(MakeAddress(candidate.host, candidate.port), candidate);
		});
97
		if ((this.model.forwarded.size > 0) || this.remoteExplorerService.getEditableData(undefined)) {
A
Alex Ross 已提交
98 99 100 101 102 103
			groups.push({
				label: nls.localize('remote.tunnelsView.forwarded', "Forwarded"),
				tunnelType: TunnelType.Forwarded,
				items: this.forwarded
			});
		}
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
			});
		}
A
Alex Ross 已提交
111 112
		const candidates = await this.candidates;
		if (candidates.length > 0) {
A
Alex Ross 已提交
113
			groups.push({
114
				label: nls.localize('remote.tunnelsView.candidates', "Not Forwarded"),
A
Alex Ross 已提交
115 116 117 118
				tunnelType: TunnelType.Candidate,
				items: candidates
			});
		}
119 120
		if (groups.length === 0) {
			groups.push(this._input);
121
		}
A
Alex Ross 已提交
122 123 124
		return groups;
	}

125 126 127 128 129 130 131
	private addProcessInfoFromCandidate(tunnelItem: ITunnelItem) {
		const key = MakeAddress(tunnelItem.remoteHost, tunnelItem.remotePort);
		if (this._candidates.has(key)) {
			tunnelItem.description = this._candidates.get(key)!.detail;
		}
	}

A
Alex Ross 已提交
132
	get forwarded(): TunnelItem[] {
133
		const forwarded = Array.from(this.model.forwarded.values()).map(tunnel => {
134 135 136
			const tunnelItem = TunnelItem.createFromTunnel(tunnel);
			this.addProcessInfoFromCandidate(tunnelItem);
			return tunnelItem;
A
Alex Ross 已提交
137 138 139 140 141 142
		}).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 已提交
143
		});
144 145 146 147
		if (this.remoteExplorerService.getEditableData(undefined)) {
			forwarded.push(this._input);
		}
		return forwarded;
A
Alex Ross 已提交
148 149
	}

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

158 159 160
	get candidates(): TunnelItem[] {
		const candidates: TunnelItem[] = [];
		this._candidates.forEach(value => {
161
			let key = MakeAddress(value.host, value.port);
162
			if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) {
163 164 165 166 167 168 169 170 171 172
				// The host:port hasn't been forwarded or detected. However, if the candidate is 0.0.0.0,
				// also check that the port hasn't already been forwarded with localhost, and vice versa.
				// For example: no need to show 0.0.0.0:3000 as a candidate if localhost:3000 is already forwarded.
				const otherHost = value.host === '0.0.0.0' ? 'localhost' : (value.host === 'localhost' ? '0.0.0.0' : undefined);
				if (otherHost) {
					key = MakeAddress(otherHost, value.port);
					if (this.model.forwarded.has(key) || this.model.detected.has(key)) {
						return;
					}
				}
173
				candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, undefined, false, undefined, value.detail));
174
			}
A
Alex Ross 已提交
175
		});
176
		return candidates;
A
Alex Ross 已提交
177 178
	}

179
	get input(): TunnelItem {
180 181 182
		return this._input;
	}

A
Alex Ross 已提交
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
	dispose() {
		super.dispose();
	}
}

interface ITunnelTemplateData {
	elementDisposable: IDisposable;
	container: HTMLElement;
	iconLabel: IconLabel;
	actionBar: ActionBar;
}

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

	private _actionRunner: ActionRunner | undefined;

	constructor(
		private readonly viewId: string,
		@IMenuService private readonly menuService: IMenuService,
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
205 206 207 208
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
A
Alex Ross 已提交
209 210 211 212 213 214 215 216 217 218 219 220 221
	) {
		super();
	}

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

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

	renderTemplate(container: HTMLElement): ITunnelTemplateData {
222
		container.classList.add('custom-view-tree-node-item');
A
Alex Ross 已提交
223 224 225 226 227 228
		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) {
229
					return this.instantiationService.createInstance(MenuEntryActionViewItem, action);
230 231
				} else if (action instanceof SubmenuItemAction) {
					return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action);
A
Alex Ross 已提交
232 233 234 235 236 237 238 239 240 241
				}

				return undefined;
			}
		});

		return { iconLabel, actionBar, container, elementDisposable: Disposable.None };
	}

	private isTunnelItem(item: ITunnelGroup | ITunnelItem): item is ITunnelItem {
242
		return !!((<ITunnelItem>item).remotePort);
A
Alex Ross 已提交
243 244 245 246 247
	}

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

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

270
	private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) {
271 272
		const label = node.label + (node.description ? (' - ' + node.description) : '');
		templateData.iconLabel.setLabel(node.label, node.description, { title: label, extraClasses: ['tunnel-view-label'] });
273
		templateData.actionBar.context = node;
274
		const contextKeyService = this._register(this.contextKeyService.createScoped());
275 276 277
		contextKeyService.createKey('view', this.viewId);
		contextKeyService.createKey('tunnelType', node.tunnelType);
		contextKeyService.createKey('tunnelCloseable', node.closeable);
278 279 280
		const disposableStore = new DisposableStore();
		templateData.elementDisposable = disposableStore;
		const menu = disposableStore.add(this.menuService.createMenu(MenuId.TunnelInline, contextKeyService));
281
		const actions: IAction[] = [];
282
		disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
283 284 285 286 287 288 289 290
		if (actions) {
			templateData.actionBar.push(actions, { icon: true, label: false });
			if (this._actionRunner) {
				templateData.actionBar.actionRunner = this._actionRunner;
			}
		}
	}

A
Alex Ross 已提交
291
	private renderInputBox(container: HTMLElement, editableData: IEditableData): IDisposable {
292 293 294 295 296
		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) => {
297 298
					const message = editableData.validationMessage(value);
					if (!message || message.severity !== Severity.Error) {
299 300 301 302
						return null;
					}

					return {
303
						content: message.content,
304 305 306 307 308 309 310 311 312 313 314
						formatContent: true,
						type: MessageType.ERROR
					};
				}
			},
			placeholder: editableData.placeholder || ''
		});
		const styler = attachInputBoxStyler(inputBox, this.themeService);

		inputBox.value = value;
		inputBox.focus();
315
		inputBox.select({ start: 0, end: editableData.startingValue ? editableData.startingValue.length : 0 });
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347

		const done = once((success: boolean, finishEditing: boolean) => {
			inputBox.element.style.display = 'none';
			const value = inputBox.value;
			dispose(toDispose);
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
		});

		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 已提交
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
	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;
	}

	getChildren(element: ITunnelViewModel | ITunnelItem | ITunnelGroup) {
		if (element instanceof TunnelViewModel) {
A
Alex Ross 已提交
372
			return element.groups();
A
Alex Ross 已提交
373 374 375 376 377 378 379 380 381 382 383 384
		} 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 已提交
385
	items?: ITunnelItem[] | Promise<ITunnelItem[]>;
A
Alex Ross 已提交
386 387 388
}

class TunnelItem implements ITunnelItem {
A
Alex Ross 已提交
389
	static createFromTunnel(tunnel: Tunnel, type: TunnelType = TunnelType.Forwarded, closeable?: boolean) {
390
		return new TunnelItem(type, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.localPort, closeable === undefined ? tunnel.closeable : closeable, tunnel.name, tunnel.description);
A
Alex Ross 已提交
391 392
	}

A
Alex Ross 已提交
393 394
	constructor(
		public tunnelType: TunnelType,
395 396
		public remoteHost: string,
		public remotePort: number,
A
Alex Ross 已提交
397
		public localAddress?: string,
398
		public localPort?: number,
A
Alex Ross 已提交
399 400 401 402 403
		public closeable?: boolean,
		public name?: string,
		private _description?: string,
	) { }
	get label(): string {
A
Alex Ross 已提交
404
		if (this.name) {
A
Alex Ross 已提交
405
			return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name);
406
		} else if (this.localAddress && (this.remoteHost !== 'localhost')) {
407
			return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}:{1} \u2192 {2}", this.remoteHost, this.remotePort, this.localAddress);
A
Alex Ross 已提交
408
		} else if (this.localAddress) {
409
			return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} \u2192 {1}", this.remotePort, this.localAddress);
410
		} else if (this.remoteHost !== 'localhost') {
411
			return nls.localize('remote.tunnelsView.forwardedPortLabel4', "{0}:{1}", this.remoteHost, this.remotePort);
A
Alex Ross 已提交
412
		} else {
413
			return nls.localize('remote.tunnelsView.forwardedPortLabel5', "{0}", this.remotePort);
A
Alex Ross 已提交
414 415 416
		}
	}

417 418 419 420
	set description(description: string | undefined) {
		this._description = description;
	}

A
Alex Ross 已提交
421
	get description(): string | undefined {
A
Alex Ross 已提交
422
		if (this._description) {
A
Alex Ross 已提交
423
			return this._description;
A
Alex Ross 已提交
424
		} else if (this.name) {
425
			return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remotePort, this.localAddress);
A
Alex Ross 已提交
426
		}
A
Alex Ross 已提交
427
		return undefined;
A
Alex Ross 已提交
428 429 430 431 432
	}
}

export const TunnelTypeContextKey = new RawContextKey<TunnelType>('tunnelType', TunnelType.Add);
export const TunnelCloseableContextKey = new RawContextKey<boolean>('tunnelCloseable', false);
433 434 435
const TunnelViewFocusContextKey = new RawContextKey<boolean>('tunnelViewFocus', false);
const TunnelViewSelectionKeyName = 'tunnelViewSelection';
const TunnelViewSelectionContextKey = new RawContextKey<ITunnelItem | undefined>(TunnelViewSelectionKeyName, undefined);
436
const PortChangableContextKey = new RawContextKey<boolean>('portChangable', false);
A
Alex Ross 已提交
437

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

S
SteVen Batten 已提交
440
export class TunnelPanel extends ViewPane {
441 442
	static readonly ID = '~remote.forwardedPorts';
	static readonly TITLE = nls.localize('remote.tunnel', "Forwarded Ports");
M
Matt Bierner 已提交
443
	private tree!: TunnelDataTree;
A
Alex Ross 已提交
444 445
	private tunnelTypeContext: IContextKey<TunnelType>;
	private tunnelCloseableContext: IContextKey<boolean>;
446 447
	private tunnelViewFocusContext: IContextKey<boolean>;
	private tunnelViewSelectionContext: IContextKey<ITunnelItem | undefined>;
448
	private portChangableContextKey: IContextKey<boolean>;
A
Alex Ross 已提交
449 450 451 452 453 454

	private titleActions: IAction[] = [];
	private readonly titleActionsDisposable = this._register(new MutableDisposable());

	constructor(
		protected viewModel: ITunnelViewModel,
S
SteVen Batten 已提交
455
		options: IViewPaneOptions,
A
Alex Ross 已提交
456 457 458 459 460
		@IKeybindingService protected keybindingService: IKeybindingService,
		@IContextMenuService protected contextMenuService: IContextMenuService,
		@IContextKeyService protected contextKeyService: IContextKeyService,
		@IConfigurationService protected configurationService: IConfigurationService,
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
461
		@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
462
		@IOpenerService openerService: IOpenerService,
A
Alex Ross 已提交
463 464 465
		@IQuickInputService protected quickInputService: IQuickInputService,
		@ICommandService protected commandService: ICommandService,
		@IMenuService private readonly menuService: IMenuService,
466
		@IContextViewService private readonly contextViewService: IContextViewService,
467
		@IThemeService themeService: IThemeService,
468 469
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
		@ITelemetryService telemetryService: ITelemetryService,
A
Alex Ross 已提交
470
	) {
471
		super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
A
Alex Ross 已提交
472 473
		this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService);
		this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService);
474 475
		this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService);
		this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService);
476
		this.portChangableContextKey = PortChangableContextKey.bindTo(contextKeyService);
A
Alex Ross 已提交
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497

		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 已提交
498 499
		super.renderBody(container);

A
Alex Ross 已提交
500 501
		const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view'));
		const treeContainer = dom.append(panelContainer, dom.$('.customview-tree'));
502
		treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');
A
Alex Ross 已提交
503

504
		const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
M
Matt Bierner 已提交
505
		this.tree = this.instantiationService.createInstance(TunnelDataTree,
A
Alex Ross 已提交
506 507 508 509 510 511 512 513 514 515 516 517 518 519
			'RemoteTunnels',
			treeContainer,
			new TunnelTreeVirtualDelegate(),
			[renderer],
			new TunnelDataSource(),
			{
				collapseByDefault: (e: ITunnelItem | ITunnelGroup): boolean => {
					return false;
				},
				keyboardNavigationLabelProvider: {
					getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => {
						return item.label;
					}
				},
520 521 522 523 524 525 526 527 528 529 530 531
				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 已提交
532 533
					},
					getWidgetAriaLabel: () => nls.localize('tunnelView', "Tunnel View")
J
João Moreno 已提交
534
				}
A
Alex Ross 已提交
535 536 537 538 539 540
			}
		);
		const actionRunner: ActionRunner = new ActionRunner();
		renderer.actionRunner = actionRunner;

		this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner)));
541
		this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
542 543 544
		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 已提交
545 546 547 548 549 550

		this.tree.setInput(this.viewModel);
		this._register(this.viewModel.onForwardedPortsChanged(() => {
			this.tree.updateChildren(undefined, true);
		}));

551
		this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(e => {
552
			if (e.element && (e.element.tunnelType === TunnelType.Add)) {
553
				this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
A
Alex Ross 已提交
554 555
			}
		}));
556 557

		this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
558
			const isEditing = !!this.remoteExplorerService.getEditableData(e);
559 560

			if (!isEditing) {
561
				treeContainer.classList.remove('highlight');
562 563 564 565 566
			}

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

			if (isEditing) {
567
				treeContainer.classList.add('highlight');
A
Alex Ross 已提交
568 569 570 571
				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);
				}
572 573 574 575
			} else {
				this.tree.domFocus();
			}
		}));
A
Alex Ross 已提交
576 577 578
	}

	private get contributedContextMenu(): IMenu {
579
		const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService));
A
Alex Ross 已提交
580 581 582 583 584 585 586
		return contributedContextMenu;
	}

	getActions(): IAction[] {
		return this.titleActions;
	}

A
Alex Ross 已提交
587 588 589 590 591
	focus(): void {
		super.focus();
		this.tree.domFocus();
	}

592 593 594 595
	private onFocusChanged(elements: ITunnelItem[]) {
		const item = elements && elements.length ? elements[0] : undefined;
		if (item) {
			this.tunnelViewSelectionContext.set(item);
596
			this.tunnelTypeContext.set(item.tunnelType);
597
			this.tunnelCloseableContext.set(!!item.closeable);
598
			this.portChangableContextKey.set(!!item.localPort);
599
		} else {
600
			this.tunnelTypeContext.reset();
601 602
			this.tunnelViewSelectionContext.reset();
			this.tunnelCloseableContext.reset();
603
			this.portChangableContextKey.reset();
604 605 606
		}
	}

A
Alex Ross 已提交
607
	private onContextMenu(treeEvent: ITreeContextMenuEvent<ITunnelItem | ITunnelGroup>, actionRunner: ActionRunner): void {
608
		if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) {
A
Alex Ross 已提交
609 610 611 612 613 614 615 616
			return;
		}
		const node: ITunnelItem | null = treeEvent.element;
		const event: UIEvent = treeEvent.browserEvent;

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

617 618 619 620
		if (node) {
			this.tree!.setFocus([node]);
			this.tunnelTypeContext.set(node.tunnelType);
			this.tunnelCloseableContext.set(!!node.closeable);
621
			this.portChangableContextKey.set(!!node.localPort);
622 623
		} else {
			this.tunnelTypeContext.set(TunnelType.Add);
624
			this.tunnelCloseableContext.set(false);
625
			this.portChangableContextKey.set(false);
626
		}
A
Alex Ross 已提交
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650

		const actions: IAction[] = [];
		this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService));

		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
		});
	}

651 652 653 654 655 656
	private onMouseDblClick(e: ITreeMouseEvent<ITunnelGroup | ITunnelItem | null>): void {
		if (!e.element) {
			this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
		}
	}

A
Alex Ross 已提交
657
	protected layoutBody(height: number, width: number): void {
J
João Moreno 已提交
658
		super.layoutBody(height, width);
A
Alex Ross 已提交
659 660 661 662 663 664 665
		this.tree.layout(height, width);
	}
}

export class TunnelPanelDescriptor implements IViewDescriptor {
	readonly id = TunnelPanel.ID;
	readonly name = TunnelPanel.TITLE;
S
Sandeep Somavarapu 已提交
666
	readonly ctorDescriptor: SyncDescriptor<TunnelPanel>;
A
Alex Ross 已提交
667 668 669 670 671 672 673
	readonly canToggleVisibility = true;
	readonly hideByDefault = false;
	readonly workspace = true;
	readonly group = 'details@0';
	readonly remoteAuthority?: string | string[];

	constructor(viewModel: ITunnelViewModel, environmentService: IWorkbenchEnvironmentService) {
S
Sandeep Somavarapu 已提交
674
		this.ctorDescriptor = new SyncDescriptor(TunnelPanel, [viewModel]);
A
Alex Ross 已提交
675 676 677 678
		this.remoteAuthority = environmentService.configuration.remoteAuthority ? environmentService.configuration.remoteAuthority.split('+')[0] : undefined;
	}
}

J
jeanp413 已提交
679 680 681 682 683 684 685 686 687 688 689
function validationMessage(validationString: string | null): { content: string, severity: Severity } | null {
	if (!validationString) {
		return null;
	}

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

690 691 692
namespace LabelTunnelAction {
	export const ID = 'remote.tunnel.label';
	export const LABEL = nls.localize('remote.tunnel.label', "Set Label");
A
Alex Ross 已提交
693 694 695

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
696 697
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
			if (context instanceof TunnelItem) {
A
Alex Ross 已提交
698
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
699
				remoteExplorerService.setEditable(context, {
700
					onFinish: async (value, success) => {
701
						if (success) {
702
							remoteExplorerService.tunnelModel.name(context.remoteHost, context.remotePort, value);
703
						}
704
						remoteExplorerService.setEditable(context, null);
705 706
					},
					validationMessage: () => null,
707
					placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
708
					startingValue: context.name
709
				});
A
Alex Ross 已提交
710 711 712 713 714 715
			}
			return;
		};
	}
}

716 717 718
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);
719

A
Alex Ross 已提交
720
namespace ForwardPortAction {
721 722
	export const INLINE_ID = 'remote.tunnel.forwardInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.forwardCommandPalette';
723
	export const LABEL: ILocalizedString = { value: nls.localize('remote.tunnel.forward', "Forward a Port"), original: 'Forward a Port' };
724
	export const TREEITEM_LABEL = nls.localize('remote.tunnel.forwardItem', "Forward Port");
A
Alex Ross 已提交
725
	const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000).");
A
Alex Ross 已提交
726

727 728 729 730 731 732 733 734
	function parseInput(value: string): { host: string, port: number } | undefined {
		const matches = value.match(/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\:|localhost:)?([0-9]+)$/);
		if (!matches) {
			return undefined;
		}
		return { host: matches[1]?.substring(0, matches[1].length - 1) || 'localhost', port: Number(matches[2]) };
	}

A
Alex Ross 已提交
735
	function validateInput(value: string): string | null {
736 737
		const parsed = parseInput(value);
		if (!parsed) {
738
			return invalidPortString;
739 740
		} else if (parsed.port >= maxPortNumber) {
			return invalidPortNumberString;
A
Alex Ross 已提交
741 742 743 744
		}
		return null;
	}

A
Alex Ross 已提交
745 746
	function error(notificationService: INotificationService, tunnel: RemoteTunnel | void, host: string, port: number) {
		if (!tunnel) {
747
			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 已提交
748 749 750
		}
	}

751
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
752 753
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
754
			const notificationService = accessor.get(INotificationService);
A
Alex Ross 已提交
755
			if (arg instanceof TunnelItem) {
A
Alex Ross 已提交
756
				remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }).then(tunnel => error(notificationService, tunnel, arg.remoteHost, arg.remotePort));
757 758
			} else {
				remoteExplorerService.setEditable(undefined, {
759
					onFinish: async (value, success) => {
760 761
						let parsed: { host: string, port: number } | undefined;
						if (success && (parsed = parseInput(value))) {
A
Alex Ross 已提交
762
							remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
763
						}
764
						remoteExplorerService.setEditable(undefined, null);
A
Alex Ross 已提交
765
					},
J
jeanp413 已提交
766
					validationMessage: (value) => validationMessage(validateInput(value)),
A
Alex Ross 已提交
767
					placeholder: forwardPrompt
A
Alex Ross 已提交
768
				});
769 770 771 772 773 774 775
			}
		};
	}

	export function commandPaletteHandler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
776
			const notificationService = accessor.get(INotificationService);
777 778 779 780 781 782 783 784 785
			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;
			if (value && (parsed = parseInput(value))) {
A
Alex Ross 已提交
786
				remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
787 788 789 790 791
			}
		};
	}
}

A
Alex Ross 已提交
792
interface QuickPickTunnel extends IQuickPickItem {
A
Alex Ross 已提交
793
	tunnel?: ITunnelItem
A
Alex Ross 已提交
794 795
}

A
Alex Ross 已提交
796 797 798 799 800 801 802 803
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 已提交
804 805 806
	});
	if (picks.length === 0) {
		picks.push({
807
			label: nls.localize('remote.tunnel.closeNoPorts', "No ports currently forwarded. Try running the {0} command", ForwardPortAction.LABEL.value)
A
Alex Ross 已提交
808 809 810 811 812
		});
	}
	return picks;
}

A
Alex Ross 已提交
813
namespace ClosePortAction {
A
Alex Ross 已提交
814 815
	export const INLINE_ID = 'remote.tunnel.closeInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.closeCommandPalette';
816
	export const LABEL: ILocalizedString = { value: nls.localize('remote.tunnel.close', "Stop Forwarding Port"), original: 'Stop Forwarding Port' };
A
Alex Ross 已提交
817

A
Alex Ross 已提交
818
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
819
		return async (accessor, arg) => {
820
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
821
			if (context instanceof TunnelItem) {
A
Alex Ross 已提交
822
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
823
				await remoteExplorerService.close({ host: context.remoteHost, port: context.remotePort });
A
Alex Ross 已提交
824 825 826
			}
		};
	}
A
Alex Ross 已提交
827 828 829 830 831 832 833

	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 已提交
834
			const picks: QuickPickInput<QuickPickTunnel>[] = makeTunnelPicks(Array.from(remoteExplorerService.tunnelModel.forwarded.values()).filter(tunnel => tunnel.closeable));
A
Alex Ross 已提交
835 836 837 838 839 840 841 842
			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 已提交
843 844 845 846 847 848 849 850 851 852 853
}

namespace OpenPortInBrowserAction {
	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) => {
			if (arg instanceof TunnelItem) {
				const model = accessor.get(IRemoteExplorerService).tunnelModel;
				const openerService = accessor.get(IOpenerService);
854 855
				const key = MakeAddress(arg.remoteHost, arg.remotePort);
				const tunnel = model.forwarded.get(key) || model.detected.get(key);
A
Alex Ross 已提交
856
				let address: string | undefined;
857
				if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) {
A
Alex Ross 已提交
858
					return openerService.open(URI.parse('http://' + address));
A
Alex Ross 已提交
859 860 861 862 863 864 865 866
				}
				return Promise.resolve();
			}
		};
	}
}

namespace CopyAddressAction {
A
Alex Ross 已提交
867 868 869 870 871 872 873 874 875 876 877
	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 已提交
878

A
Alex Ross 已提交
879
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
880
		return async (accessor, arg) => {
881 882 883
			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 已提交
884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
			}
		};
	}

	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 已提交
901 902 903 904 905
			}
		};
	}
}

906 907 908 909 910 911 912 913 914 915 916 917
namespace RefreshTunnelViewAction {
	export const ID = 'remote.tunnel.refresh';
	export const LABEL = nls.localize('remote.tunnel.refreshView', "Refresh");

	export function handler(): ICommandHandler {
		return (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
			return remoteExplorerService.refresh();
		};
	}
}

918 919 920 921 922 923 924
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;
925 926
		} else if (Number(value) >= maxPortNumber) {
			return invalidPortNumberString;
927 928 929 930 931 932 933
		}
		return null;
	}

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
934
			const notificationService = accessor.get(INotificationService);
935 936 937 938 939 940 941
			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 });
942
							const numberValue = Number(value);
943
							const newForward = await remoteExplorerService.forward({ host: context.remoteHost, port: context.remotePort }, numberValue, context.name);
944 945 946
							if (newForward && newForward.tunnelLocalPort !== numberValue) {
								notificationService.warn(nls.localize('remote.tunnel.changeLocalPortNumber', "The local port {0} is not available. Port number {1} has been used instead", value, newForward.tunnelLocalPort));
							}
947 948
						}
					},
J
jeanp413 已提交
949
					validationMessage: (value) => validationMessage(validateInput(value)),
950 951 952 953 954 955 956
					placeholder: nls.localize('remote.tunnelsView.changePort', "New local port")
				});
			}
		};
	}
}

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

959 960
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: LabelTunnelAction.ID,
961
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
962 963 964 965 966 967 968
	when: ContextKeyExpr.and(TunnelViewFocusContextKey, TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)),
	primary: KeyCode.F2,
	mac: {
		primary: KeyCode.Enter
	},
	handler: LabelTunnelAction.handler()
});
969 970
CommandsRegistry.registerCommand(ForwardPortAction.INLINE_ID, ForwardPortAction.inlineHandler());
CommandsRegistry.registerCommand(ForwardPortAction.COMMANDPALETTE_ID, ForwardPortAction.commandPaletteHandler());
971 972
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: ClosePortAction.INLINE_ID,
973
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
974 975 976 977 978 979 980 981
	when: ContextKeyExpr.and(TunnelCloseableContextKey, TunnelViewFocusContextKey),
	primary: KeyCode.Delete,
	mac: {
		primary: KeyMod.CtrlCmd | KeyCode.Backspace
	},
	handler: ClosePortAction.inlineHandler()
});

A
Alex Ross 已提交
982
CommandsRegistry.registerCommand(ClosePortAction.COMMANDPALETTE_ID, ClosePortAction.commandPaletteHandler());
A
Alex Ross 已提交
983
CommandsRegistry.registerCommand(OpenPortInBrowserAction.ID, OpenPortInBrowserAction.handler());
984 985
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: CopyAddressAction.INLINE_ID,
986
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
987 988 989 990
	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 已提交
991
CommandsRegistry.registerCommand(CopyAddressAction.COMMANDPALETTE_ID, CopyAddressAction.commandPaletteHandler());
992
CommandsRegistry.registerCommand(RefreshTunnelViewAction.ID, RefreshTunnelViewAction.handler());
993
CommandsRegistry.registerCommand(ChangeLocalPortAction.ID, ChangeLocalPortAction.handler());
A
Alex Ross 已提交
994

A
Alex Ross 已提交
995 996 997 998 999 1000 1001
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
		id: ClosePortAction.COMMANDPALETTE_ID,
		title: ClosePortAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
1002 1003
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
1004
		id: ForwardPortAction.COMMANDPALETTE_ID,
1005 1006 1007 1008
		title: ForwardPortAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
A
Alex Ross 已提交
1009 1010 1011 1012 1013 1014 1015
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
		id: CopyAddressAction.COMMANDPALETTE_ID,
		title: CopyAddressAction.COMMANDPALETTE_LABEL
	},
	when: forwardedPortsViewEnabled
}));
A
Alex Ross 已提交
1016 1017 1018 1019
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
	group: 'navigation',
	order: 0,
	command: {
A
Alex Ross 已提交
1020
		id: ForwardPortAction.INLINE_ID,
A
Alex Ross 已提交
1021
		title: ForwardPortAction.LABEL,
1022
		icon: { id: 'codicon/plus' }
A
Alex Ross 已提交
1023 1024
	}
}));
1025 1026 1027 1028 1029 1030 1031 1032 1033
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
	group: 'navigation',
	order: 1,
	command: {
		id: RefreshTunnelViewAction.ID,
		title: RefreshTunnelViewAction.LABEL,
		icon: { id: 'codicon/refresh' }
	}
}));
A
Alex Ross 已提交
1034 1035 1036 1037
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 0,
	command: {
A
Alex Ross 已提交
1038 1039
		id: CopyAddressAction.INLINE_ID,
		title: CopyAddressAction.INLINE_LABEL,
A
Alex Ross 已提交
1040
	},
A
Alex Ross 已提交
1041
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
1042 1043 1044 1045 1046 1047 1048 1049
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
	},
A
Alex Ross 已提交
1050
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
1051 1052 1053 1054 1055
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 2,
	command: {
1056 1057
		id: LabelTunnelAction.ID,
		title: LabelTunnelAction.LABEL,
A
Alex Ross 已提交
1058 1059 1060
	},
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)
}));
1061 1062 1063 1064 1065 1066 1067
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '1_manage',
	order: 0,
	command: {
		id: ChangeLocalPortAction.ID,
		title: ChangeLocalPortAction.LABEL,
	},
1068
	when: ContextKeyExpr.and(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), PortChangableContextKey)
1069
}));
A
Alex Ross 已提交
1070 1071 1072 1073
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
1074
		id: ForwardPortAction.INLINE_ID,
1075
		title: ForwardPortAction.TREEITEM_LABEL,
A
Alex Ross 已提交
1076
	},
1077
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Candidate), TunnelTypeContextKey.isEqualTo(TunnelType.Add))
A
Alex Ross 已提交
1078 1079
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
1080 1081
	group: '1_manage',
	order: 1,
A
Alex Ross 已提交
1082
	command: {
A
Alex Ross 已提交
1083
		id: ClosePortAction.INLINE_ID,
A
Alex Ross 已提交
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
		title: ClosePortAction.LABEL,
	},
	when: TunnelCloseableContextKey
}));

MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
1094
		icon: { id: 'codicon/globe' }
A
Alex Ross 已提交
1095
	},
A
Alex Ross 已提交
1096
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
1097 1098 1099 1100
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
1101
		id: ForwardPortAction.INLINE_ID,
1102
		title: ForwardPortAction.TREEITEM_LABEL,
1103
		icon: { id: 'codicon/plus' }
A
Alex Ross 已提交
1104
	},
A
Alex Ross 已提交
1105
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate)
A
Alex Ross 已提交
1106 1107 1108 1109
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 2,
	command: {
A
Alex Ross 已提交
1110
		id: ClosePortAction.INLINE_ID,
A
Alex Ross 已提交
1111
		title: ClosePortAction.LABEL,
1112
		icon: { id: 'codicon/x' }
A
Alex Ross 已提交
1113 1114 1115
	},
	when: TunnelCloseableContextKey
}));