tabsTitleControl.ts 28.3 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 { shorten } from 'vs/base/common/labels';
B
Benjamin Pasero 已提交
16
import { ActionRunner, IAction } from 'vs/base/common/actions';
17
import { Position, IEditorInput, Verbosity, IUntitledResourceInput } from 'vs/platform/editor/common/editor';
18
import { IEditorGroup, toResource } from 'vs/workbench/common/editor';
J
Johannes Rieken 已提交
19 20 21 22 23 24 25 26 27 28 29 30 31
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 { 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 { 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 已提交
32
import { IWindowService } from 'vs/platform/windows/common/windows';
J
Johannes Rieken 已提交
33
import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl';
J
Johannes Rieken 已提交
34
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
35
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
36 37 38 39
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';
40 41
import { DelegatingWorkbenchEditorService } from 'vs/workbench/services/editor/browser/editorService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
B
Benjamin Pasero 已提交
42
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
B
Benjamin Pasero 已提交
43 44
import { INACTIVE_TAB_BACKGROUND, ACTIVE_TAB_BACKGROUND, ACTIVE_TAB_ACTIVE_GROUP_FOREGROUND, ACTIVE_TAB_INACTIVE_GROUP_FOREGROUND, INACTIVE_TAB_ACTIVE_GROUP_FOREGROUND, INACTIVE_TAB_INACTIVE_GROUP_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { highContrastOutline } from 'vs/platform/theme/common/colorRegistry';
B
Benjamin Pasero 已提交
45 46 47 48 49 50

interface IEditorInputLabel {
	editor: IEditorInput;
	name: string;
	hasAmbiguousName?: boolean;
	description?: string;
51
	title?: string;
B
Benjamin Pasero 已提交
52
}
B
wip  
Benjamin Pasero 已提交
53 54

export class TabsTitleControl extends TitleControl {
B
Benjamin Pasero 已提交
55 56 57
	private titleContainer: HTMLElement;
	private tabsContainer: HTMLElement;
	private activeTab: HTMLElement;
58
	private editorLabels: EditorLabel[];
59
	private scrollbar: ScrollableElement;
60
	private tabDisposeables: IDisposable[];
61
	private blockRevealActiveTab: boolean;
B
wip  
Benjamin Pasero 已提交
62 63 64 65 66 67

	constructor(
		@IContextMenuService contextMenuService: IContextMenuService,
		@IInstantiationService instantiationService: IInstantiationService,
		@IWorkbenchEditorService editorService: IWorkbenchEditorService,
		@IEditorGroupService editorGroupService: IEditorGroupService,
68
		@IContextKeyService contextKeyService: IContextKeyService,
69
		@IKeybindingService keybindingService: IKeybindingService,
B
wip  
Benjamin Pasero 已提交
70
		@ITelemetryService telemetryService: ITelemetryService,
71
		@IMessageService messageService: IMessageService,
72
		@IMenuService menuService: IMenuService,
J
Joao Moreno 已提交
73
		@IQuickOpenService quickOpenService: IQuickOpenService,
74 75
		@IWindowService private windowService: IWindowService,
		@IThemeService themeService: IThemeService
B
wip  
Benjamin Pasero 已提交
76
	) {
77
		super(contextMenuService, instantiationService, editorService, editorGroupService, contextKeyService, keybindingService, telemetryService, messageService, menuService, quickOpenService, themeService);
B
wip  
Benjamin Pasero 已提交
78

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

83 84 85 86 87 88 89
	protected initActions(services: IInstantiationService): void {
		super.initActions(this.createScopedInstantiationService());
	}

	private createScopedInstantiationService(): IInstantiationService {
		const stacks = this.editorGroupService.getStacksModel();
		const delegatingEditorService = this.instantiationService.createInstance(DelegatingWorkbenchEditorService);
90 91 92 93 94

		// We create a scoped instantiation service to override the behaviour when closing an inactive editor
		// Specifically we want to move focus back to the editor when an inactive editor is closed from anywhere
		// in the tabs title control (e.g. mouse middle click, context menu on tab). This is only needed for
		// the inactive editors because closing the active one will always cause a tab switch that sets focus.
95 96
		// We also want to block the tabs container to reveal the currently active tab because that makes it very
		// hard to close multiple inactive tabs next to each other.
97 98 99
		delegatingEditorService.setEditorCloseHandler((position, editor) => {
			const group = stacks.groupAt(position);
			if (group && stacks.isActive(group) && !group.isActive(editor)) {
100
				this.editorGroupService.focusGroup(group);
101 102
			}

103 104
			this.blockRevealActiveTab = true;

105 106 107
			return TPromise.as(void 0);
		});

108
		return this.instantiationService.createChild(new ServiceCollection([IWorkbenchEditorService, delegatingEditorService]));
109 110
	}

B
wip  
Benjamin Pasero 已提交
111 112 113
	public setContext(group: IEditorGroup): void {
		super.setContext(group);

114
		this.editorActionsToolbar.context = { group };
B
wip  
Benjamin Pasero 已提交
115 116
	}

117
	public create(parent: HTMLElement): void {
118
		super.create(parent);
119

120
		this.titleContainer = parent;
B
wip  
Benjamin Pasero 已提交
121

122
		// Tabs Container
B
Benjamin Pasero 已提交
123
		this.tabsContainer = document.createElement('div');
124
		this.tabsContainer.setAttribute('role', 'tablist');
B
Benjamin Pasero 已提交
125
		DOM.addClass(this.tabsContainer, 'tabs-container');
126

127
		// Forward scrolling inside the container to our custom scrollbar
128
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.SCROLL, e => {
B
Benjamin Pasero 已提交
129 130 131 132 133 134 135
			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
				});
			}
		}));

136
		// New file when double clicking on tabs container (but not tabs)
137
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DBLCLICK, e => {
138 139 140 141
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
				DOM.EventHelper.stop(e);

142
				const group = this.context;
B
Benjamin Pasero 已提交
143
				if (group) {
144
					this.editorService.openEditor({ options: { pinned: true, index: group.count /* always at the end */ } } as IUntitledResourceInput).done(null, errors.onUnexpectedError); // untitled are always pinned
B
Benjamin Pasero 已提交
145
				}
146 147 148
			}
		}));

