tabsTitleControl.ts 23.2 KB
Newer Older
B
wip  
Benjamin Pasero 已提交
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.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import 'vs/css!./media/tabstitle';
B
Benjamin Pasero 已提交
9
import nls = require('vs/nls');
B
Benjamin Pasero 已提交
10
import { TPromise } from 'vs/base/common/winjs.base';
B
Benjamin Pasero 已提交
11 12
import errors = require('vs/base/common/errors');
import DOM = require('vs/base/browser/dom');
J
Johannes Rieken 已提交
13 14
import { isMacintosh } from 'vs/base/common/platform';
import { MIME_BINARY } from 'vs/base/common/mime';
B
Benjamin Pasero 已提交
15
import { ActionRunner, IAction } from 'vs/base/common/actions';
J
Johannes Rieken 已提交
16
import { Position, IEditorInput } from 'vs/platform/editor/common/editor';
B
Benjamin Pasero 已提交
17
import { IEditorGroup, asFileEditorInput, getResource } from 'vs/workbench/common/editor';
J
Johannes Rieken 已提交
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { EditorLabel } from 'vs/workbench/browser/labels';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IMessageService } from 'vs/platform/message/common/message';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMenuService } from 'vs/platform/actions/common/actions';
J
Joao Moreno 已提交
33
import { IWindowService } from 'vs/platform/windows/common/windows';
J
Johannes Rieken 已提交
34
import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl';
J
Johannes Rieken 已提交
35
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
36
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
37 38 39 40
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { extractResources } from 'vs/base/browser/dnd';
import { LinkedMap } from 'vs/base/common/map';
B
Benjamin Pasero 已提交
41 42 43 44 45 46 47 48 49
import paths = require('vs/base/common/paths');

interface IEditorInputLabel {
	editor: IEditorInput;
	name: string;
	hasAmbiguousName?: boolean;
	description?: string;
	verboseDescription?: string;
}
B
wip  
Benjamin Pasero 已提交
50 51

export class TabsTitleControl extends TitleControl {
B
Benjamin Pasero 已提交
52 53 54
	private titleContainer: HTMLElement;
	private tabsContainer: HTMLElement;
	private activeTab: HTMLElement;
55
	private editorLabels: EditorLabel[];
56
	private scrollbar: ScrollableElement;
57
	private tabDisposeables: IDisposable[];
B
wip  
Benjamin Pasero 已提交
58 59 60 61

	constructor(
		@IContextMenuService contextMenuService: IContextMenuService,
		@IInstantiationService instantiationService: IInstantiationService,
62
		@IConfigurationService configurationService: IConfigurationService,
B
wip  
Benjamin Pasero 已提交
63 64
		@IWorkbenchEditorService editorService: IWorkbenchEditorService,
		@IEditorGroupService editorGroupService: IEditorGroupService,
65
		@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
66
		@IContextKeyService contextKeyService: IContextKeyService,
67
		@IKeybindingService keybindingService: IKeybindingService,
B
wip  
Benjamin Pasero 已提交
68
		@ITelemetryService telemetryService: ITelemetryService,
69
		@IMessageService messageService: IMessageService,
70
		@IMenuService menuService: IMenuService,
J
Joao Moreno 已提交
71 72
		@IQuickOpenService quickOpenService: IQuickOpenService,
		@IWindowService private windowService: IWindowService
B
wip  
Benjamin Pasero 已提交
73
	) {
74
		super(contextMenuService, instantiationService, configurationService, editorService, editorGroupService, contextKeyService, keybindingService, telemetryService, messageService, menuService, quickOpenService);
B
wip  
Benjamin Pasero 已提交
75

B
Benjamin Pasero 已提交
76
		this.tabDisposeables = [];
77
		this.editorLabels = [];
B
wip  
Benjamin Pasero 已提交
78 79 80 81 82
	}

