tunnelView.ts 43.5 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';
J
João Moreno 已提交
10
import { WorkbenchAsyncDataTree, TreeResourceNavigator } 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';
A
Alex Ross 已提交
24 25 26
import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { ActionRunner, IAction } from 'vs/base/common/actions';
27
import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions';
A
Alex Ross 已提交
28
import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } 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';
A
Alex Ross 已提交
44 45

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

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

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

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

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

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

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

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

168
	get input(): TunnelItem {
169 170 171
		return this._input;
	}

A
Alex Ross 已提交
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
	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,
194 195 196 197
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
A
Alex Ross 已提交
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
	) {
		super();
	}

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

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

	renderTemplate(container: HTMLElement): ITunnelTemplateData {
		dom.addClass(container, 'custom-view-tree-node-item');
		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: undefined // this.actionViewItemProvider
			actionViewItemProvider: (action: IAction) => {
				if (action instanceof MenuItemAction) {
					return this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action);
				}

				return undefined;
			}
		});

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

	private isTunnelItem(item: ITunnelGroup | ITunnelItem): item is ITunnelItem {
230
		return !!((<ITunnelItem>item).remotePort);
A
Alex Ross 已提交
231 232 233 234 235
	}

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

A
Alex Ross 已提交
237 238
		// reset
		templateData.actionBar.clear();
A
Alex Ross 已提交
239
		let editableData: IEditableData | undefined;
A
Alex Ross 已提交
240
		if (this.isTunnelItem(node)) {
241
			editableData = this.remoteExplorerService.getEditableData(node);
242 243
			if (editableData) {
				templateData.iconLabel.element.style.display = 'none';
A
Alex Ross 已提交
244
				this.renderInputBox(templateData.container, editableData);
245 246 247
			} else {
				templateData.iconLabel.element.style.display = 'flex';
				this.renderTunnel(node, templateData);
A
Alex Ross 已提交
248
			}
249
		} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined))) {
A
Alex Ross 已提交
250 251
			templateData.iconLabel.element.style.display = 'none';
			this.renderInputBox(templateData.container, editableData);
A
Alex Ross 已提交
252
		} else {
A
Alex Ross 已提交
253
			templateData.iconLabel.element.style.display = 'flex';
A
Alex Ross 已提交
254 255 256 257
			templateData.iconLabel.setLabel(node.label);
		}
	}

258
	private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) {
259 260
		const label = node.label + (node.description ? (' - ' + node.description) : '');
		templateData.iconLabel.setLabel(node.label, node.description, { title: label, extraClasses: ['tunnel-view-label'] });
261
		templateData.actionBar.context = node;
262
		const contextKeyService = this._register(this.contextKeyService.createScoped());
263 264 265
		contextKeyService.createKey('view', this.viewId);
		contextKeyService.createKey('tunnelType', node.tunnelType);
		contextKeyService.createKey('tunnelCloseable', node.closeable);
266 267 268
		const disposableStore = new DisposableStore();
		templateData.elementDisposable = disposableStore;
		const menu = disposableStore.add(this.menuService.createMenu(MenuId.TunnelInline, contextKeyService));
269
		const actions: IAction[] = [];
270
		disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
271 272 273 274 275 276 277 278
		if (actions) {
			templateData.actionBar.push(actions, { icon: true, label: false });
			if (this._actionRunner) {
				templateData.actionBar.actionRunner = this._actionRunner;
			}
		}
	}

A
Alex Ross 已提交
279
	private renderInputBox(container: HTMLElement, editableData: IEditableData): IDisposable {
280 281 282 283 284
		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) => {
285 286
					const message = editableData.validationMessage(value);
					if (!message || message.severity !== Severity.Error) {
287 288 289 290
						return null;
					}

					return {
291
						content: message.content,
292 293 294 295 296 297 298 299 300 301 302
						formatContent: true,
						type: MessageType.ERROR
					};
				}
			},
			placeholder: editableData.placeholder || ''
		});
		const styler = attachInputBoxStyler(inputBox, this.themeService);

		inputBox.value = value;
		inputBox.focus();
303
		inputBox.select({ start: 0, end: editableData.startingValue ? editableData.startingValue.length : 0 });
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335

		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 已提交
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
	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 已提交
360
			return element.groups();
A
Alex Ross 已提交
361 362 363 364 365 366 367 368 369 370 371 372
		} 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 已提交
373
	items?: ITunnelItem[] | Promise<ITunnelItem[]>;