149 150 151 152 153 154
		// Custom Scrollbar
		this.scrollbar = new ScrollableElement(this.tabsContainer, {
			horizontal: ScrollbarVisibility.Auto,
			vertical: ScrollbarVisibility.Hidden,
			scrollYToX: true,
			useShadows: false,
155
			canUseTranslate3d: false,
156 157 158 159 160 161 162 163
			horizontalScrollbarSize: 3
		});

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

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

B
Benjamin Pasero 已提交
165
		// Drag over
166
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_OVER, (e: DragEvent) => {
B
Benjamin Pasero 已提交
167 168
			DOM.addClass(this.tabsContainer, 'scroll'); // enable support to scroll while dragging

B
Benjamin Pasero 已提交
169 170
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
B
Benjamin Pasero 已提交
171
				this.updateDropFeedback(this.tabsContainer, true);
B
Benjamin Pasero 已提交
172 173 174 175
			}
		}));

		// Drag leave
176
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
B
Benjamin Pasero 已提交
177
			this.updateDropFeedback(this.tabsContainer, false);
B
Benjamin Pasero 已提交
178
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
179 180 181
		}));

		// Drag end
182
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_END, (e: DragEvent) => {
B
Benjamin Pasero 已提交
183
			this.updateDropFeedback(this.tabsContainer, false);
B
Benjamin Pasero 已提交
184
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
185 186
		}));

B
Benjamin Pasero 已提交
187
		// Drop onto tabs container
188
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DROP, (e: DragEvent) => {
B
Benjamin Pasero 已提交
189
			this.updateDropFeedback(this.tabsContainer, false);
B
Benjamin Pasero 已提交
190
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
191

B
Benjamin Pasero 已提交
192
			const target = e.target;
B
Benjamin Pasero 已提交
193
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
B
Benjamin Pasero 已提交
194 195
				const group = this.context;
				if (group) {
196 197 198
					const targetPosition = this.stacks.positionOfGroup(group);
					const targetIndex = group.count;

B
Benjamin Pasero 已提交
199
					this.onDrop(e, group, targetPosition, targetIndex);
B
Benjamin Pasero 已提交
200 201 202 203
				}
			}
		}));

204
		// Editor Actions Container
205 206 207
		const editorActionsContainer = document.createElement('div');
		DOM.addClass(editorActionsContainer, 'editor-actions');
		this.titleContainer.appendChild(editorActionsContainer);
