tunnelView.ts 41.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';
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 27 28
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';
import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions';
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 31
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INotificationService } 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';
A
Alex Ross 已提交
43 44

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

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

export class TunnelViewModel extends Disposable implements ITunnelViewModel {
	private _onForwardedPortsChanged: Emitter<void> = new Emitter();
	public onForwardedPortsChanged: Event<void> = this._onForwardedPortsChanged.event;
	private model: TunnelModel;
69
	private _input: TunnelItem;
A
Alex Ross 已提交
70 71

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

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

	get forwarded(): TunnelItem[] {
119
		const forwarded = Array.from(this.model.forwarded.values()).map(tunnel => {
A
Alex Ross 已提交
120
			return TunnelItem.createFromTunnel(tunnel);
A
Alex Ross 已提交
121
		});
122 123 124 125
		if (this.remoteExplorerService.getEditableData(undefined)) {
			forwarded.push(this._input);
		}
		return forwarded;
A
Alex Ross 已提交
126 127
	}

A
Alex Ross 已提交
128 129
	get detected(): TunnelItem[] {
		return Array.from(this.model.detected.values()).map(tunnel => {
A
Alex Ross 已提交
130
			return TunnelItem.createFromTunnel(tunnel, TunnelType.Detected, false);
A
Alex Ross 已提交
131 132 133
		});
	}

A
Alex Ross 已提交
134 135 136 137
	get candidates(): Promise<TunnelItem[]> {
		return this.model.candidates.then(values => {
			const candidates: TunnelItem[] = [];
			values.forEach(value => {
138 139 140
				const key = MakeAddress(value.host, value.port);
				if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) {
					candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, false, undefined, value.detail));
A
Alex Ross 已提交
141 142 143 144
				}
			});
			return candidates;
		});
A
Alex Ross 已提交
145 146
	}

147
	get input(): TunnelItem {
148 149 150
		return this._input;
	}

A
Alex Ross 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
	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,
173 174 175 176
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
A
Alex Ross 已提交
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
	) {
		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 {
209
		return !!((<ITunnelItem>item).remotePort);
A
Alex Ross 已提交
210 211 212 213 214
	}

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

A
Alex Ross 已提交
216 217
		// reset
		templateData.actionBar.clear();
A
Alex Ross 已提交
218
		let editableData: IEditableData | undefined;
A
Alex Ross 已提交
219
		if (this.isTunnelItem(node)) {
220
			editableData = this.remoteExplorerService.getEditableData(node);
221 222
			if (editableData) {
				templateData.iconLabel.element.style.display = 'none';
A
Alex Ross 已提交
223
				this.renderInputBox(templateData.container, editableData);
224 225 226
			} else {
				templateData.iconLabel.element.style.display = 'flex';
				this.renderTunnel(node, templateData);
A
Alex Ross 已提交
227
			}
228
		} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined))) {
A
Alex Ross 已提交
229 230
			templateData.iconLabel.element.style.display = 'none';
			this.renderInputBox(templateData.container, editableData);
A
Alex Ross 已提交
231
		} else {
A
Alex Ross 已提交
232
			templateData.iconLabel.element.style.display = 'flex';
A
Alex Ross 已提交
233 234 235 236
			templateData.iconLabel.setLabel(node.label);
		}
	}

237
	private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) {
238 239
		const label = node.label + (node.description ? (' - ' + node.description) : '');
		templateData.iconLabel.setLabel(node.label, node.description, { title: label, extraClasses: ['tunnel-view-label'] });
240
		templateData.actionBar.context = node;
241
		const contextKeyService = this._register(this.contextKeyService.createScoped());
242 243 244
		contextKeyService.createKey('view', this.viewId);
		contextKeyService.createKey('tunnelType', node.tunnelType);
		contextKeyService.createKey('tunnelCloseable', node.closeable);
245 246 247
		const disposableStore = new DisposableStore();
		templateData.elementDisposable = disposableStore;
		const menu = disposableStore.add(this.menuService.createMenu(MenuId.TunnelInline, contextKeyService));
248
		const actions: IAction[] = [];
249
		disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
250 251 252 253 254 255 256 257
		if (actions) {
			templateData.actionBar.push(actions, { icon: true, label: false });
			if (this._actionRunner) {
				templateData.actionBar.actionRunner = this._actionRunner;
			}
		}
	}