	public setContext(group: IEditorGroup): void {
		super.setContext(group);

83
		this.editorActionsToolbar.context = { group };
B
wip  
Benjamin Pasero 已提交
84 85
	}

86
	public create(parent: HTMLElement): void {
87
		super.create(parent);
88

89
		this.titleContainer = parent;
B
wip  
Benjamin Pasero 已提交
90

91
		// Tabs Container
B
Benjamin Pasero 已提交
92
		this.tabsContainer = document.createElement('div');
93
		this.tabsContainer.setAttribute('role', 'tablist');
B
Benjamin Pasero 已提交
94
		DOM.addClass(this.tabsContainer, 'tabs-container');
95

96
		// Forward scrolling inside the container to our custom scrollbar
B
Benjamin Pasero 已提交
97 98 99 100 101 102 103 104
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.SCROLL, e => {
			if (DOM.hasClass(this.tabsContainer, 'scroll')) {
				this.scrollbar.updateState({
					scrollLeft: this.tabsContainer.scrollLeft // during DND the  container gets scrolled so we need to update the custom scrollbar
				});
			}
		}));

105 106 107 108 109 110
		// New file when double clicking on tabs container (but not tabs)
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DBLCLICK, e => {
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
				DOM.EventHelper.stop(e);

111
				const group = this.context;
B
Benjamin Pasero 已提交
112 113 114
				if (group) {
					this.editorService.openEditor(this.untitledEditorService.createOrGet(), { pinned: true, index: group.count /* always at the end */ }).done(null, errors.onUnexpectedError); // untitled are always pinned
				}
115 116 117
			}
		}));

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
		// Custom Scrollbar
		this.scrollbar = new ScrollableElement(this.tabsContainer, {
			horizontal: ScrollbarVisibility.Auto,
			vertical: ScrollbarVisibility.Hidden,
			scrollYToX: true,
			useShadows: false,
			canUseTranslate3d: true,
			horizontalScrollbarSize: 3
		});

		this.scrollbar.onScroll(e => {
			this.tabsContainer.scrollLeft = e.scrollLeft;
		});

		this.titleContainer.appendChild(this.scrollbar.getDomNode());
B
Benjamin Pasero 已提交
133

B
Benjamin Pasero 已提交
134 135
		// Drag over
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_OVER, (e: DragEvent) => {
B
Benjamin Pasero 已提交
136 137
			DOM.addClass(this.tabsContainer, 'scroll'); // enable support to scroll while dragging

B
Benjamin Pasero 已提交
138 139 140 141 142 143 144 145 146
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
				DOM.addClass(this.tabsContainer, 'dropfeedback');
			}
		}));

		// Drag leave
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
			DOM.removeClass(this.tabsContainer, 'dropfeedback');
B
Benjamin Pasero 已提交
147
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
148 149 150 151 152
		}));

		// Drag end
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_END, (e: DragEvent) => {
			DOM.removeClass(this.tabsContainer, 'dropfeedback');
B
Benjamin Pasero 已提交
153
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
154 155
		}));

B
Benjamin Pasero 已提交
156 157
		// Drop onto tabs container
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DROP, (e: DragEvent) => {
B
Benjamin Pasero 已提交
158
			DOM.removeClass(this.tabsContainer, 'dropfeedback');
B
Benjamin Pasero 已提交
159
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
160

B
Benjamin Pasero 已提交
161
			const target = e.target;
B
Benjamin Pasero 已提交
162
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
B
Benjamin Pasero 已提交
163 164
				const group = this.context;
				if (group) {
165 166 167
					const targetPosition = this.stacks.positionOfGroup(group);
					const targetIndex = group.count;

B
Benjamin Pasero 已提交
168
					this.onDrop(e, group, targetPosition, targetIndex);
B
Benjamin Pasero 已提交
169 170 171 172
				}
			}
		}));

173
		// Editor Actions Container
174 175 176
		const editorActionsContainer = document.createElement('div');
		DOM.addClass(editorActionsContainer, 'editor-actions');
		this.titleContainer.appendChild(editorActionsContainer);
177 178 179

		// Editor Actions Toolbar
		this.createEditorActionsToolBar(editorActionsContainer);
B
wip  
Benjamin Pasero 已提交
180 181
	}