208 209 210

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

B
Benjamin Pasero 已提交
213 214 215 216 217 218 219 220 221
	private updateDropFeedback(element: HTMLElement, isDND: boolean, index?: number): void {
		const isTab = (typeof index === 'number');
		const isActiveTab = isTab && this.context.isActive(this.context.getEditor(index));

		// Background
		const noDNDBackgroundColor = isTab ? this.getColor(isActiveTab ? ACTIVE_TAB_BACKGROUND : INACTIVE_TAB_BACKGROUND) : null;
		element.style.backgroundColor = isDND ? this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) : noDNDBackgroundColor;

		// Outline
222 223
		const hcOutline = this.getColor(highContrastOutline);
		if (hcOutline && isDND) {
B
Benjamin Pasero 已提交
224 225
			element.style.outlineWidth = '2px';
			element.style.outlineStyle = 'dashed';
226
			element.style.outlineColor = hcOutline;
B
Benjamin Pasero 已提交
227
			element.style.outlineOffset = isTab ? '-5px' : '-3px';
B
Benjamin Pasero 已提交
228 229 230
		} else {
			element.style.outlineWidth = null;
			element.style.outlineStyle = null;
231
			element.style.outlineColor = hcOutline;
B
Benjamin Pasero 已提交
232
			element.style.outlineOffset = null;
B
Benjamin Pasero 已提交
233 234 235
		}
	}

B
Benjamin Pasero 已提交
236 237 238 239
	public allowDragging(element: HTMLElement): boolean {
		return (element.className === 'tabs-container');
	}