A
Alex Ross 已提交
258
	private renderInputBox(container: HTMLElement, editableData: IEditableData): IDisposable {
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
		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) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

					return {
						content,
						formatContent: true,
						type: MessageType.ERROR
					};
				}
			},
			placeholder: editableData.placeholder || ''
		});
		const styler = attachInputBoxStyler(inputBox, this.themeService);

		inputBox.value = value;
		inputBox.focus();
282
		inputBox.select({ start: 0, end: editableData.startingValue ? editableData.startingValue.length : 0 });
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314

		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 已提交
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
	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 已提交
339
			return element.groups();
A
Alex Ross 已提交
340 341 342 343 344 345 346 347 348 349 350 351
		} 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 已提交
352
	items?: ITunnelItem[] | Promise<ITunnelItem[]>;
A
Alex Ross 已提交
353 354 355
}

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

A
Alex Ross 已提交
360 361
	constructor(
		public tunnelType: TunnelType,
362 363
		public remoteHost: string,
		public remotePort: number,
A
Alex Ross 已提交
364
		public localAddress?: string,
A
Alex Ross 已提交
365 366 367 368 369
		public closeable?: boolean,
		public name?: string,
		private _description?: string,
	) { }
	get label(): string {
A
Alex Ross 已提交
370
		if (this.name) {
A
Alex Ross 已提交
371
			return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name);
372
		} else if (this.localAddress && (this.remoteHost !== 'localhost')) {
373
			return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}:{1} \u2192 {2}", this.remoteHost, this.remotePort, this.localAddress);
A
Alex Ross 已提交
374
		} else if (this.localAddress) {
375
			return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} \u2192 {1}", this.remotePort, this.localAddress);
376
		} else if (this.remoteHost !== 'localhost') {
377
			return nls.localize('remote.tunnelsView.forwardedPortLabel4', "{0}:{1}", this.remoteHost, this.remotePort);
A
Alex Ross 已提交
378
		} else {
379
			return nls.localize('remote.tunnelsView.forwardedPortLabel5', "{0}", this.remotePort);
A
Alex Ross 已提交
380 381 382 383
		}
	}

	get description(): string | undefined {
A
Alex Ross 已提交
384
		if (this._description) {
A
Alex Ross 已提交
385
			return this._description;
A
Alex Ross 已提交
386
		} else if (this.name) {
387
			return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remotePort, this.localAddress);
A
Alex Ross 已提交
388
		}
A
Alex Ross 已提交
389
		return undefined;
A
Alex Ross 已提交
390 391 392
	}
}

393 394 395 396 397
function isHostAndPort(address: string | undefined): boolean {
	const result = address ? address.match(/^(localhost|([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)|([0-9]+:[0-9]+:[0-9]+:[0-9]+:[0-9]+:[0-9]+:[0-9]+:[0-9]+)):[0-9]+$/) : [];
	return (!!result && result.length > 0);
}

A
Alex Ross 已提交
398 399
export const TunnelTypeContextKey = new RawContextKey<TunnelType>('tunnelType', TunnelType.Add);
export const TunnelCloseableContextKey = new RawContextKey<boolean>('tunnelCloseable', false);
400 401 402
const TunnelViewFocusContextKey = new RawContextKey<boolean>('tunnelViewFocus', false);
const TunnelViewSelectionKeyName = 'tunnelViewSelection';
const TunnelViewSelectionContextKey = new RawContextKey<ITunnelItem | undefined>(TunnelViewSelectionKeyName, undefined);
403
const PortChangableContextKey = new RawContextKey<boolean>('portChangable', false);
A
Alex Ross 已提交
404

S
SteVen Batten 已提交
405
export class TunnelPanel extends ViewPane {
406 407
	static readonly ID = '~remote.forwardedPorts';
	static readonly TITLE = nls.localize('remote.tunnel', "Forwarded Ports");
A
Alex Ross 已提交
408 409 410
	private tree!: WorkbenchAsyncDataTree<any, any, any>;
	private tunnelTypeContext: IContextKey<TunnelType>;
	private tunnelCloseableContext: IContextKey<boolean>;
411 412
	private tunnelViewFocusContext: IContextKey<boolean>;
	private tunnelViewSelectionContext: IContextKey<ITunnelItem | undefined>;
413
	private portChangableContextKey: IContextKey<boolean>;
A
Alex Ross 已提交
414 415 416 417 418 419

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

	constructor(
		protected viewModel: ITunnelViewModel,
S
SteVen Batten 已提交
420
		options: IViewPaneOptions,
A
Alex Ross 已提交
421 422 423 424 425
		@IKeybindingService protected keybindingService: IKeybindingService,
		@IContextMenuService protected contextMenuService: IContextMenuService,
		@IContextKeyService protected contextKeyService: IContextKeyService,
		@IConfigurationService protected configurationService: IConfigurationService,
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
426
		@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
A
Alex Ross 已提交
427 428 429 430
		@IOpenerService protected openerService: IOpenerService,
		@IQuickInputService protected quickInputService: IQuickInputService,
		@ICommandService protected commandService: ICommandService,
		@IMenuService private readonly menuService: IMenuService,
431 432 433 434
		@INotificationService private readonly notificationService: INotificationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
A
Alex Ross 已提交
435
	) {
436
		super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService);
A
Alex Ross 已提交
437 438
		this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService);
		this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService);