B
Benjamin Pasero 已提交
182 183 184 185
	public allowDragging(element: HTMLElement): boolean {
		return (element.className === 'tabs-container');
	}

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
	protected doUpdate(): void {
		if (!this.context) {
			return;
		}

		const group = this.context;

		// Tabs container activity state
		const isActive = this.stacks.isActive(group);
		if (isActive) {
			DOM.addClass(this.titleContainer, 'active');
		} else {
			DOM.removeClass(this.titleContainer, 'active');
		}

201 202
		// Compute labels and protect against duplicates
		const editorsOfGroup = this.context.getEditors();
B
Benjamin Pasero 已提交
203
		const labels = this.getUniqueTabLabels(editorsOfGroup);
204

205
		// Tab label and styles
206
		editorsOfGroup.forEach((editor, index) => {
207 208
			const tabContainer = this.tabsContainer.children[index];
			if (tabContainer instanceof HTMLElement) {
209
				const isPinned = group.isPinned(index);
210 211 212
				const isActive = group.isActive(editor);
				const isDirty = editor.isDirty();

213 214
				const label = labels[index];
				const name = label.name;
B
Benjamin Pasero 已提交
215
				const description = label.hasAmbiguousName && label.description ? label.description : '';
216
				const verboseDescription = label.verboseDescription || '';
217

218
				// Container
B
Benjamin Pasero 已提交
219
				tabContainer.setAttribute('aria-label', `${name}, tab`);
220
				tabContainer.title = verboseDescription;
221

222 223 224
				// Label
				const tabLabel = this.editorLabels[index];
				tabLabel.setLabel({ name, description, resource: getResource(editor) }, { extraClasses: ['tab-label'], italic: !isPinned });
225 226 227 228

				// Active state
				if (isActive) {
					DOM.addClass(tabContainer, 'active');
229
					tabContainer.setAttribute('aria-selected', 'true');
230 231 232
					this.activeTab = tabContainer;
				} else {
					DOM.removeClass(tabContainer, 'active');
233
					tabContainer.setAttribute('aria-selected', 'false');
234 235 236 237 238 239 240 241 242 243
				}

				// Dirty State
				if (isDirty) {
					DOM.addClass(tabContainer, 'dirty');
				} else {
					DOM.removeClass(tabContainer, 'dirty');
				}
			}
		});
244

245
		// Update Editor Actions Toolbar
246
		this.updateEditorActionsToolbar();
247

B
Benjamin Pasero 已提交
248
		// Ensure the active tab is always revealed
249
		this.layout();
250 251
	}

B
Benjamin Pasero 已提交
252 253 254 255
	private getUniqueTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
		const labels: IEditorInputLabel[] = [];

		const mapLabelToDuplicates = new LinkedMap<string, IEditorInputLabel[]>();
256
		const mapLabelAndDescriptionToDuplicates = new LinkedMap<string, IEditorInputLabel[]>();
B
Benjamin Pasero 已提交
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274

		// Build labels and descriptions for each editor
		editors.forEach(editor => {
			let description = editor.getDescription();
			if (description && description.indexOf(paths.nativeSep) >= 0) {
				description = paths.basename(description); // optimize for editors that show paths and build a shorter description to keep tab width small
			}

			const item: IEditorInputLabel = {
				editor,
				name: editor.getName(),
				description,
				verboseDescription: editor.getDescription(true)
			};
			labels.push(item);

			mapLabelToDuplicates.getOrSet(item.name, []).push(item);
			if (item.description) {
275
				mapLabelAndDescriptionToDuplicates.getOrSet(item.name + item.description, []).push(item);
B
Benjamin Pasero 已提交
276 277 278 279 280 281 282 283 284 285 286 287 288
			}
		});

		// Mark label duplicates
		const labelDuplicates = mapLabelToDuplicates.values();
		labelDuplicates.forEach(duplicates => {
			if (duplicates.length > 1) {
				duplicates.forEach(duplicate => {
					duplicate.hasAmbiguousName = true;
				});
			}
		});

289 290
		// React to duplicates for combination of label and description
		const descriptionDuplicates = mapLabelAndDescriptionToDuplicates.values();
B
Benjamin Pasero 已提交
291 292 293 294 295 296 297 298 299 300 301
		descriptionDuplicates.forEach(duplicates => {
			if (duplicates.length > 1) {
				duplicates.forEach(duplicate => {
					duplicate.description = duplicate.editor.getDescription(); // fallback to full description if the short description still has duplicates
				});
			}
		});

		return labels;
	}