240 241 242 243 244 245 246 247
	protected doUpdate(): void {
		if (!this.context) {
			return;
		}

		const group = this.context;

		// Tabs container activity state
248 249
		const isGroupActive = this.stacks.isActive(group);
		if (isGroupActive) {
250 251 252 253 254
			DOM.addClass(this.titleContainer, 'active');
		} else {
			DOM.removeClass(this.titleContainer, 'active');
		}

255 256
		// Compute labels and protect against duplicates
		const editorsOfGroup = this.context.getEditors();
B
Benjamin Pasero 已提交
257
		const labels = this.getUniqueTabLabels(editorsOfGroup);
258

259
		// Tab label and styles
260
		editorsOfGroup.forEach((editor, index) => {
261 262
			const tabContainer = this.tabsContainer.children[index];
			if (tabContainer instanceof HTMLElement) {
263
				const isPinned = group.isPinned(index);
264
				const isTabActive = group.isActive(editor);
265 266
				const isDirty = editor.isDirty();

267 268
				const label = labels[index];
				const name = label.name;
B
Benjamin Pasero 已提交
269
				const description = label.hasAmbiguousName && label.description ? label.description : '';
270
				const title = label.title || '';
271

272
				// Container
B
Benjamin Pasero 已提交
273
				tabContainer.setAttribute('aria-label', `${name}, tab`);
274
				tabContainer.title = title;
275 276
				tabContainer.style.borderLeftColor = (index !== 0) ? this.getColor(TAB_BORDER) : null;
				tabContainer.style.borderRightColor = (index === editorsOfGroup.length - 1) ? this.getColor(TAB_BORDER) : null;
277
				tabContainer.style.outlineColor = this.getColor(highContrastOutline);
B
Benjamin Pasero 已提交
278

279
				const tabOptions = this.editorGroupService.getTabOptions();
280
				['off', 'left'].forEach(option => {
281
					const domAction = tabOptions.tabCloseButton === option ? DOM.addClass : DOM.removeClass;
282 283
					domAction(tabContainer, `close-button-${option}`);
				});
284

285 286
				// Label
				const tabLabel = this.editorLabels[index];
287
				tabLabel.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { extraClasses: ['tab-label'], italic: !isPinned });
288 289

				// Active state
290
				if (isTabActive) {
291
					DOM.addClass(tabContainer, 'active');
292
					tabContainer.setAttribute('aria-selected', 'true');
293
					tabContainer.style.backgroundColor = this.getColor(ACTIVE_TAB_BACKGROUND);
294 295
					tabLabel.element.style.color = this.getColor(isGroupActive ? ACTIVE_TAB_ACTIVE_GROUP_FOREGROUND : ACTIVE_TAB_INACTIVE_GROUP_FOREGROUND);

296 297 298
					this.activeTab = tabContainer;
				} else {
					DOM.removeClass(tabContainer, 'active');
299
					tabContainer.setAttribute('aria-selected', 'false');
300
					tabContainer.style.backgroundColor = this.getColor(INACTIVE_TAB_BACKGROUND);
301
					tabLabel.element.style.color = this.getColor(isGroupActive ? INACTIVE_TAB_ACTIVE_GROUP_FOREGROUND : INACTIVE_TAB_INACTIVE_GROUP_FOREGROUND);
302 303 304 305 306 307 308 309 310 311
				}

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

313
		// Update Editor Actions Toolbar
314
		this.updateEditorActionsToolbar();
315

B
Benjamin Pasero 已提交
316
		// Ensure the active tab is always revealed
317
		this.layout();
318 319
	}

B
Benjamin Pasero 已提交
320 321 322 323
	private getUniqueTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
		const labels: IEditorInputLabel[] = [];

		const mapLabelToDuplicates = new LinkedMap<string, IEditorInputLabel[]>();
B
Benjamin Pasero 已提交
324
		const mapLabelAndDescriptionToDuplicates = new LinkedMap<string, IEditorInputLabel[]>();
B
Benjamin Pasero 已提交
325 326 327 328 329 330 331 332

		// Build labels and descriptions for each editor
		editors.forEach(editor => {
			let description = editor.getDescription();
			const item: IEditorInputLabel = {
				editor,
				name: editor.getName(),
				description,
333
				title: editor.getTitle(Verbosity.LONG)
B
Benjamin Pasero 已提交
334 335 336 337
			};
			labels.push(item);

			mapLabelToDuplicates.getOrSet(item.name, []).push(item);
B
Benjamin Pasero 已提交
338 339 340 341

			if (typeof description === 'string') {
				mapLabelAndDescriptionToDuplicates.getOrSet(`${item.name}${item.description}`, []).push(item);
			}
B
Benjamin Pasero 已提交
342 343
		});

H
hun1ahpu 已提交
344
		// Mark duplicates and shorten their descriptions
B
Benjamin Pasero 已提交
345 346
		const labelDuplicates = mapLabelToDuplicates.values();
		labelDuplicates.forEach(duplicates => {
347
			if (duplicates.length > 1) {
B
Benjamin Pasero 已提交
348 349 350 351
				duplicates = duplicates.filter(d => {
					// we could have items with equal label and description. in that case it does not make much
					// sense to produce a shortened version of the label, so we ignore those kind of items
					return typeof d.description === 'string' && mapLabelAndDescriptionToDuplicates.get(`${d.name}${d.description}`).length === 1;
B
Benjamin Pasero 已提交
352
				});
B
Benjamin Pasero 已提交
353 354 355 356 357 358 359 360

				if (duplicates.length > 1) {
					const shortenedDescriptions = shorten(duplicates.map(duplicate => duplicate.editor.getDescription()));
					duplicates.forEach((duplicate, i) => {
						duplicate.description = shortenedDescriptions[i];
						duplicate.hasAmbiguousName = true;
					});
				}
B
Benjamin Pasero 已提交
361 362 363 364 365 366
			}
		});

		return labels;
	}

367
	protected doRefresh(): void {
B
wip  
Benjamin Pasero 已提交
368
		const group = this.context;
369
		const editor = group && group.activeEditor;
B
wip  
Benjamin Pasero 已提交
370
		if (!editor) {
371 372
			this.clearTabs();

373
			this.clearEditorActionsToolbar();
B
wip  
Benjamin Pasero 已提交
374 375 376 377

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

B
Benjamin Pasero 已提交
378 379
		// Handle Tabs
		this.handleTabs(group.count);
380
		DOM.removeClass(this.titleContainer, 'empty');
381

382
		// Update Tabs
383
		this.doUpdate();
B
wip  
Benjamin Pasero 已提交
384 385
	}

386
	private clearTabs(): void {
B
Benjamin Pasero 已提交
387
		DOM.clearNode(this.tabsContainer);
388 389

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

392
		DOM.addClass(this.titleContainer, 'empty');
393 394
	}

B
Benjamin Pasero 已提交
395 396 397
	private handleTabs(tabsNeeded: number): void {
		const tabs = this.tabsContainer.children;
		const tabsCount = tabs.length;
B
Benjamin Pasero 已提交
398

B
Benjamin Pasero 已提交
399 400 401 402
		// Nothing to do if count did not change
		if (tabsCount === tabsNeeded) {
			return;
		}
B
Benjamin Pasero 已提交
403

B
Benjamin Pasero 已提交
404 405 406 407 408 409
		// We need more tabs: create new ones
		if (tabsCount < tabsNeeded) {
			for (let i = tabsCount; i < tabsNeeded; i++) {
				this.tabsContainer.appendChild(this.createTab(i));
			}
		}
410

B
Benjamin Pasero 已提交
411 412 413 414
		// 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();
415
				this.editorLabels.pop();
B
Benjamin Pasero 已提交
416
				this.tabDisposeables.pop().dispose();
417
			}
B
Benjamin Pasero 已提交
418 419
		}
	}
