tunnelView.ts 31.6 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 } from 'vs/workbench/common/views';
A
Alex Ross 已提交
10 11
import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService';
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 17 18 19 20
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';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
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';
29
import { IRemoteExplorerService, TunnelModel, MakeAddress, TunnelType, ITunnelItem } 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 35 36 37
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';
import { KeyCode } from 'vs/base/common/keyCodes';
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';
A
Alex Ross 已提交
42 43

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

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

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

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

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

	get forwarded(): TunnelItem[] {
118
		const forwarded = Array.from(this.model.forwarded.values()).map(tunnel => {
119
			return new TunnelItem(TunnelType.Forwarded, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description);
A
Alex Ross 已提交
120
		});
121 122 123 124
		if (this.remoteExplorerService.getEditableData(undefined)) {
			forwarded.push(this._input);
		}
		return forwarded;
A
Alex Ross 已提交
125 126
	}

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

A
Alex Ross 已提交
133 134 135 136
	get candidates(): Promise<TunnelItem[]> {
		return this.model.candidates.then(values => {
			const candidates: TunnelItem[] = [];
			values.forEach(value => {
137 138 139
				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 已提交
140 141 142 143
				}
			});
			return candidates;
		});
A
Alex Ross 已提交
144 145
	}

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

A
Alex Ross 已提交
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
	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,
172 173 174 175
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
A
Alex Ross 已提交
176 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
	) {
		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 {
208
		return !!((<ITunnelItem>item).remotePort);
A
Alex Ross 已提交
209 210 211 212 213
	}

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

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

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

A
Alex Ross 已提交
257
	private renderInputBox(container: HTMLElement, editableData: IEditableData): IDisposable {
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
		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();
281
		inputBox.select({ start: 0, end: editableData.startingValue ? editableData.startingValue.length : 0 });
282 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

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

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

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

export const TunnelTypeContextKey = new RawContextKey<TunnelType>('tunnelType', TunnelType.Add);
export const TunnelCloseableContextKey = new RawContextKey<boolean>('tunnelCloseable', false);

S
SteVen Batten 已提交
391
export class TunnelPanel extends ViewPane {
392 393
	static readonly ID = '~remote.forwardedPorts';
	static readonly TITLE = nls.localize('remote.tunnel', "Forwarded Ports");
A
Alex Ross 已提交
394 395 396 397 398 399 400 401 402
	private tree!: WorkbenchAsyncDataTree<any, any, any>;
	private tunnelTypeContext: IContextKey<TunnelType>;
	private tunnelCloseableContext: IContextKey<boolean>;

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

	constructor(
		protected viewModel: ITunnelViewModel,
S
SteVen Batten 已提交
403
		options: IViewPaneOptions,
A
Alex Ross 已提交
404 405 406 407 408 409 410 411 412
		@IKeybindingService protected keybindingService: IKeybindingService,
		@IContextMenuService protected contextMenuService: IContextMenuService,
		@IContextKeyService protected contextKeyService: IContextKeyService,
		@IConfigurationService protected configurationService: IConfigurationService,
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
		@IOpenerService protected openerService: IOpenerService,
		@IQuickInputService protected quickInputService: IQuickInputService,
		@ICommandService protected commandService: ICommandService,
		@IMenuService private readonly menuService: IMenuService,
413 414 415 416
		@INotificationService private readonly notificationService: INotificationService,
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
A
Alex Ross 已提交
417
	) {
418
		super(options, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService);
A
Alex Ross 已提交
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
		this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService);
		this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService);

		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 已提交
442 443
		const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view'));
		const treeContainer = dom.append(panelContainer, dom.$('.customview-tree'));
A
Alex Ross 已提交
444 445
		dom.addClass(treeContainer, 'file-icon-themable-tree');
		dom.addClass(treeContainer, 'show-file-icons');
A
Alex Ross 已提交
446

447
		const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
A
Alex Ross 已提交
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
		this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree,
			'RemoteTunnels',
			treeContainer,
			new TunnelTreeVirtualDelegate(),
			[renderer],
			new TunnelDataSource(),
			{
				keyboardSupport: true,
				collapseByDefault: (e: ITunnelItem | ITunnelGroup): boolean => {
					return false;
				},
				keyboardNavigationLabelProvider: {
					getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => {
						return item.label;
					}
				},
464
				multipleSelectionSupport: false
A
Alex Ross 已提交
465 466 467 468 469 470
			}
		);
		const actionRunner: ActionRunner = new ActionRunner();
		renderer.actionRunner = actionRunner;

		this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner)));