302
	protected doRefresh(): void {
B
wip  
Benjamin Pasero 已提交
303
		const group = this.context;
304
		const editor = group && group.activeEditor;
B
wip  
Benjamin Pasero 已提交
305
		if (!editor) {
306 307
			this.clearTabs();

308
			this.clearEditorActionsToolbar();
B
wip  
Benjamin Pasero 已提交
309 310 311 312

			return; // return early if we are being closed
		}

B
Benjamin Pasero 已提交
313 314 315
		// Handle Tabs
		this.handleTabs(group.count);
		DOM.addClass(this.titleContainer, 'shows-tabs');
316

317
		// Update Tabs
318
		this.doUpdate();
B
wip  
Benjamin Pasero 已提交
319 320
	}

321
	private clearTabs(): void {
B
Benjamin Pasero 已提交
322
		DOM.clearNode(this.tabsContainer);
323 324

		this.tabDisposeables = dispose(this.tabDisposeables);
325
		this.editorLabels = [];
B
Benjamin Pasero 已提交
326

327
		DOM.removeClass(this.titleContainer, 'shows-tabs');
328 329
	}

B
Benjamin Pasero 已提交
330 331 332
	private handleTabs(tabsNeeded: number): void {
		const tabs = this.tabsContainer.children;
		const tabsCount = tabs.length;
B
Benjamin Pasero 已提交
333

B
Benjamin Pasero 已提交
334 335 336 337
		// Nothing to do if count did not change
		if (tabsCount === tabsNeeded) {
			return;
		}
B
Benjamin Pasero 已提交
338

B
Benjamin Pasero 已提交
339 340 341 342 343 344
		// We need more tabs: create new ones
		if (tabsCount < tabsNeeded) {
			for (let i = tabsCount; i < tabsNeeded; i++) {
				this.tabsContainer.appendChild(this.createTab(i));
			}
		}
345

B
Benjamin Pasero 已提交
346 347 348 349
		// We need less tabs: delete the ones we do not need
		else {
			for (let i = 0; i < tabsCount - tabsNeeded; i++) {
				(this.tabsContainer.lastChild as HTMLElement).remove();
350
				this.editorLabels.pop();
B
Benjamin Pasero 已提交
351
				this.tabDisposeables.pop().dispose();
352
			}
B
Benjamin Pasero 已提交
353 354
		}
	}
355

B
Benjamin Pasero 已提交
356
	private createTab(index: number): HTMLElement {
357

B
Benjamin Pasero 已提交
358 359 360 361 362 363
		// Tab Container
		const tabContainer = document.createElement('div');
		tabContainer.draggable = true;
		tabContainer.tabIndex = 0;
		tabContainer.setAttribute('role', 'presentation'); // cannot use role "tab" here due to https://github.com/Microsoft/vscode/issues/8659
		DOM.addClass(tabContainer, 'tab monaco-editor-background');
B
Benjamin Pasero 已提交
364

B
Benjamin Pasero 已提交
365 366 367 368 369
		if (!this.showTabCloseButton) {
			DOM.addClass(tabContainer, 'no-close-button');
		} else {
			DOM.removeClass(tabContainer, 'no-close-button');
		}
B
Benjamin Pasero 已提交
370

B
Benjamin Pasero 已提交
371 372 373
		// Tab Editor Label
		const editorLabel = this.instantiationService.createInstance(EditorLabel, tabContainer, void 0);
		this.editorLabels.push(editorLabel);
B
Benjamin Pasero 已提交
374

B
Benjamin Pasero 已提交
375 376 377 378
		// Tab Close
		const tabCloseContainer = document.createElement('div');
		DOM.addClass(tabCloseContainer, 'tab-close');
		tabContainer.appendChild(tabCloseContainer);
B
Benjamin Pasero 已提交
379

B
Benjamin Pasero 已提交
380 381
		const bar = new ActionBar(tabCloseContainer, { ariaLabel: nls.localize('araLabelTabActions', "Tab actions"), actionRunner: new TabActionRunner(() => this.context, index) });
		bar.push(this.closeEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeEditorAction) });
382

B
Benjamin Pasero 已提交
383
		// Eventing
384 385 386
		const disposable = this.hookTabListeners(tabContainer, index);

		this.tabDisposeables.push(combinedDisposable([disposable, bar, editorLabel]));
B
Benjamin Pasero 已提交
387 388

		return tabContainer;