A
Alex Ross 已提交
374 375 376
}

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

A
Alex Ross 已提交
381 382
	constructor(
		public tunnelType: TunnelType,
383 384
		public remoteHost: string,
		public remotePort: number,
A
Alex Ross 已提交
385
		public localAddress?: string,
386
		public localPort?: number,
A
Alex Ross 已提交
387 388 389 390 391
		public closeable?: boolean,
		public name?: string,
		private _description?: string,
	) { }
	get label(): string {
A
Alex Ross 已提交
392
		if (this.name) {
A
Alex Ross 已提交
393
			return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name);
394
		} else if (this.localAddress && (this.remoteHost !== 'localhost')) {
395
			return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}:{1} \u2192 {2}", this.remoteHost, this.remotePort, this.localAddress);
A
Alex Ross 已提交
396
		} else if (this.localAddress) {
397
			return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} \u2192 {1}", this.remotePort, this.localAddress);
398
		} else if (this.remoteHost !== 'localhost') {
399
			return nls.localize('remote.tunnelsView.forwardedPortLabel4', "{0}:{1}", this.remoteHost, this.remotePort);
A
Alex Ross 已提交
400
		} else {
401
			return nls.localize('remote.tunnelsView.forwardedPortLabel5', "{0}", this.remotePort);
A
Alex Ross 已提交
402 403 404
		}
	}

405 406 407 408
	set description(description: string | undefined) {
		this._description = description;
	}

A
Alex Ross 已提交
409
	get description(): string | undefined {
A
Alex Ross 已提交
410
		if (this._description) {
A
Alex Ross 已提交
411
			return this._description;
A
Alex Ross 已提交
412
		} else if (this.name) {
413
			return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remotePort, this.localAddress);
A
Alex Ross 已提交
414
		}
A
Alex Ross 已提交
415
		return undefined;
A
Alex Ross 已提交
416 417 418 419 420
	}
}

export const TunnelTypeContextKey = new RawContextKey<TunnelType>('tunnelType', TunnelType.Add);
export const TunnelCloseableContextKey = new RawContextKey<boolean>('tunnelCloseable', false);
421 422 423
const TunnelViewFocusContextKey = new RawContextKey<boolean>('tunnelViewFocus', false);
const TunnelViewSelectionKeyName = 'tunnelViewSelection';
const TunnelViewSelectionContextKey = new RawContextKey<ITunnelItem | undefined>(TunnelViewSelectionKeyName, undefined);
424
const PortChangableContextKey = new RawContextKey<boolean>('portChangable', false);
A
Alex Ross 已提交
425

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

S
SteVen Batten 已提交
428
export class TunnelPanel extends ViewPane {
429 430
	static readonly ID = '~remote.forwardedPorts';
	static readonly TITLE = nls.localize('remote.tunnel', "Forwarded Ports");
M
Matt Bierner 已提交
431
	private tree!: TunnelDataTree;
A
Alex Ross 已提交
432 433
	private tunnelTypeContext: IContextKey<TunnelType>;
	private tunnelCloseableContext: IContextKey<boolean>;
434 435
	private tunnelViewFocusContext: IContextKey<boolean>;
	private tunnelViewSelectionContext: IContextKey<ITunnelItem | undefined>;
436
	private portChangableContextKey: IContextKey<boolean>;
A
Alex Ross 已提交
437 438 439 440 441 442

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

	constructor(
		protected viewModel: ITunnelViewModel,
S
SteVen Batten 已提交
443
		options: IViewPaneOptions,
A
Alex Ross 已提交
444 445 446 447 448
		@IKeybindingService protected keybindingService: IKeybindingService,
		@IContextMenuService protected contextMenuService: IContextMenuService,
		@IContextKeyService protected contextKeyService: IContextKeyService,
		@IConfigurationService protected configurationService: IConfigurationService,
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
449
		@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
450
		@IOpenerService openerService: IOpenerService,
A
Alex Ross 已提交
451 452 453
		@IQuickInputService protected quickInputService: IQuickInputService,
		@ICommandService protected commandService: ICommandService,
		@IMenuService private readonly menuService: IMenuService,
454 455
		@INotificationService private readonly notificationService: INotificationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
456
		@IThemeService themeService: IThemeService,
457 458
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
		@ITelemetryService telemetryService: ITelemetryService,
A
Alex Ross 已提交
459
	) {
460
		super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
A
Alex Ross 已提交
461 462
		this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService);
		this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService);