420

B
Benjamin Pasero 已提交
421
	private createTab(index: number): HTMLElement {
422

B
Benjamin Pasero 已提交
423 424 425 426 427
		// 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
428
		DOM.addClass(tabContainer, 'tab');
B
Benjamin Pasero 已提交
429

B
Benjamin Pasero 已提交
430 431 432
		// Tab Editor Label
		const editorLabel = this.instantiationService.createInstance(EditorLabel, tabContainer, void 0);
		this.editorLabels.push(editorLabel);
B
Benjamin Pasero 已提交
433

B
Benjamin Pasero 已提交
434 435 436 437
		// Tab Close
		const tabCloseContainer = document.createElement('div');
		DOM.addClass(tabCloseContainer, 'tab-close');
		tabContainer.appendChild(tabCloseContainer);
B
Benjamin Pasero 已提交
438

B
Benjamin Pasero 已提交
439 440
		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) });
441

B
Benjamin Pasero 已提交
442
		// Eventing
443 444 445
		const disposable = this.hookTabListeners(tabContainer, index);

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

		return tabContainer;
448 449 450 451 452 453 454
	}

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

455 456 457 458 459 460 461 462 463
		const visibleContainerWidth = this.tabsContainer.offsetWidth;
		const totalContainerWidth = this.tabsContainer.scrollWidth;

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

464 465 466 467 468 469 470
		// Return now if we are blocked to reveal the active tab and clear flag
		if (this.blockRevealActiveTab) {
			this.blockRevealActiveTab = false;
			return;
		}

		// Reveal the active one
B
Benjamin Pasero 已提交
471 472 473
		const containerScrollPosX = this.tabsContainer.scrollLeft;
		const activeTabPosX = this.activeTab.offsetLeft;
		const activeTabWidth = this.activeTab.offsetWidth;
474
		const activeTabFits = activeTabWidth <= visibleContainerWidth;
B
Benjamin Pasero 已提交
475 476

		// Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right
477 478
		// 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) {
479 480 481
			this.scrollbar.updateState({
				scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */)
			});
B
Benjamin Pasero 已提交
482 483
		}

484 485
		// Tab is overlflowng to the left or does not fit: Scroll it into view to the left
		else if (containerScrollPosX > activeTabPosX || !activeTabFits) {
486 487 488
			this.scrollbar.updateState({
				scrollLeft: this.activeTab.offsetLeft
			});
B
Benjamin Pasero 已提交
489
		}
B
Benjamin Pasero 已提交
490 491
	}

492
	private hookTabListeners(tab: HTMLElement, index: number): IDisposable {
B
Benjamin Pasero 已提交
493
		const disposables: IDisposable[] = [];
B
Benjamin Pasero 已提交
494 495

		// Open on Click
496
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
497 498
			tab.blur();

B
Benjamin Pasero 已提交
499
			const { editor, position } = this.toTabContext(index);
B
Benjamin Pasero 已提交
500
			if (e.button === 0 /* Left Button */ && !DOM.findParentWithClass((e.target || e.srcElement) as HTMLElement, 'monaco-action-bar', 'tab')) {
501 502 503 504 505
				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
506
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_UP, (e: MouseEvent) => {
507
			DOM.EventHelper.stop(e);
508
			tab.blur();
509 510

			if (e.button === 1 /* Middle Button */) {
511
				this.closeEditorAction.run(this.toTabContext(index)).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
512
			}
B
Benjamin Pasero 已提交
513
		}));