471
		this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
A
Alex Ross 已提交
472 473 474 475 476 477

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

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

480 481
		this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
			if (e.element && (e.element.tunnelType === TunnelType.Add)) {
482
				this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
A
Alex Ross 已提交
483 484
			}
		}));
485 486

		this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
487
			const isEditing = !!this.remoteExplorerService.getEditableData(e);
488 489 490 491 492 493 494 495 496

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

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

			if (isEditing) {
				dom.addClass(treeContainer, 'highlight');
497
				this.tree.reveal(e ? e : this.viewModel.input);
498 499 500 501
			} else {
				this.tree.domFocus();
			}
		}));
A
Alex Ross 已提交
502 503 504
	}

	private get contributedContextMenu(): IMenu {
505
		const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService));
A
Alex Ross 已提交
506 507 508 509 510 511 512
		return contributedContextMenu;
	}

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

A
Alex Ross 已提交
513 514 515 516 517
	focus(): void {
		super.focus();
		this.tree.domFocus();
	}

A
Alex Ross 已提交
518
	private onContextMenu(treeEvent: ITreeContextMenuEvent<ITunnelItem | ITunnelGroup>, actionRunner: ActionRunner): void {
519
		if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) {
A
Alex Ross 已提交
520 521 522 523 524 525 526 527
			return;
		}
		const node: ITunnelItem | null = treeEvent.element;
		const event: UIEvent = treeEvent.browserEvent;

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

528 529 530 531 532 533
		if (node) {
			this.tree!.setFocus([node]);
			this.tunnelTypeContext.set(node.tunnelType);
			this.tunnelCloseableContext.set(!!node.closeable);
		} else {
			this.tunnelTypeContext.set(TunnelType.Add);
534
			this.tunnelCloseableContext.set(false);
535
		}
A
Alex Ross 已提交
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559

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

560 561 562 563 564 565
	private onMouseDblClick(e: ITreeMouseEvent<ITunnelGroup | ITunnelItem | null>): void {
		if (!e.element) {
			this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
		}
	}

A
Alex Ross 已提交
566 567 568 569 570 571 572 573 574 575 576 577
	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 已提交
578
	readonly ctorDescriptor: SyncDescriptor<TunnelPanel>;
A
Alex Ross 已提交
579 580 581 582 583 584 585
	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 已提交
586
		this.ctorDescriptor = new SyncDescriptor(TunnelPanel, [viewModel]);
A
Alex Ross 已提交
587 588 589 590
		this.remoteAuthority = environmentService.configuration.remoteAuthority ? environmentService.configuration.remoteAuthority.split('+')[0] : undefined;
	}
}

591 592 593
namespace LabelTunnelAction {
	export const ID = 'remote.tunnel.label';
	export const LABEL = nls.localize('remote.tunnel.label', "Set Label");
A
Alex Ross 已提交
594 595 596 597 598

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			if (arg instanceof TunnelItem) {
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
599
				remoteExplorerService.setEditable(arg, {
600 601
					onFinish: (value, success) => {
						if (success) {
602
							remoteExplorerService.tunnelModel.name(arg.remoteHost, arg.remotePort, value);
603
						}
604
						remoteExplorerService.setEditable(arg, null);
605 606
					},
					validationMessage: () => null,
607
					placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
608 609
					startingValue: arg.name
				});
A
Alex Ross 已提交
610 611 612 613 614 615 616
			}
			return;
		};
	}
}