439 440
		this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService);
		this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService);
441
		this.portChangableContextKey = PortChangableContextKey.bindTo(contextKeyService);
A
Alex Ross 已提交
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462

		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 {
A
Alex Ross 已提交
463 464
		const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view'));
		const treeContainer = dom.append(panelContainer, dom.$('.customview-tree'));
A
Alex Ross 已提交
465 466
		dom.addClass(treeContainer, 'file-icon-themable-tree');
		dom.addClass(treeContainer, 'show-file-icons');
A
Alex Ross 已提交
467

468
		const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
A
Alex Ross 已提交
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
		this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree,
			'RemoteTunnels',
			treeContainer,
			new TunnelTreeVirtualDelegate(),
			[renderer],
			new TunnelDataSource(),
			{
				collapseByDefault: (e: ITunnelItem | ITunnelGroup): boolean => {
					return false;
				},
				keyboardNavigationLabelProvider: {
					getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => {
						return item.label;
					}
				},
484
				multipleSelectionSupport: false
A
Alex Ross 已提交
485 486 487 488 489 490
			}
		);
		const actionRunner: ActionRunner = new ActionRunner();
		renderer.actionRunner = actionRunner;

		this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner)));
491
		this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
492 493 494
		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 已提交
495 496 497 498 499 500

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

501
		const navigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: false, openOnSelection: false }));
A
Alex Ross 已提交
502

503 504
		this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
			if (e.element && (e.element.tunnelType === TunnelType.Add)) {
505
				this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
A
Alex Ross 已提交
506 507
			}
		}));
508 509

		this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
510
			const isEditing = !!this.remoteExplorerService.getEditableData(e);
511 512 513 514 515 516 517 518 519

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

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

			if (isEditing) {
				dom.addClass(treeContainer, 'highlight');
A
Alex Ross 已提交
520 521 522 523
				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);
				}
524 525 526 527
			} else {
				this.tree.domFocus();
			}
		}));
A
Alex Ross 已提交
528 529 530
	}

	private get contributedContextMenu(): IMenu {
531
		const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService));
A
Alex Ross 已提交
532 533 534 535 536 537 538
		return contributedContextMenu;
	}

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

A
Alex Ross 已提交
539 540 541 542 543
	focus(): void {
		super.focus();
		this.tree.domFocus();
	}

544 545 546 547
	private onFocusChanged(elements: ITunnelItem[]) {
		const item = elements && elements.length ? elements[0] : undefined;
		if (item) {
			this.tunnelViewSelectionContext.set(item);
548
			this.tunnelTypeContext.set(item.tunnelType);
549
			this.tunnelCloseableContext.set(!!item.closeable);
550
			this.portChangableContextKey.set(isHostAndPort(item.localAddress));
551
		} else {
552
			this.tunnelTypeContext.reset();
553 554
			this.tunnelViewSelectionContext.reset();
			this.tunnelCloseableContext.reset();
555
			this.portChangableContextKey.reset();
556 557 558
		}
	}

A
Alex Ross 已提交
559
	private onContextMenu(treeEvent: ITreeContextMenuEvent<ITunnelItem | ITunnelGroup>, actionRunner: ActionRunner): void {
560
		if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) {
A
Alex Ross 已提交
561 562 563 564 565 566 567 568
			return;
		}
		const node: ITunnelItem | null = treeEvent.element;
		const event: UIEvent = treeEvent.browserEvent;

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