B
Benjamin Pasero 已提交
514

515
		// Context menu on Shift+F10
516
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
517 518 519 520
			const event = new StandardKeyboardEvent(e);
			if (event.shiftKey && event.keyCode === KeyCode.F10) {
				DOM.EventHelper.stop(e);

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

				this.onContextMenu({ group, editor }, e, tab);
524 525 526
			}
		}));

B
Benjamin Pasero 已提交
527
		// Keyboard accessibility
528
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
B
Benjamin Pasero 已提交
529
			const event = new StandardKeyboardEvent(e);
B
Benjamin Pasero 已提交
530
			let handled = false;
B
Benjamin Pasero 已提交
531

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

B
Benjamin Pasero 已提交
534
			// Run action on Enter/Space
A
Alexandru Dima 已提交
535
			if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
B
Benjamin Pasero 已提交
536
				handled = true;
B
Benjamin Pasero 已提交
537
				this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
538 539
			}

B
Benjamin Pasero 已提交
540
			// Navigate in editors
A
Alexandru Dima 已提交
541
			else if ([KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.Home, KeyCode.End].some(kb => event.equals(kb))) {
B
Benjamin Pasero 已提交
542
				let targetIndex: number;
A
Alexandru Dima 已提交
543
				if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.UpArrow)) {
B
Benjamin Pasero 已提交
544
					targetIndex = index - 1;
A
Alexandru Dima 已提交
545
				} else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.DownArrow)) {
B
Benjamin Pasero 已提交
546
					targetIndex = index + 1;
A
Alexandru Dima 已提交
547
				} else if (event.equals(KeyCode.Home)) {
B
Benjamin Pasero 已提交
548 549 550 551 552
					targetIndex = 0;
				} else {
					targetIndex = group.count - 1;
				}

B
Benjamin Pasero 已提交
553 554 555
				const target = group.getEditor(targetIndex);
				if (target) {
					handled = true;
556
					this.editorService.openEditor(target, { preserveFocus: true }, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
557 558 559
					(<HTMLElement>this.tabsContainer.childNodes[targetIndex]).focus();
				}
			}
B
Benjamin Pasero 已提交
560

B
Benjamin Pasero 已提交
561
			if (handled) {
562
				DOM.EventHelper.stop(e, true);
B
Benjamin Pasero 已提交
563
			}
564 565 566 567 568

			// 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 已提交
569 570
		}));

B
Benjamin Pasero 已提交
571
		// Pin on double click
572
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DBLCLICK, (e: MouseEvent) => {
B
Benjamin Pasero 已提交
573 574
			DOM.EventHelper.stop(e);

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

577
			this.editorGroupService.pinEditor(group, editor);
B
Benjamin Pasero 已提交
578
		}));
B
Benjamin Pasero 已提交
579

B
Benjamin Pasero 已提交
580
		// Context menu
581
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.CONTEXT_MENU, (e: Event) => {
582
			DOM.EventHelper.stop(e, true);
B
Benjamin Pasero 已提交
583 584 585
			const { group, editor } = this.toTabContext(index);

			this.onContextMenu({ group, editor }, e, tab);
586
		}, true /* use capture to fix https://github.com/Microsoft/vscode/issues/19145 */));
B
Benjamin Pasero 已提交
587 588

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

592
			this.onEditorDragStart({ editor, group });
593
			e.dataTransfer.effectAllowed = 'copyMove';
B
Benjamin Pasero 已提交
594

B
Benjamin Pasero 已提交
595
			// Insert transfer accordingly
596 597 598
			const fileResource = toResource(editor, { supportSideBySide: true, filter: 'file' });
			if (fileResource) {
				const resource = fileResource.toString();
B
Benjamin Pasero 已提交
599 600
				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 已提交
601
			}
B
Benjamin Pasero 已提交
602 603
		}));

604 605 606 607 608 609
		// 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 已提交
610
		// Drag over
611
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
612
			counter++;
B
Benjamin Pasero 已提交
613
			this.updateDropFeedback(tab, true, index);
B
Benjamin Pasero 已提交
614 615 616
		}));

		// Drag leave
617
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
618 619
			counter--;
			if (counter === 0) {
B
Benjamin Pasero 已提交
620
				this.updateDropFeedback(tab, false, index);
621
			}