389 390 391 392 393 394 395
	}

	public layout(): void {
		if (!this.activeTab) {
			return;
		}

396 397 398 399 400 401 402 403 404
		const visibleContainerWidth = this.tabsContainer.offsetWidth;
		const totalContainerWidth = this.tabsContainer.scrollWidth;

		// Update scrollbar
		this.scrollbar.updateState({
			width: visibleContainerWidth,
			scrollWidth: totalContainerWidth
		});

B
Benjamin Pasero 已提交
405
		// Always reveal the active one
B
Benjamin Pasero 已提交
406 407 408
		const containerScrollPosX = this.tabsContainer.scrollLeft;
		const activeTabPosX = this.activeTab.offsetLeft;
		const activeTabWidth = this.activeTab.offsetWidth;
409
		const activeTabFits = activeTabWidth <= visibleContainerWidth;
B
Benjamin Pasero 已提交
410 411

		// Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right
412 413
		// Note: only try to do this if we actually have enough width to give to show the tab fully!
		if (activeTabFits && containerScrollPosX + visibleContainerWidth < activeTabPosX + activeTabWidth) {
414 415 416
			this.scrollbar.updateState({
				scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */)
			});
B
Benjamin Pasero 已提交
417 418
		}

419 420
		// Tab is overlflowng to the left or does not fit: Scroll it into view to the left
		else if (containerScrollPosX > activeTabPosX || !activeTabFits) {
421 422 423
			this.scrollbar.updateState({
				scrollLeft: this.activeTab.offsetLeft
			});
B
Benjamin Pasero 已提交
424
		}
B
Benjamin Pasero 已提交
425 426
	}

427 428
	private hookTabListeners(tab: HTMLElement, index: number): IDisposable {
		const disposables = [];
B
Benjamin Pasero 已提交
429 430

		// Open on Click
431
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
432 433
			tab.blur();

B
Benjamin Pasero 已提交
434
			const { editor, position } = this.toTabContext(index);
B
Benjamin Pasero 已提交
435
			if (e.button === 0 /* Left Button */ && !DOM.findParentWithClass((e.target || e.srcElement) as HTMLElement, 'monaco-action-bar', 'tab')) {
436 437 438 439 440
				setTimeout(() => this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError)); // timeout to keep focus in editor after mouse up
			}
		}));

		// Close on mouse middle click
441
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_UP, (e: MouseEvent) => {
442
			DOM.EventHelper.stop(e);
443
			tab.blur();
444 445

			if (e.button === 1 /* Middle Button */) {
B
Benjamin Pasero 已提交
446 447
				const { editor, position } = this.toTabContext(index);

448
				this.editorService.closeEditor(position, editor).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
449
			}
B
Benjamin Pasero 已提交
450
		}));
B
Benjamin Pasero 已提交
451

452
		// Context menu on Shift+F10
453
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
454 455 456 457
			const event = new StandardKeyboardEvent(e);
			if (event.shiftKey && event.keyCode === KeyCode.F10) {
				DOM.EventHelper.stop(e);

B
Benjamin Pasero 已提交
458 459 460
				const { group, editor } = this.toTabContext(index);

				this.onContextMenu({ group, editor }, e, tab);
461 462 463
			}
		}));

B
Benjamin Pasero 已提交
464
		// Keyboard accessibility
465
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
B
Benjamin Pasero 已提交
466
			const event = new StandardKeyboardEvent(e);
B
Benjamin Pasero 已提交
467
			let handled = false;
B
Benjamin Pasero 已提交
468

B
Benjamin Pasero 已提交
469 470
			const { group, position, editor } = this.toTabContext(index);

B
Benjamin Pasero 已提交
471
			// Run action on Enter/Space
A
Alexandru Dima 已提交
472
			if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
B
Benjamin Pasero 已提交
473
				handled = true;
B
Benjamin Pasero 已提交
474
				this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
475 476
			}

B
Benjamin Pasero 已提交
477
			// Navigate in editors
A
Alexandru Dima 已提交
478
			else if ([KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.Home, KeyCode.End].some(kb => event.equals(kb))) {
B
Benjamin Pasero 已提交
479
				let targetIndex: number;
A
Alexandru Dima 已提交
480
				if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.UpArrow)) {
B
Benjamin Pasero 已提交
481
					targetIndex = index - 1;
A
Alexandru Dima 已提交
482
				} else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.DownArrow)) {
B
Benjamin Pasero 已提交
483
					targetIndex = index + 1;
A
Alexandru Dima 已提交
484
				} else if (event.equals(KeyCode.Home)) {
B
Benjamin Pasero 已提交
485 486 487 488 489
					targetIndex = 0;
				} else {
					targetIndex = group.count - 1;
				}