569 570 571 572
		if (node) {
			this.tree!.setFocus([node]);
			this.tunnelTypeContext.set(node.tunnelType);
			this.tunnelCloseableContext.set(!!node.closeable);
573
			this.portChangableContextKey.set(isHostAndPort(node.localAddress));
574 575
		} else {
			this.tunnelTypeContext.set(TunnelType.Add);
576
			this.tunnelCloseableContext.set(false);
577
			this.portChangableContextKey.set(false);
578
		}
A
Alex Ross 已提交
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602

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

603 604 605 606 607 608
	private onMouseDblClick(e: ITreeMouseEvent<ITunnelGroup | ITunnelItem | null>): void {
		if (!e.element) {
			this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
		}
	}

A
Alex Ross 已提交
609 610 611 612 613 614 615 616 617 618 619 620
	protected layoutBody(height: number, width: number): void {
		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 已提交
621
	readonly ctorDescriptor: SyncDescriptor<TunnelPanel>;
A
Alex Ross 已提交
622 623 624 625 626 627 628
	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 已提交
629
		this.ctorDescriptor = new SyncDescriptor(TunnelPanel, [viewModel]);
A
Alex Ross 已提交
630 631 632 633
		this.remoteAuthority = environmentService.configuration.remoteAuthority ? environmentService.configuration.remoteAuthority.split('+')[0] : undefined;
	}
}

634 635 636
namespace LabelTunnelAction {
	export const ID = 'remote.tunnel.label';
	export const LABEL = nls.localize('remote.tunnel.label', "Set Label");
A
Alex Ross 已提交
637 638 639

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
640 641
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
			if (context instanceof TunnelItem) {
A
Alex Ross 已提交
642
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
643
				remoteExplorerService.setEditable(context, {
644 645
					onFinish: (value, success) => {
						if (success) {
646
							remoteExplorerService.tunnelModel.name(context.remoteHost, context.remotePort, value);
647
						}
648
						remoteExplorerService.setEditable(context, null);
649 650
					},
					validationMessage: () => null,
651
					placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
652
					startingValue: context.name
653
				});
A
Alex Ross 已提交
654 655 656 657 658 659
			}
			return;
		};
	}
}

660 661 662
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);
663

A
Alex Ross 已提交
664
namespace ForwardPortAction {
665 666
	export const INLINE_ID = 'remote.tunnel.forwardInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.forwardCommandPalette';
667
	export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port");
668
	export const TREEITEM_LABEL = nls.localize('remote.tunnel.forwardItem', "Forward Port");
A
Alex Ross 已提交
669
	const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000).");
A
Alex Ross 已提交
670

671 672 673 674 675 676 677 678
	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 已提交
679
	function validateInput(value: string): string | null {
680 681
		const parsed = parseInput(value);
		if (!parsed) {
682
			return invalidPortString;
683 684
		} else if (parsed.port >= maxPortNumber) {
			return invalidPortNumberString;
A
Alex Ross 已提交
685 686 687 688
		}
		return null;
	}

A
Alex Ross 已提交
689 690
	function error(notificationService: INotificationService, tunnel: RemoteTunnel | void, host: string, port: number) {
		if (!tunnel) {
691
			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 已提交
692 693 694
		}
	}

695
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
696 697
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
698
			const notificationService = accessor.get(INotificationService);
A
Alex Ross 已提交
699
			if (arg instanceof TunnelItem) {
A
Alex Ross 已提交
700
				remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }).then(tunnel => error(notificationService, tunnel, arg.remoteHost, arg.remotePort));
701 702
			} else {
				remoteExplorerService.setEditable(undefined, {
A
Alex Ross 已提交
703
					onFinish: (value, success) => {
704 705
						let parsed: { host: string, port: number } | undefined;
						if (success && (parsed = parseInput(value))) {
A
Alex Ross 已提交
706
							remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
707
						}
708
						remoteExplorerService.setEditable(undefined, null);
A
Alex Ross 已提交
709
					},
A
Alex Ross 已提交
710 711
					validationMessage: validateInput,
					placeholder: forwardPrompt
A
Alex Ross 已提交
712
				});
713 714 715 716 717 718 719
			}
		};
	}

	export function commandPaletteHandler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
720
			const notificationService = accessor.get(INotificationService);
721 722 723 724 725 726 727 728 729
			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 已提交
730
				remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
731 732 733 734 735
			}
		};
	}
}

A
Alex Ross 已提交
736
interface QuickPickTunnel extends IQuickPickItem {
A
Alex Ross 已提交
737
	tunnel?: ITunnelItem
A
Alex Ross 已提交
738 739
}