B
Benjamin Pasero 已提交
622 623 624
		}));

		// Drag end
625
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_END, (e: DragEvent) => {
626
			counter = 0;
B
Benjamin Pasero 已提交
627
			this.updateDropFeedback(tab, false, index);
B
Benjamin Pasero 已提交
628

629
			this.onEditorDragEnd();
B
Benjamin Pasero 已提交
630 631 632
		}));

		// Drop
633
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DROP, (e: DragEvent) => {
634
			counter = 0;
B
Benjamin Pasero 已提交
635
			this.updateDropFeedback(tab, false, index);
B
Benjamin Pasero 已提交
636

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

B
Benjamin Pasero 已提交
639
			this.onDrop(e, group, position, index);
B
Benjamin Pasero 已提交
640
		}));
641 642

		return combinedDisposable(disposables);
B
Benjamin Pasero 已提交
643
	}
B
Benjamin Pasero 已提交
644

B
Benjamin Pasero 已提交
645 646 647 648 649 650 651 652
	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 已提交
653
	private onDrop(e: DragEvent, group: IEditorGroup, targetPosition: Position, targetIndex: number): void {
B
Benjamin Pasero 已提交
654
		this.updateDropFeedback(this.tabsContainer, false);
655
		DOM.removeClass(this.tabsContainer, 'scroll');
656

B
Benjamin Pasero 已提交
657 658 659 660
		// Local DND
		const draggedEditor = TabsTitleControl.getDraggedEditor();
		if (draggedEditor) {
			DOM.EventHelper.stop(e, true);
661

B
Benjamin Pasero 已提交
662 663
			// Move editor to target position and index
			if (this.isMoveOperation(e, draggedEditor.group, group)) {
664
				this.editorGroupService.moveEditor(draggedEditor.editor, draggedEditor.group, group, { index: targetIndex });
B
Benjamin Pasero 已提交
665
			}
666

B
Benjamin Pasero 已提交
667
			// Copy: just open editor at target index
668
			else {
B
Benjamin Pasero 已提交
669
				this.editorService.openEditor(draggedEditor.editor, { pinned: true, index: targetIndex }, targetPosition).done(null, errors.onUnexpectedError);
670
			}
B
Benjamin Pasero 已提交
671 672 673 674 675 676 677 678

			this.onEditorDragEnd();
		}

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

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

684
		// Handle resources
685
		if (resources.length) {
B
Benjamin Pasero 已提交
686
			DOM.EventHelper.stop(e, true);
687

688 689 690 691 692 693 694 695 696 697 698 699 700
			// 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 => {
701
				return {
702
					input: { resource: d.resource, options: { pinned: true, index: targetIndex } },
703 704
					position: targetPosition
				};
J
Joao Moreno 已提交
705
			})).then(() => {
706
				this.editorGroupService.focusGroup(targetPosition);
J
Joao Moreno 已提交
707 708
				return this.windowService.focusWindow();
			}).done(null, errors.onUnexpectedError);
709 710 711
		}
	}

712 713 714 715 716
	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 已提交
717
}
B
Benjamin Pasero 已提交
718 719 720 721 722 723 724 725 726

class TabActionRunner extends ActionRunner {

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

	public run(action: IAction, context?: any): TPromise<any> {
		const group = this.group();
B
Benjamin Pasero 已提交
727 728 729
		if (!group) {
			return TPromise.as(void 0);
		}
B
Benjamin Pasero 已提交
730 731 732

		return super.run(action, { group, editor: group.getEditor(this.index) });
	}
B
Benjamin Pasero 已提交
733 734 735 736
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {

B
Benjamin Pasero 已提交
737 738 739
	// Styling with Outline color (e.g. high contrast theme)
	const outline = theme.getColor(highContrastOutline);
	if (outline) {
B
Benjamin Pasero 已提交
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
		collector.addRule(`
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:hover  {
				outline: 1px solid;
				outline-offset: -5px;
			}

			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:hover  {
				outline: 1px dashed;
				outline-offset: -5px;
			}

			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active > .tab-close .action-label,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:hover > .tab-close .action-label,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty > .tab-close .action-label,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:hover > .tab-close .action-label {
				opacity: 1 !important;
			}
		`);
	}
});