B
Benjamin Pasero 已提交
490 491 492
				const target = group.getEditor(targetIndex);
				if (target) {
					handled = true;
493
					this.editorService.openEditor(target, { preserveFocus: true }, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
494 495 496
					(<HTMLElement>this.tabsContainer.childNodes[targetIndex]).focus();
				}
			}
B
Benjamin Pasero 已提交
497

B
Benjamin Pasero 已提交
498
			if (handled) {
499
				DOM.EventHelper.stop(e, true);
B
Benjamin Pasero 已提交
500
			}
501 502 503 504 505

			// moving in the tabs container can have an impact on scrolling position, so we need to update the custom scrollbar
			this.scrollbar.updateState({
				scrollLeft: this.tabsContainer.scrollLeft
			});
B
Benjamin Pasero 已提交
506 507
		}));

B
Benjamin Pasero 已提交
508
		// Pin on double click
509
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DBLCLICK, (e: MouseEvent) => {
B
Benjamin Pasero 已提交
510 511
			DOM.EventHelper.stop(e);

B
Benjamin Pasero 已提交
512 513
			const { group, editor } = this.toTabContext(index);

514
			this.editorGroupService.pinEditor(group, editor);
B
Benjamin Pasero 已提交
515
		}));
B
Benjamin Pasero 已提交
516

B
Benjamin Pasero 已提交
517
		// Context menu
518
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.CONTEXT_MENU, (e: Event) => {
B
Benjamin Pasero 已提交
519 520 521 522
			const { group, editor } = this.toTabContext(index);

			this.onContextMenu({ group, editor }, e, tab);
		}));
B
Benjamin Pasero 已提交
523 524

		// Drag start
525
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_START, (e: DragEvent) => {
B
Benjamin Pasero 已提交
526 527
			const { group, editor } = this.toTabContext(index);

528
			this.onEditorDragStart({ editor, group });
529
			e.dataTransfer.effectAllowed = 'copyMove';
B
Benjamin Pasero 已提交
530

B
Benjamin Pasero 已提交
531
			// Insert transfer accordingly
B
Benjamin Pasero 已提交
532 533
			const fileInput = asFileEditorInput(editor, true);
			if (fileInput) {
B
Benjamin Pasero 已提交
534 535 536
				const resource = fileInput.getResource().toString();
				e.dataTransfer.setData('URL', resource); // enables cross window DND of tabs
				e.dataTransfer.setData('DownloadURL', [MIME_BINARY, editor.getName(), resource].join(':')); // enables support to drag a tab as file to desktop
B
Benjamin Pasero 已提交
537
			}
B
Benjamin Pasero 已提交
538 539
		}));

540 541 542 543 544 545
		// We need to keep track of DRAG_ENTER and DRAG_LEAVE events because a tab is not just a div without children,
		// it contains a label and a close button. HTML gives us DRAG_ENTER and DRAG_LEAVE events when hovering over
		// these children and this can cause flicker of the drop feedback. The workaround is to count the events and only
		// remove the drop feedback when the counter is 0 (see https://github.com/Microsoft/vscode/issues/14470)
		let counter = 0;

B
Benjamin Pasero 已提交
546
		// Drag over
547
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
548
			counter++;
B
Benjamin Pasero 已提交
549 550 551 552
			DOM.addClass(tab, 'dropfeedback');
		}));

		// Drag leave
553
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
554 555 556 557
			counter--;
			if (counter === 0) {
				DOM.removeClass(tab, 'dropfeedback');
			}
B
Benjamin Pasero 已提交
558 559 560
		}));

		// Drag end
561
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_END, (e: DragEvent) => {
562
			counter = 0;
B
Benjamin Pasero 已提交
563
			DOM.removeClass(tab, 'dropfeedback');
B
Benjamin Pasero 已提交
564

565
			this.onEditorDragEnd();
B
Benjamin Pasero 已提交
566 567 568
		}));

		// Drop