A
Alex Ross 已提交
740 741 742 743 744 745 746 747
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 已提交
748 749 750 751 752 753 754 755 756
	});
	if (picks.length === 0) {
		picks.push({
			label: nls.localize('remote.tunnel.closeNoPorts', "No ports currently forwarded. Try running the {0} command", ForwardPortAction.LABEL)
		});
	}
	return picks;
}

A
Alex Ross 已提交
757
namespace ClosePortAction {
A
Alex Ross 已提交
758 759
	export const INLINE_ID = 'remote.tunnel.closeInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.closeCommandPalette';
A
Alex Ross 已提交
760 761
	export const LABEL = nls.localize('remote.tunnel.close', "Stop Forwarding Port");

A
Alex Ross 已提交
762
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
763
		return async (accessor, arg) => {
764
			const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
765
			if (context instanceof TunnelItem) {
A
Alex Ross 已提交
766
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
767
				await remoteExplorerService.close({ host: context.remoteHost, port: context.remotePort });
A
Alex Ross 已提交
768 769 770
			}
		};
	}
A
Alex Ross 已提交
771 772 773 774 775 776 777

	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 已提交
778
			const picks: QuickPickInput<QuickPickTunnel>[] = makeTunnelPicks(Array.from(remoteExplorerService.tunnelModel.forwarded.values()).filter(tunnel => tunnel.closeable));
A
Alex Ross 已提交
779 780 781 782 783 784 785 786
			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 已提交
787 788 789 790 791 792 793 794 795 796 797
}

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);
798 799
				const key = MakeAddress(arg.remoteHost, arg.remotePort);
				const tunnel = model.forwarded.get(key) || model.detected.get(key);
A
Alex Ross 已提交
800
				let address: string | undefined;
801
				if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) {
A
Alex Ross 已提交
802
					return openerService.open(URI.parse('http://' + address));
A
Alex Ross 已提交
803 804 805 806 807 808 809 810
				}
				return Promise.resolve();
			}
		};
	}
}

namespace CopyAddressAction {
A
Alex Ross 已提交
811 812 813 814 815 816 817 818 819 820 821
	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 已提交
822

A
Alex Ross 已提交
823
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
824
		return async (accessor, arg) => {
825 826 827
			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 已提交
828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844
			}
		};
	}

	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 已提交
845 846 847 848 849
			}
		};
	}
}

850 851 852 853 854 855 856 857 858 859 860 861
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();
		};
	}
}

862 863 864 865 866 867 868
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;
869 870
		} else if (Number(value) >= maxPortNumber) {
			return invalidPortNumberString;
871 872 873 874 875 876 877
		}
		return null;
	}

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
878
			const notificationService = accessor.get(INotificationService);
879 880 881 882 883 884 885
			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 });
886
							const numberValue = Number(value);
887
							const newForward = await remoteExplorerService.forward({ host: context.remoteHost, port: context.remotePort }, numberValue, context.name);
888 889 890
							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));
							}
891 892 893 894 895 896 897 898 899 900
						}
					},
					validationMessage: validateInput,
					placeholder: nls.localize('remote.tunnelsView.changePort', "New local port")
				});
			}
		};
	}
}

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

903 904
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: LabelTunnelAction.ID,
905
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
906 907 908 909 910 911 912
	when: ContextKeyExpr.and(TunnelViewFocusContextKey, TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)),
	primary: KeyCode.F2,
	mac: {
		primary: KeyCode.Enter
	},
	handler: LabelTunnelAction.handler()
});
913 914
CommandsRegistry.registerCommand(ForwardPortAction.INLINE_ID, ForwardPortAction.inlineHandler());
CommandsRegistry.registerCommand(ForwardPortAction.COMMANDPALETTE_ID, ForwardPortAction.commandPaletteHandler());
915 916
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: ClosePortAction.INLINE_ID,
917
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
918 919 920 921 922 923 924 925
	when: ContextKeyExpr.and(TunnelCloseableContextKey, TunnelViewFocusContextKey),
	primary: KeyCode.Delete,
	mac: {
		primary: KeyMod.CtrlCmd | KeyCode.Backspace
	},
	handler: ClosePortAction.inlineHandler()
});