463 464
		this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService);
		this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService);
465
		this.portChangableContextKey = PortChangableContextKey.bindTo(contextKeyService);
A
Alex Ross 已提交
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486

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

A
Alex Ross 已提交
489 490
		const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view'));
		const treeContainer = dom.append(panelContainer, dom.$('.customview-tree'));
A
Alex Ross 已提交
491 492
		dom.addClass(treeContainer, 'file-icon-themable-tree');
		dom.addClass(treeContainer, 'show-file-icons');
A
Alex Ross 已提交
493

494
		const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
M
Matt Bierner 已提交
495
		this.tree = this.instantiationService.createInstance(TunnelDataTree,
A
Alex Ross 已提交
496 497 498 499 500 501 502 503 504 505 506 507 508 509
			'RemoteTunnels',
			treeContainer,
			new TunnelTreeVirtualDelegate(),
			[renderer],
			new TunnelDataSource(),
			{
				collapseByDefault: (e: ITunnelItem | ITunnelGroup): boolean => {
					return false;
				},
				keyboardNavigationLabelProvider: {
					getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => {
						return item.label;
					}
				},
510 511 512 513 514 515 516 517 518 519 520 521 522 523
				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;
						}
					}
				}
A
Alex Ross 已提交
524 525 526 527 528 529
			}
		);
		const actionRunner: ActionRunner = new ActionRunner();
		renderer.actionRunner = actionRunner;

		this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner)));
530
		this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
531 532 533
		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 已提交
534 535 536 537 538 539

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

J
João Moreno 已提交
540
		const navigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: false, openOnSelection: false }));
A
Alex Ross 已提交
541

542 543
		this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
			if (e.element && (e.element.tunnelType === TunnelType.Add)) {
544
				this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
A
Alex Ross 已提交
545 546
			}
		}));
547 548

		this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
549
			const isEditing = !!this.remoteExplorerService.getEditableData(e);
550 551 552 553 554 555 556 557 558

			if (!isEditing) {
				dom.removeClass(treeContainer, 'highlight');
			}

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

			if (isEditing) {
				dom.addClass(treeContainer, 'highlight');
A
Alex Ross 已提交
559 560 561 562
				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);
				}
563 564 565 566
			} else {
				this.tree.domFocus();
			}
		}));
A
Alex Ross 已提交
567 568 569
	}

	private get contributedContextMenu(): IMenu {
570
		const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService));
A
Alex Ross 已提交
571 572 573 574 575 576 577
		return contributedContextMenu;
	}

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

A
Alex Ross 已提交
578 579 580 581 582
	focus(): void {
		super.focus();
		this.tree.domFocus();
	}

583 584 585 586
	private onFocusChanged(elements: ITunnelItem[]) {
		const item = elements && elements.length ? elements[0] : undefined;
		if (item) {
			this.tunnelViewSelectionContext.set(item);
587
			this.tunnelTypeContext.set(item.tunnelType);
588
			this.tunnelCloseableContext.set(!!item.closeable);
589
			this.portChangableContextKey.set(!!item.localPort);
590
		} else {
591
			this.tunnelTypeContext.reset();
592 593
			this.tunnelViewSelectionContext.reset();
			this.tunnelCloseableContext.reset();
594
			this.portChangableContextKey.reset();
595 596 597
		}
	}

A
Alex Ross 已提交
598
	private onContextMenu(treeEvent: ITreeContextMenuEvent<ITunnelItem | ITunnelGroup>, actionRunner: ActionRunner): void {
599
		if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) {
A
Alex Ross 已提交
600 601 602 603 604 605 606 607
			return;
		}
		const node: ITunnelItem | null = treeEvent.element;
		const event: UIEvent = treeEvent.browserEvent;

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

608 609 610 611
		if (node) {
			this.tree!.setFocus([node]);
			this.tunnelTypeContext.set(node.tunnelType);
			this.tunnelCloseableContext.set(!!node.closeable);
612
			this.portChangableContextKey.set(!!node.localPort);
613 614
		} else {
			this.tunnelTypeContext.set(TunnelType.Add);
615
			this.tunnelCloseableContext.set(false);
616
			this.portChangableContextKey.set(false);
617
		}
A
Alex Ross 已提交
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641

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

642 643 644 645 646 647
	private onMouseDblClick(e: ITreeMouseEvent<ITunnelGroup | ITunnelItem | null>): void {
		if (!e.element) {
			this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
		}
	}