569
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DROP, (e: DragEvent) => {
570
			counter = 0;
B
Benjamin Pasero 已提交
571 572
			DOM.removeClass(tab, 'dropfeedback');

B
Benjamin Pasero 已提交
573
			const { group, position } = this.toTabContext(index);
574

B
Benjamin Pasero 已提交
575
			this.onDrop(e, group, position, index);
B
Benjamin Pasero 已提交
576
		}));
577 578

		return combinedDisposable(disposables);
B
Benjamin Pasero 已提交
579
	}
B
Benjamin Pasero 已提交
580

B
Benjamin Pasero 已提交
581 582 583 584 585 586 587 588
	private toTabContext(index: number): { group: IEditorGroup, position: Position, editor: IEditorInput } {
		const group = this.context;
		const position = this.stacks.positionOfGroup(group);
		const editor = group.getEditor(index);

		return { group, position, editor };
	}

B
Benjamin Pasero 已提交
589
	private onDrop(e: DragEvent, group: IEditorGroup, targetPosition: Position, targetIndex: number): void {
590 591
		DOM.removeClass(this.tabsContainer, 'dropfeedback');
		DOM.removeClass(this.tabsContainer, 'scroll');
592

B
Benjamin Pasero 已提交
593 594 595 596
		// Local DND
		const draggedEditor = TabsTitleControl.getDraggedEditor();
		if (draggedEditor) {
			DOM.EventHelper.stop(e, true);
597

B
Benjamin Pasero 已提交
598 599 600
			// Move editor to target position and index
			if (this.isMoveOperation(e, draggedEditor.group, group)) {
				this.editorGroupService.moveEditor(draggedEditor.editor, draggedEditor.group, group, targetIndex);
B
Benjamin Pasero 已提交
601
			}
602

B
Benjamin Pasero 已提交
603
			// Copy: just open editor at target index
604
			else {
B
Benjamin Pasero 已提交
605
				this.editorService.openEditor(draggedEditor.editor, { pinned: true, index: targetIndex }, targetPosition).done(null, errors.onUnexpectedError);
606
			}
B
Benjamin Pasero 已提交
607 608 609 610 611 612 613 614

			this.onEditorDragEnd();
		}

		// External DND
		else {
			this.handleExternalDrop(e, targetPosition, targetIndex);
		}
B
Benjamin Pasero 已提交
615
	}
616

617
	private handleExternalDrop(e: DragEvent, targetPosition: Position, targetIndex: number): void {
618
		const resources = extractResources(e).filter(d => d.resource.scheme === 'file' || d.resource.scheme === 'untitled');
619

620
		// Handle resources
621
		if (resources.length) {
B
Benjamin Pasero 已提交
622
			DOM.EventHelper.stop(e, true);
623

624 625 626 627 628 629 630 631 632 633 634 635 636
			// Add external ones to recently open list
			const externalResources = resources.filter(d => d.isExternal).map(d => d.resource);
			if (externalResources.length) {
				this.windowService.addToRecentlyOpen(externalResources.map(resource => {
					return {
						path: resource.fsPath,
						isFile: true
					};
				}));
			}

			// Open in Editor
			this.editorService.openEditors(resources.map(d => {
637
				return {
638
					input: { resource: d.resource, options: { pinned: true, index: targetIndex } },
639 640
					position: targetPosition
				};
J
Joao Moreno 已提交
641
			})).then(() => {
642
				this.editorGroupService.focusGroup(targetPosition);
J
Joao Moreno 已提交
643 644
				return this.windowService.focusWindow();
			}).done(null, errors.onUnexpectedError);
645 646 647
		}
	}

648 649 650 651 652
	private isMoveOperation(e: DragEvent, source: IEditorGroup, target: IEditorGroup) {
		const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh);

		return !isCopy || source.id === target.id;
	}
J
Johannes Rieken 已提交
653
}
B
Benjamin Pasero 已提交
654 655 656 657 658 659 660 661 662 663 664 665 666

class TabActionRunner extends ActionRunner {

	constructor(private group: () => IEditorGroup, private index: number) {
		super();
	}

	public run(action: IAction, context?: any): TPromise<any> {
		const group = this.group();

		return super.run(action, { group, editor: group.getEditor(this.index) });
	}
}