A
Alex Ross 已提交
926
CommandsRegistry.registerCommand(ClosePortAction.COMMANDPALETTE_ID, ClosePortAction.commandPaletteHandler());
A
Alex Ross 已提交
927
CommandsRegistry.registerCommand(OpenPortInBrowserAction.ID, OpenPortInBrowserAction.handler());
928 929
KeybindingsRegistry.registerCommandAndKeybindingRule({
	id: CopyAddressAction.INLINE_ID,
930
	weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus,
931 932 933 934
	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 已提交
935
CommandsRegistry.registerCommand(CopyAddressAction.COMMANDPALETTE_ID, CopyAddressAction.commandPaletteHandler());
936
CommandsRegistry.registerCommand(RefreshTunnelViewAction.ID, RefreshTunnelViewAction.handler());
937
CommandsRegistry.registerCommand(ChangeLocalPortAction.ID, ChangeLocalPortAction.handler());
A
Alex Ross 已提交
938

A
Alex Ross 已提交
939 940 941 942 943 944 945
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
		id: ClosePortAction.COMMANDPALETTE_ID,
		title: ClosePortAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
946 947
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
948
		id: ForwardPortAction.COMMANDPALETTE_ID,
949 950 951 952
		title: ForwardPortAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
A
Alex Ross 已提交
953 954 955 956 957 958 959
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
		id: CopyAddressAction.COMMANDPALETTE_ID,
		title: CopyAddressAction.COMMANDPALETTE_LABEL
	},
	when: forwardedPortsViewEnabled
}));
A
Alex Ross 已提交
960 961 962 963
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
	group: 'navigation',
	order: 0,
	command: {
A
Alex Ross 已提交
964
		id: ForwardPortAction.INLINE_ID,
A
Alex Ross 已提交
965
		title: ForwardPortAction.LABEL,
966
		icon: { id: 'codicon/plus' }
A
Alex Ross 已提交
967 968
	}
}));
969 970 971 972 973 974 975 976 977
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
	group: 'navigation',
	order: 1,
	command: {
		id: RefreshTunnelViewAction.ID,
		title: RefreshTunnelViewAction.LABEL,
		icon: { id: 'codicon/refresh' }
	}
}));
A
Alex Ross 已提交
978 979 980 981
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 0,
	command: {
A
Alex Ross 已提交
982 983
		id: CopyAddressAction.INLINE_ID,
		title: CopyAddressAction.INLINE_LABEL,
A
Alex Ross 已提交
984
	},
A
Alex Ross 已提交
985
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
986 987 988 989 990 991 992 993
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
	},
A
Alex Ross 已提交
994
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
995 996 997 998 999
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 2,
	command: {
1000 1001
		id: LabelTunnelAction.ID,
		title: LabelTunnelAction.LABEL,
A
Alex Ross 已提交
1002 1003 1004
	},
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)
}));
1005 1006 1007 1008 1009 1010 1011 1012 1013
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '1_manage',
	order: 0,
	command: {
		id: ChangeLocalPortAction.ID,
		title: ChangeLocalPortAction.LABEL,
	},
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)
}));
A
Alex Ross 已提交
1014 1015 1016 1017
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
1018
		id: ForwardPortAction.INLINE_ID,
1019
		title: ForwardPortAction.TREEITEM_LABEL,
A
Alex Ross 已提交
1020
	},
1021
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Candidate), TunnelTypeContextKey.isEqualTo(TunnelType.Add))
A
Alex Ross 已提交
1022 1023
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
1024 1025
	group: '1_manage',
	order: 1,
A
Alex Ross 已提交
1026
	command: {
A
Alex Ross 已提交
1027
		id: ClosePortAction.INLINE_ID,
A
Alex Ross 已提交
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
		title: ClosePortAction.LABEL,
	},
	when: TunnelCloseableContextKey
}));

MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
1038
		icon: { id: 'codicon/globe' }
A
Alex Ross 已提交
1039
	},
A
Alex Ross 已提交
1040
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
1041 1042 1043 1044
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
1045
		id: ForwardPortAction.INLINE_ID,
1046
		title: ForwardPortAction.TREEITEM_LABEL,
1047
		icon: { id: 'codicon/plus' }
A
Alex Ross 已提交
1048
	},
A
Alex Ross 已提交
1049
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate)
A
Alex Ross 已提交
1050 1051 1052 1053
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 2,
	command: {
A
Alex Ross 已提交
1054
		id: ClosePortAction.INLINE_ID,
A
Alex Ross 已提交
1055
		title: ClosePortAction.LABEL,
1056
		icon: { id: 'codicon/x' }
A
Alex Ross 已提交
1057 1058 1059
	},
	when: TunnelCloseableContextKey
}));