namespace ForwardPortAction {
617 618
	export const INLINE_ID = 'remote.tunnel.forwardInline';
	export const COMMANDPALETTE_ID = 'remote.tunnel.forwardCommandPalette';
619
	export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port");
A
Alex Ross 已提交
620
	const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000).");
A
Alex Ross 已提交
621

622 623 624 625 626 627 628 629
	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 已提交
630 631 632 633 634 635 636
	function validateInput(value: string): string | null {
		if (!parseInput(value)) {
			return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid");
		}
		return null;
	}

A
Alex Ross 已提交
637 638 639 640 641 642
	function error(notificationService: INotificationService, tunnel: RemoteTunnel | void, host: string, port: number) {
		if (!tunnel) {
			notificationService.error(nls.localize('remote.tunnel.forwardError', "Unable to forward {0}:{1}. The host may not be available.", host, port));
		}
	}

643
	export function inlineHandler(): ICommandHandler {
A
Alex Ross 已提交
644 645
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
646
			const notificationService = accessor.get(INotificationService);
A
Alex Ross 已提交
647
			if (arg instanceof TunnelItem) {
A
Alex Ross 已提交
648
				remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }).then(tunnel => error(notificationService, tunnel, arg.remoteHost, arg.remotePort));
649 650
			} else {
				remoteExplorerService.setEditable(undefined, {
A
Alex Ross 已提交
651
					onFinish: (value, success) => {
652 653
						let parsed: { host: string, port: number } | undefined;
						if (success && (parsed = parseInput(value))) {
A
Alex Ross 已提交
654
							remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
655
						}
656
						remoteExplorerService.setEditable(undefined, null);
A
Alex Ross 已提交
657
					},
A
Alex Ross 已提交
658 659
					validationMessage: validateInput,
					placeholder: forwardPrompt
A
Alex Ross 已提交
660
				});
661 662 663 664 665 666 667
			}
		};
	}

	export function commandPaletteHandler(): ICommandHandler {
		return async (accessor, arg) => {
			const remoteExplorerService = accessor.get(IRemoteExplorerService);
A
Alex Ross 已提交
668
			const notificationService = accessor.get(INotificationService);
669 670 671 672 673 674 675 676 677
			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 已提交
678
				remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
A
Alex Ross 已提交
679 680 681 682 683 684 685 686 687 688 689 690 691
			}
		};
	}
}

namespace ClosePortAction {
	export const ID = 'remote.tunnel.close';
	export const LABEL = nls.localize('remote.tunnel.close', "Stop Forwarding Port");

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			if (arg instanceof TunnelItem) {
				const remoteExplorerService = accessor.get(IRemoteExplorerService);
692
				await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort });
A
Alex Ross 已提交
693 694 695 696 697 698 699 700 701 702 703 704 705 706
			}
		};
	}
}

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);
707 708
				const key = MakeAddress(arg.remoteHost, arg.remotePort);
				const tunnel = model.forwarded.get(key) || model.detected.get(key);
A
Alex Ross 已提交
709
				let address: string | undefined;
710
				if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) {
A
Alex Ross 已提交
711
					return openerService.open(URI.parse('http://' + address));
A
Alex Ross 已提交
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
				}
				return Promise.resolve();
			}
		};
	}
}

namespace CopyAddressAction {
	export const ID = 'remote.tunnel.copyAddress';
	export const LABEL = nls.localize('remote.tunnel.copyAddress', "Copy Address");

	export function handler(): ICommandHandler {
		return async (accessor, arg) => {
			if (arg instanceof TunnelItem) {
				const model = accessor.get(IRemoteExplorerService).tunnelModel;
				const clipboard = accessor.get(IClipboardService);
728
				const address = model.address(arg.remoteHost, arg.remotePort);
A
Alex Ross 已提交
729 730 731 732 733 734 735 736
				if (address) {
					await clipboard.writeText(address.toString());
				}
			}
		};
	}
}