A
Alex Ross 已提交
648
	protected layoutBody(height: number, width: number): void {
J
João Moreno 已提交
649
		super.layoutBody(height, width);
A
Alex Ross 已提交
650 651 652 653 654 655 656 657 658 659 660
		this.tree.layout(height, width);
	}

	getActionViewItem(action: IAction): IActionViewItem | undefined {
		return action instanceof MenuItemAction ? new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService) : undefined;
	}
}

export class TunnelPanelDescriptor implements IViewDescriptor {
	readonly id = TunnelPanel.ID;
	readonly name = TunnelPanel.TITLE;
S
Sandeep Somavarapu 已提交
661
	readonly ctorDescriptor: SyncDescriptor<TunnelPanel>;
A
Alex Ross 已提交
662 663 664 665 666 667 668
	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 已提交
669
		this.ctorDescriptor = new SyncDescriptor(TunnelPanel, [viewModel]);
A
Alex Ross 已提交
670 671 672 673
		this.remoteAuthority = environmentService.configuration.remoteAuthority ? environmentService.configuration.remoteAuthority.split('+')[0] : undefined;
	}
}

J
jeanp413 已提交
674 675 676 677 678 679 680 681 682 683 684
function validationMessage(validationString: string | null): { content: string, severity: Severity } | null {
	if (!validationString) {
		return null;
	}

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

685 686 687
namespace LabelTunnelAction {
	export const ID = 'remote.tunnel.label';
	export const LABEL = nls.localize('remote.tunnel.label', "Set Label");
A
Alex Ross 已提交
688 689 690

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

711 712 713
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);
714

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

722 723 724 725 726 727 728 729
	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 已提交
730
	function validateInput(value: string): string | null {
731 732
		const parsed = parseInput(value);
		if (!parsed) {
733
			return invalidPortString;
734 735
		} else if (parsed.port >= maxPortNumber) {
			return invalidPortNumberString;
A
Alex Ross 已提交
736 737 738 739
		}
		return null;
	}

A
Alex Ross 已提交
740 741
	function error(notificationService: INotificationService, tunnel: RemoteTunnel | void, host: string, port: number) {
		if (!tunnel) {
742
			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 已提交
743 744 745
		}
	}

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

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

A
Alex Ross 已提交
787
interface QuickPickTunnel extends IQuickPickItem {
A
Alex Ross 已提交
788
	tunnel?: ITunnelItem
A
Alex Ross 已提交
789 790
}

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

A
Alex Ross 已提交
808
namespace ClosePortAction {
A
Alex Ross 已提交
809 810
	export const INLINE_ID = 'remote.tunnel.closeInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.closeCommandPalette';
811
	export const LABEL: ILocalizedString = { value: nls.localize('remote.tunnel.close', "Stop Forwarding Port"), original: 'Stop Forwarding Port' };
A
Alex Ross 已提交
812

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

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

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);
849 850
				const key = MakeAddress(arg.remoteHost, arg.remotePort);
				const tunnel = model.forwarded.get(key) || model.detected.get(key);
A
Alex Ross 已提交
851
				let address: string | undefined;
852
				if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) {
A
Alex Ross 已提交
853
					return openerService.open(URI.parse('http://' + address));
A
Alex Ross 已提交
854 855 856 857 858 859 860 861
				}
				return Promise.resolve();
			}
		};
	}
}

namespace CopyAddressAction {
A
Alex Ross 已提交
862 863 864 865 866 867 868 869 870 871 872
	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 已提交
873

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

	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 已提交
896 897 898 899 900
			}
		};
	}
}

901 902 903 904 905 906 907 908 909 910 911 912
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();
		};
	}
}

913 914 915 916 917 918 919
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;
920 921
		} else if (Number(value) >= maxPortNumber) {
			return invalidPortNumberString;
922 923 924 925 926 927 928
		}
		return null;
	}

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
929
			const notificationService = accessor.get(INotificationService);
930 931 932 933 934 935 936
			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 });
937
							const numberValue = Number(value);
938
							const newForward = await remoteExplorerService.forward({ host: context.remoteHost, port: context.remotePort }, numberValue, context.name);
939 940 941
							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));
							}
942 943
						}
					},
J
jeanp413 已提交
944
					validationMessage: (value) => validationMessage(validateInput(value)),
945 946 947 948 949 950 951
					placeholder: nls.localize('remote.tunnelsView.changePort', "New local port")
				});
			}
		};
	}
}

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

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

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

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

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