737 738 739 740 741 742 743 744 745 746 747 748
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();
		};
	}
}

749
CommandsRegistry.registerCommand(LabelTunnelAction.ID, LabelTunnelAction.handler());
750 751
CommandsRegistry.registerCommand(ForwardPortAction.INLINE_ID, ForwardPortAction.inlineHandler());
CommandsRegistry.registerCommand(ForwardPortAction.COMMANDPALETTE_ID, ForwardPortAction.commandPaletteHandler());
A
Alex Ross 已提交
752 753 754
CommandsRegistry.registerCommand(ClosePortAction.ID, ClosePortAction.handler());
CommandsRegistry.registerCommand(OpenPortInBrowserAction.ID, OpenPortInBrowserAction.handler());
CommandsRegistry.registerCommand(CopyAddressAction.ID, CopyAddressAction.handler());
755
CommandsRegistry.registerCommand(RefreshTunnelViewAction.ID, RefreshTunnelViewAction.handler());
A
Alex Ross 已提交
756

757 758
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
	command: {
759
		id: ForwardPortAction.COMMANDPALETTE_ID,
760 761 762 763
		title: ForwardPortAction.LABEL
	},
	when: forwardedPortsViewEnabled
}));
A
Alex Ross 已提交
764 765 766 767
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
	group: 'navigation',
	order: 0,
	command: {
A
Alex Ross 已提交
768
		id: ForwardPortAction.INLINE_ID,
A
Alex Ross 已提交
769
		title: ForwardPortAction.LABEL,
770
		icon: { id: 'codicon/plus' }
A
Alex Ross 已提交
771 772
	}
}));
773 774 775 776 777 778 779 780 781
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
	group: 'navigation',
	order: 1,
	command: {
		id: RefreshTunnelViewAction.ID,
		title: RefreshTunnelViewAction.LABEL,
		icon: { id: 'codicon/refresh' }
	}
}));
A
Alex Ross 已提交
782 783 784 785 786 787 788
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 0,
	command: {
		id: CopyAddressAction.ID,
		title: CopyAddressAction.LABEL,
	},
A
Alex Ross 已提交
789
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
790 791 792 793 794 795 796 797
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
	},
A
Alex Ross 已提交
798
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
799 800 801 802 803
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 2,
	command: {
804 805
		id: LabelTunnelAction.ID,
		title: LabelTunnelAction.LABEL,
A
Alex Ross 已提交
806 807 808 809 810 811 812
	},
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded)
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 1,
	command: {
813
		id: ForwardPortAction.INLINE_ID,
A
Alex Ross 已提交
814 815
		title: ForwardPortAction.LABEL,
	},
816
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Candidate), TunnelTypeContextKey.isEqualTo(TunnelType.Add))
A
Alex Ross 已提交
817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
	group: '0_manage',
	order: 3,
	command: {
		id: ClosePortAction.ID,
		title: ClosePortAction.LABEL,
	},
	when: TunnelCloseableContextKey
}));

MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
		id: OpenPortInBrowserAction.ID,
		title: OpenPortInBrowserAction.LABEL,
833
		icon: { id: 'codicon/globe' }
A
Alex Ross 已提交
834
	},
A
Alex Ross 已提交
835
	when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))
A
Alex Ross 已提交
836 837 838 839
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 0,
	command: {
840
		id: ForwardPortAction.INLINE_ID,
A
Alex Ross 已提交
841
		title: ForwardPortAction.LABEL,
842
		icon: { id: 'codicon/plus' }
A
Alex Ross 已提交
843
	},
A
Alex Ross 已提交
844
	when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate)
A
Alex Ross 已提交
845 846 847 848 849 850
}));
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
	order: 2,
	command: {
		id: ClosePortAction.ID,
		title: ClosePortAction.LABEL,
851
		icon: { id: 'codicon/x' }
A
Alex Ross 已提交
852 853 854
	},
	when: TunnelCloseableContextKey
}));