editorPart.ts 35.1 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

B
Benjamin Pasero 已提交
8
import 'vs/workbench/browser/parts/editor/editor.contribution';
9
import { IThemeService } from 'vs/platform/theme/common/themeService';
10
import { Part } from 'vs/workbench/browser/part';
J
Joao Moreno 已提交
11
import { Dimension, isAncestor, toggleClass, addClass, $ } from 'vs/base/browser/dom';
12
import { Event, Emitter, once, Relay, anyEvent } from 'vs/base/common/event';
13
import { contrastBorder, editorBackground, registerColor } from 'vs/platform/theme/common/colorRegistry';
14
import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions, GroupsOrder, GroupChangeKind, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument } from 'vs/workbench/services/group/common/editorGroupsService';
B
Benjamin Pasero 已提交
15
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Joao Moreno 已提交
16
import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, ISerializedNode, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid';
17
import { GroupIdentifier, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
18
import { values } from 'vs/base/common/map';
19
import { EDITOR_GROUP_BORDER } from 'vs/workbench/common/theme';
20
import { distinct } from 'vs/base/common/arrays';
21
import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptions, getEditorPartOptions, impactsEditorPartOptions, IEditorPartOptionsChangeEvent, EditorGroupsServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
22
import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView';
23
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
24
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
25
import { assign } from 'vs/base/common/objects';
26
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
27
import { Scope } from 'vs/workbench/common/memento';
B
Benjamin Pasero 已提交
28
import { ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup';
29 30
import { TValueCallback, TPromise } from 'vs/base/common/winjs.base';
import { always } from 'vs/base/common/async';
31 32
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IWindowService } from 'vs/platform/windows/common/windows';
33
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
34
import { EditorDropTarget } from 'vs/workbench/browser/parts/editor/editorDropTarget';
35
import { localize } from 'vs/nls';
36
import { Color } from 'vs/base/common/color';
I
isidor 已提交
37
import { CenteredViewLayout } from 'vs/base/browser/ui/centered/centeredViewLayout';
I
isidor 已提交
38
import { IView } from 'vs/base/browser/ui/grid/gridview';
39

40
interface IEditorPartUIState {
41
	serializedGrid: ISerializedGrid;
42 43 44 45
	activeGroup: GroupIdentifier;
	mostRecentActiveGroups: GroupIdentifier[];
}

J
Joao Moreno 已提交
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
class GridWidgetView<T extends IView> implements IView {

	readonly element: HTMLElement = $('.grid-view-container');

	get minimumWidth(): number { return this.gridWidget ? this.gridWidget.minimumWidth : 0; }
	get maximumWidth(): number { return this.gridWidget ? this.gridWidget.maximumWidth : Number.POSITIVE_INFINITY; }
	get minimumHeight(): number { return this.gridWidget ? this.gridWidget.minimumHeight : 0; }
	get maximumHeight(): number { return this.gridWidget ? this.gridWidget.maximumHeight : Number.POSITIVE_INFINITY; }

	private _onDidChange = new Relay<{ width: number; height: number; }>();
	readonly onDidChange: Event<{ width: number; height: number; }> = this._onDidChange.event;

	private _gridWidget: Grid<T>;

	get gridWidget(): Grid<T> {
		return this._gridWidget;
	}

	set gridWidget(grid: Grid<T>) {
		this.element.innerHTML = '';

		if (grid) {
			this.element.appendChild(grid.element);
			this._onDidChange.input = grid.onDidChange;
		} else {
			this._onDidChange.input = Event.None;
		}

		this._gridWidget = grid;
	}

	layout(width: number, height: number): void {
		if (this.gridWidget) {
			this.gridWidget.layout(width, height);
		}
	}

	dispose(): void {
		this._onDidChange.dispose();
	}
}

88 89 90 91 92 93
export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', {
	dark: editorBackground,
	light: editorBackground,
	hc: editorBackground
}, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout."));

B
Benjamin Pasero 已提交
94
export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor {
95

B
Benjamin Pasero 已提交
96
	_serviceBrand: any;
97

B
Benjamin Pasero 已提交
98
	private static readonly EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.state';
99
	private static readonly EDITOR_PART_CENTERED_VIEW_STORAGE_KEY = 'editorpart.centeredview';
100

101 102
	//#region Events

103
	private _onDidLayout: Emitter<Dimension> = this._register(new Emitter<Dimension>());
104 105
	get onDidLayout(): Event<Dimension> { return this._onDidLayout.event; }

106 107
	private _onDidActiveGroupChange: Emitter<IEditorGroupView> = this._register(new Emitter<IEditorGroupView>());
	get onDidActiveGroupChange(): Event<IEditorGroupView> { return this._onDidActiveGroupChange.event; }
108

109 110
	private _onDidAddGroup: Emitter<IEditorGroupView> = this._register(new Emitter<IEditorGroupView>());
	get onDidAddGroup(): Event<IEditorGroupView> { return this._onDidAddGroup.event; }
111

112 113
	private _onDidRemoveGroup: Emitter<IEditorGroupView> = this._register(new Emitter<IEditorGroupView>());
	get onDidRemoveGroup(): Event<IEditorGroupView> { return this._onDidRemoveGroup.event; }
114

115 116
	private _onDidMoveGroup: Emitter<IEditorGroupView> = this._register(new Emitter<IEditorGroupView>());
	get onDidMoveGroup(): Event<IEditorGroupView> { return this._onDidMoveGroup.event; }
B
Benjamin Pasero 已提交
117

B
Benjamin Pasero 已提交
118 119 120
	private onDidSetGridWidget = this._register(new Emitter<{ width: number; height: number; }>());
	private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; }>());
	get onDidSizeConstraintsChange(): Event<{ width: number; height: number; }> { return anyEvent(this.onDidSetGridWidget.event, this._onDidSizeConstraintsChange.event); }
121

122 123
	private _onDidPreferredSizeChange: Emitter<void> = this._register(new Emitter<void>());
	get onDidPreferredSizeChange(): Event<void> { return this._onDidPreferredSizeChange.event; }
124

125 126
	//#endregion

127
	private dimension: Dimension;
128 129 130
	private _preferredSize: Dimension;

	private memento: object;
131 132
	private globalMemento: object;

133
	private _partOptions: IEditorPartOptions;
134

135 136
	private _activeGroup: IEditorGroupView;
	private groupViews: Map<GroupIdentifier, IEditorGroupView> = new Map<GroupIdentifier, IEditorGroupView>();
137
	private mostRecentActiveGroups: GroupIdentifier[] = [];
138

139
	private container: HTMLElement;
140
	private centeredLayoutWidget: CenteredViewLayout;
141
	private gridWidget: SerializableGrid<IEditorGroupView>;
J
Joao Moreno 已提交
142
	private gridWidgetView: GridWidgetView<IEditorGroupView>;
143

144
	private _whenRestored: TPromise<void>;
145 146
	private whenRestoredComplete: TValueCallback<void>;

147 148
	private previousUIState: IEditorPartUIState;

149 150
	constructor(
		id: string,
151
		private restorePreviousState: boolean,
B
Benjamin Pasero 已提交
152
		@IInstantiationService private instantiationService: IInstantiationService,
153
		@IThemeService themeService: IThemeService,
154
		@IConfigurationService private configurationService: IConfigurationService,
155
		@IStorageService private storageService: IStorageService,
156
		@INotificationService private notificationService: INotificationService,
157 158
		@IWindowService private windowService: IWindowService,
		@ILifecycleService private lifecycleService: ILifecycleService
159 160 161
	) {
		super(id, { hasTitle: false }, themeService);

J
Joao Moreno 已提交
162 163
		this.gridWidgetView = new GridWidgetView<IEditorGroupView>();

164
		this._partOptions = getEditorPartOptions(this.configurationService.getValue<IWorkbenchEditorConfiguration>());
165
		this.memento = this.getMemento(this.storageService, Scope.WORKSPACE);
166
		this.globalMemento = this.getMemento(this.storageService, Scope.GLOBAL);
167

168 169 170 171
		this._whenRestored = new TPromise(resolve => {
			this.whenRestoredComplete = resolve;
		});

172
		this.registerListeners();
173
	}
174

175
	//#region IEditorGroupsAccessor
176

177
	private enforcedPartOptions: IEditorPartOptions[] = [];
178

179 180
	private _onDidEditorPartOptionsChange: Emitter<IEditorPartOptionsChangeEvent> = this._register(new Emitter<IEditorPartOptionsChangeEvent>());
	get onDidEditorPartOptionsChange(): Event<IEditorPartOptionsChangeEvent> { return this._onDidEditorPartOptionsChange.event; }
181 182 183 184 185 186 187

	private registerListeners(): void {
		this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
	}

	private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
		if (impactsEditorPartOptions(event)) {
188
			this.handleChangedPartOptions();
189 190 191
		}
	}

192 193 194 195 196 197 198 199 200 201 202 203 204
	private handleChangedPartOptions(): void {
		const oldPartOptions = this._partOptions;
		const newPartOptions = getEditorPartOptions(this.configurationService.getValue<IWorkbenchEditorConfiguration>());

		this.enforcedPartOptions.forEach(enforcedPartOptions => {
			assign(newPartOptions, enforcedPartOptions); // check for overrides
		});

		this._partOptions = newPartOptions;

		this._onDidEditorPartOptionsChange.fire({ oldPartOptions, newPartOptions });
	}

205
	get partOptions(): IEditorPartOptions {
206 207 208
		return this._partOptions;
	}

209
	enforcePartOptions(options: IEditorPartOptions): IDisposable {
210 211 212
		this.enforcedPartOptions.push(options);
		this.handleChangedPartOptions();

213 214 215 216
		return toDisposable(() => {
			this.enforcedPartOptions.splice(this.enforcedPartOptions.indexOf(options), 1);
			this.handleChangedPartOptions();
		});
217 218
	}

219 220
	//#endregion

221
	//#region IEditorGroupsService
222

223
	get activeGroup(): IEditorGroupView {
224
		return this._activeGroup;
225 226
	}

227
	get groups(): IEditorGroupView[] {
228
		return values(this.groupViews);
229
	}
230

231 232 233 234
	get count(): number {
		return this.groupViews.size;
	}

235
	get orientation(): GroupOrientation {
236 237 238 239
		if (!this.gridWidget) {
			return void 0; // we have not been created yet
		}

240 241 242
		return this.gridWidget.orientation === Orientation.VERTICAL ? GroupOrientation.VERTICAL : GroupOrientation.HORIZONTAL;
	}

243
	get whenRestored(): TPromise<void> {
244 245 246
		return this._whenRestored;
	}

247
	getGroups(order = GroupsOrder.CREATION_TIME): IEditorGroupView[] {
248
		switch (order) {
249 250 251
			case GroupsOrder.CREATION_TIME:
				return this.groups;

252 253 254 255 256 257 258
			case GroupsOrder.MOST_RECENTLY_ACTIVE:
				const mostRecentActive = this.mostRecentActiveGroups.map(groupId => this.getGroup(groupId));

				// there can be groups that got never active, even though they exist. in this case
				// make sure to ust append them at the end so that all groups are returned properly
				return distinct([...mostRecentActive, ...this.groups]);

259
			case GroupsOrder.GRID_APPEARANCE:
260
				const views: IEditorGroupView[] = [];
261 262 263
				if (this.gridWidget) {
					this.fillGridNodes(views, this.gridWidget.getViews());
				}
264 265 266 267

				return views;
		}
	}
268

269
	private fillGridNodes(target: IEditorGroupView[], node: GridBranchNode<IEditorGroupView> | GridNode<IEditorGroupView>): void {
270 271 272 273 274
		if (isGridBranchNode(node)) {
			node.children.forEach(child => this.fillGridNodes(target, child));
		} else {
			target.push(node.view);
		}
275 276
	}

277
	getGroup(identifier: GroupIdentifier): IEditorGroupView {
278
		return this.groupViews.get(identifier);
279
	}
280

B
Benjamin Pasero 已提交
281
	findGroup(scope: IFindGroupScope, source: IEditorGroupView | GroupIdentifier = this.activeGroup, wrap?: boolean): IEditorGroupView {
282 283 284

		// by direction
		if (typeof scope.direction === 'number') {
B
Benjamin Pasero 已提交
285
			return this.doFindGroupByDirection(scope.direction, source, wrap);
286 287 288
		}

		// by location
B
Benjamin Pasero 已提交
289
		return this.doFindGroupByLocation(scope.location, source, wrap);
290 291
	}

B
Benjamin Pasero 已提交
292
	private doFindGroupByDirection(direction: GroupDirection, source: IEditorGroupView | GroupIdentifier, wrap?: boolean): IEditorGroupView {
293
		const sourceGroupView = this.assertGroupView(source);
294

295
		// Find neighbours and sort by our MRU list
B
Benjamin Pasero 已提交
296
		const neighbours = this.gridWidget.getNeighborViews(sourceGroupView, this.toGridViewDirection(direction), wrap);
297
		neighbours.sort(((n1, n2) => this.mostRecentActiveGroups.indexOf(n1.id) - this.mostRecentActiveGroups.indexOf(n2.id)));
298

299
		return neighbours[0];
300 301
	}

B
Benjamin Pasero 已提交
302
	private doFindGroupByLocation(location: GroupLocation, source: IEditorGroupView | GroupIdentifier, wrap?: boolean): IEditorGroupView {
303
		const sourceGroupView = this.assertGroupView(source);
B
Benjamin Pasero 已提交
304
		const groups = this.getGroups(GroupsOrder.GRID_APPEARANCE);
305 306 307 308 309 310 311 312
		const index = groups.indexOf(sourceGroupView);

		switch (location) {
			case GroupLocation.FIRST:
				return groups[0];
			case GroupLocation.LAST:
				return groups[groups.length - 1];
			case GroupLocation.NEXT:
B
Benjamin Pasero 已提交
313 314 315 316 317 318
				let nextGroup = groups[index + 1];
				if (!nextGroup && wrap) {
					nextGroup = this.doFindGroupByLocation(GroupLocation.FIRST, source);
				}

				return nextGroup;
319
			case GroupLocation.PREVIOUS:
B
Benjamin Pasero 已提交
320 321 322 323 324 325
				let previousGroup = groups[index - 1];
				if (!previousGroup && wrap) {
					previousGroup = this.doFindGroupByLocation(GroupLocation.LAST, source);
				}

				return previousGroup;
326 327 328
		}
	}

329
	activateGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
B
Benjamin Pasero 已提交
330 331
		const groupView = this.assertGroupView(group);
		this.doSetGroupActive(groupView);
332 333 334 335

		return groupView;
	}

336
	getSize(group: IEditorGroupView | GroupIdentifier): number {
337 338
		const groupView = this.assertGroupView(group);

339 340
		return this.gridWidget.getViewSize(groupView);
	}
341

342 343 344 345
	setSize(group: IEditorGroupView | GroupIdentifier, size: number): void {
		const groupView = this.assertGroupView(group);

		this.gridWidget.resizeView(groupView, size);
346 347
	}

348 349 350 351 352
	arrangeGroups(arrangement: GroupsArrangement): void {
		if (this.count < 2) {
			return; // require at least 2 groups to show
		}

353 354 355 356
		if (!this.gridWidget) {
			return; // we have not been created yet
		}

357 358
		// Even all group sizes
		if (arrangement === GroupsArrangement.EVEN) {
359
			this.gridWidget.distributeViewSizes();
360 361 362 363
		}

		// Maximize the current active group
		else {
364
			this.gridWidget.maximizeViewSize(this.activeGroup);
365 366 367
		}
	}

368
	setGroupOrientation(orientation: GroupOrientation): void {
369 370 371 372
		if (!this.gridWidget) {
			return; // we have not been created yet
		}

373 374 375 376
		const newOrientation = (orientation === GroupOrientation.HORIZONTAL) ? Orientation.HORIZONTAL : Orientation.VERTICAL;
		if (this.gridWidget.orientation !== newOrientation) {
			this.gridWidget.orientation = newOrientation;

377 378
			// Mark preferred size as changed
			this.resetPreferredSize();
379
		}
380 381
	}

382
	applyLayout(layout: EditorGroupLayout): void {
383
		const gridHasFocus = isAncestor(document.activeElement, this.container);
384 385

		// Determine how many groups we need overall
J
João Moreno 已提交
386
		let layoutGroupsCount = 0;
387 388 389 390 391
		function countGroups(groups: GroupLayoutArgument[]): void {
			groups.forEach(group => {
				if (Array.isArray(group.groups)) {
					countGroups(group.groups);
				} else {
J
João Moreno 已提交
392
					layoutGroupsCount++;
393 394 395 396 397 398
				}
			});
		}
		countGroups(layout.groups);

		// If we currently have too many groups, merge them into the last one
J
João Moreno 已提交
399 400 401 402 403
		let currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE);
		if (layoutGroupsCount < currentGroupViews.length) {
			const lastGroupInLayout = currentGroupViews[layoutGroupsCount - 1];
			currentGroupViews.forEach((group, index) => {
				if (index >= layoutGroupsCount) {
404 405 406 407
					this.mergeGroup(group, lastGroupInLayout);
				}
			});

J
João Moreno 已提交
408
			currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE);
409 410
		}

J
João Moreno 已提交
411
		const activeGroup = this.activeGroup;
412

J
João Moreno 已提交
413 414 415 416 417
		// Prepare grid descriptor to create new grid from
		const gridDescriptor = createSerializedGrid({
			orientation: this.toGridViewOrientation(layout.orientation, this.gridWidget.orientation),
			groups: layout.groups
		});
418

J
João Moreno 已提交
419
		// Recreate gridwidget with descriptor
420
		this.doCreateGridControlWithState(gridDescriptor, activeGroup.id, currentGroupViews);
421

J
João Moreno 已提交
422 423
		// Layout
		this.doLayout(this.dimension);
424

J
João Moreno 已提交
425 426 427 428 429
		// Update container
		this.updateContainer();

		// Mark preferred size as changed
		this.resetPreferredSize();
430

J
João Moreno 已提交
431 432 433 434 435 436
		// Events for groupd that got added
		this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(groupView => {
			if (currentGroupViews.indexOf(groupView) === -1) {
				this._onDidAddGroup.fire(groupView);
			}
		});
437

J
João Moreno 已提交
438
		// Restore focus as needed
439 440 441 442 443
		if (gridHasFocus) {
			this._activeGroup.focus();
		}
	}

444
	addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): IEditorGroupView {
B
Benjamin Pasero 已提交
445 446
		const locationView = this.assertGroupView(location);

B
Benjamin Pasero 已提交
447
		const group = this.doAddGroup(locationView, direction);
448 449 450 451 452 453

		if (options && options.activate) {
			this.doSetGroupActive(group);
		}

		return group;
B
Benjamin Pasero 已提交
454 455
	}

456
	private doAddGroup(locationView: IEditorGroupView, direction: GroupDirection, groupToCopy?: IEditorGroupView): IEditorGroupView {
B
Benjamin Pasero 已提交
457
		const newGroupView = this.doCreateGroupView(groupToCopy);
458 459

		// Add to grid widget
J
Joao Moreno 已提交
460
		this.gridWidget.addView(
J
Joao Moreno 已提交
461
			newGroupView,
J
Joao Moreno 已提交
462
			Sizing.Distribute,
B
Benjamin Pasero 已提交
463
			locationView,
464 465
			this.toGridViewDirection(direction),
		);
B
Benjamin Pasero 已提交
466

467 468 469
		// Update container
		this.updateContainer();

470 471 472
		// Mark preferred size as changed
		this.resetPreferredSize();

473 474 475
		// Event
		this._onDidAddGroup.fire(newGroupView);

476
		return newGroupView;
477 478
	}

479
	private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroup): IEditorGroupView {
480

481 482 483
		// Label: just use the number of existing groups as label
		const label = this.getGroupLabel(this.count + 1);

484
		// Create group view
485 486 487
		let groupView: IEditorGroupView;
		if (from instanceof EditorGroupView) {
			groupView = EditorGroupView.createCopy(from, this, label, this.instantiationService);
488
		} else if (isSerializedEditorGroup(from)) {
489
			groupView = EditorGroupView.createFromSerialized(from, this, label, this.instantiationService);
490
		} else {
491
			groupView = EditorGroupView.createNew(this, label, this.instantiationService);
492 493
		}

494 495
		// Keep in map
		this.groupViews.set(groupView.id, groupView);
496

497 498 499 500 501
		// Track focus
		let groupDisposables: IDisposable[] = [];
		groupDisposables.push(groupView.onDidFocus(() => {
			this.doSetGroupActive(groupView);
		}));
502

503
		// Track editor change
504 505 506 507
		groupDisposables.push(groupView.onDidGroupChange(e => {
			if (e.kind === GroupChangeKind.EDITOR_ACTIVE) {
				this.updateContainer();
			}
508
		}));
509

510 511 512 513 514 515
		// Track dispose
		once(groupView.onWillDispose)(() => {
			groupDisposables = dispose(groupDisposables);
			this.groupViews.delete(groupView.id);
			this.doUpdateMostRecentActive(groupView);
		});
516

517
		return groupView;
518 519
	}

520
	private doSetGroupActive(group: IEditorGroupView): void {
521 522 523 524 525
		if (this._activeGroup === group) {
			return; // return if this is already the active group
		}

		const previousActiveGroup = this._activeGroup;
526
		this._activeGroup = group;
527

528 529 530
		// Update list of most recently active groups
		this.doUpdateMostRecentActive(group, true);

531 532 533 534 535 536 537 538 539
		// Mark previous one as inactive
		if (previousActiveGroup) {
			previousActiveGroup.setActive(false);
		}

		// Mark group as new active
		group.setActive(true);

		// Event
540
		this._onDidActiveGroupChange.fire(group);
541 542
	}

543
	private doUpdateMostRecentActive(group: IEditorGroupView, makeMostRecentlyActive?: boolean): void {
544
		const index = this.mostRecentActiveGroups.indexOf(group.id);
545 546

		// Remove from MRU list
547
		if (index !== -1) {
548
			this.mostRecentActiveGroups.splice(index, 1);
549 550
		}

551
		// Add to front as needed
552
		if (makeMostRecentlyActive) {
553
			this.mostRecentActiveGroups.unshift(group.id);
554 555 556
		}
	}

B
Benjamin Pasero 已提交
557 558 559 560 561 562 563 564 565
	private toGridViewDirection(direction: GroupDirection): Direction {
		switch (direction) {
			case GroupDirection.UP: return Direction.Up;
			case GroupDirection.DOWN: return Direction.Down;
			case GroupDirection.LEFT: return Direction.Left;
			case GroupDirection.RIGHT: return Direction.Right;
		}
	}

J
João Moreno 已提交
566 567 568 569 570 571 572 573
	private toGridViewOrientation(orientation: GroupOrientation, fallback?: Orientation): Orientation {
		if (typeof orientation === 'number') {
			return orientation === GroupOrientation.HORIZONTAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
		}

		return fallback;
	}

574
	removeGroup(group: IEditorGroupView | GroupIdentifier): void {
B
Benjamin Pasero 已提交
575
		const groupView = this.assertGroupView(group);
B
Benjamin Pasero 已提交
576 577
		if (this.groupViews.size === 1) {
			return; // Cannot remove the last root group
578
		}
579

B
Benjamin Pasero 已提交
580 581 582 583 584 585
		// Remove empty group
		if (groupView.isEmpty()) {
			return this.doRemoveEmptyGroup(groupView);
		}

		// Remove group with editors
586
		this.doRemoveGroupWithEditors(groupView);
B
Benjamin Pasero 已提交
587 588
	}

589
	private doRemoveGroupWithEditors(groupView: IEditorGroupView): void {
590
		const mostRecentlyActiveGroups = this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
B
Benjamin Pasero 已提交
591

592
		let lastActiveGroup: IEditorGroupView;
B
Benjamin Pasero 已提交
593 594 595 596 597 598 599 600
		if (this._activeGroup === groupView) {
			lastActiveGroup = mostRecentlyActiveGroups[1];
		} else {
			lastActiveGroup = mostRecentlyActiveGroups[0];
		}

		// Removing a group with editors should merge these editors into the
		// last active group and then remove this group.
601
		this.mergeGroup(groupView, lastActiveGroup);
B
Benjamin Pasero 已提交
602 603
	}

604
	private doRemoveEmptyGroup(groupView: IEditorGroupView): void {
605
		const gridHasFocus = isAncestor(document.activeElement, this.container);
606

607 608
		// Activate next group if the removed one was active
		if (this._activeGroup === groupView) {
609 610 611
			const mostRecentlyActiveGroups = this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
			const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current group we are about to dispose
			this.activateGroup(nextActiveGroup);
612
		}
613

614
		// Remove from grid widget & dispose
615
		this.gridWidget.removeView(groupView, Sizing.Distribute);
616
		groupView.dispose();
617

618 619
		// Restore focus if we had it previously (we run this after gridWidget.removeView() is called
		// because removing a view can mean to reparent it and thus focus would be removed otherwise)
620
		if (gridHasFocus) {
B
Benjamin Pasero 已提交
621
			this._activeGroup.focus();
622 623
		}

624 625 626 627 628 629
		// Update labels: since our labels are created using the index of the
		// group, removing a group might produce gaps. So we iterate over all
		// groups and reassign the label based on the index.
		this.getGroups(GroupsOrder.CREATION_TIME).forEach((group, index) => {
			group.setLabel(this.getGroupLabel(index + 1));
		});
630

631 632
		// Update container
		this.updateContainer();
633

634 635 636
		// Mark preferred size as changed
		this.resetPreferredSize();

637
		// Event
638 639
		this._onDidRemoveGroup.fire(groupView);
	}
640

641 642 643 644
	private getGroupLabel(index: number): string {
		return localize('groupLabel', "Group {0}", index);
	}

645
	moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
646 647
		const sourceView = this.assertGroupView(group);
		const targetView = this.assertGroupView(location);
648

649 650
		if (sourceView.id === targetView.id) {
			throw new Error('Cannot move group into its own');
B
Benjamin Pasero 已提交
651 652
		}

653 654 655 656 657
		const groupHasFocus = isAncestor(document.activeElement, sourceView.element);

		// Move is a simple remove and add of the same view
		this.gridWidget.removeView(sourceView, Sizing.Distribute);
		this.gridWidget.addView(sourceView, Sizing.Distribute, targetView, this.toGridViewDirection(direction));
B
Benjamin Pasero 已提交
658 659 660 661

		// Restore focus if we had it previously (we run this after gridWidget.removeView() is called
		// because removing a view can mean to reparent it and thus focus would be removed otherwise)
		if (groupHasFocus) {
B
Benjamin Pasero 已提交
662
			sourceView.focus();
B
Benjamin Pasero 已提交
663 664 665
		}

		// Event
666
		this._onDidMoveGroup.fire(sourceView);
B
Benjamin Pasero 已提交
667

668
		return sourceView;
B
Benjamin Pasero 已提交
669 670
	}

671
	copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
B
Benjamin Pasero 已提交
672 673 674
		const groupView = this.assertGroupView(group);
		const locationView = this.assertGroupView(location);

675 676 677 678 679 680 681
		const groupHasFocus = isAncestor(document.activeElement, groupView.element);

		// Copy the group view
		const copiedGroupView = this.doAddGroup(locationView, direction, groupView);

		// Restore focus if we had it
		if (groupHasFocus) {
B
Benjamin Pasero 已提交
682
			copiedGroupView.focus();
683 684 685
		}

		return copiedGroupView;
B
Benjamin Pasero 已提交
686 687
	}

688
	mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroupView {
B
Benjamin Pasero 已提交
689 690 691
		const sourceView = this.assertGroupView(group);
		const targetView = this.assertGroupView(target);

B
Benjamin Pasero 已提交
692
		// Move/Copy editors over into target
B
Benjamin Pasero 已提交
693
		let index = (options && typeof options.index === 'number') ? options.index : targetView.count;
B
Benjamin Pasero 已提交
694
		sourceView.editors.forEach(editor => {
B
Benjamin Pasero 已提交
695
			const inactive = !sourceView.isActive(editor);
696 697 698 699 700 701 702
			const copyOptions: ICopyEditorOptions = { index, inactive, preserveFocus: inactive };

			if (options && options.mode === MergeGroupMode.COPY_EDITORS) {
				sourceView.copyEditor(editor, targetView, copyOptions);
			} else {
				sourceView.moveEditor(editor, targetView, copyOptions);
			}
B
Benjamin Pasero 已提交
703 704 705 706

			index++;
		});

707 708
		// Remove source if the view is now empty and not already removed
		if (sourceView.isEmpty() && !sourceView.disposed /* could have been disposed already via workbench.editor.closeEmptyGroups setting */) {
709 710 711 712
			this.removeGroup(sourceView);
		}

		return targetView;
B
Benjamin Pasero 已提交
713 714
	}

715
	private assertGroupView(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
716
		if (typeof group === 'number') {
B
Benjamin Pasero 已提交
717 718 719 720 721
			group = this.getGroup(group);
		}

		if (!group) {
			throw new Error('Invalid editor group provided!');
722 723 724
		}

		return group;
725 726
	}

727 728
	//#endregion

729
	//#region Part
730

B
Benjamin Pasero 已提交
731 732 733 734 735
	get minimumWidth(): number { return this.gridWidget ? this.gridWidget.minimumWidth : 0; }
	get maximumWidth(): number { return this.gridWidget ? this.gridWidget.maximumWidth : Number.POSITIVE_INFINITY; }
	get minimumHeight(): number { return this.gridWidget ? this.gridWidget.minimumHeight : 0; }
	get maximumHeight(): number { return this.gridWidget ? this.gridWidget.maximumHeight : Number.POSITIVE_INFINITY; }

736 737
	get preferredSize(): Dimension {
		if (!this._preferredSize) {
738
			this._preferredSize = new Dimension(this.gridWidget.minimumWidth, this.gridWidget.minimumHeight);
739 740 741 742 743 744 745 746 747 748 749 750
		}

		return this._preferredSize;
	}

	private resetPreferredSize(): void {

		// Reset (will be computed upon next access)
		this._preferredSize = void 0;

		// Event
		this._onDidPreferredSizeChange.fire();
751 752
	}

J
João Moreno 已提交
753
	private get gridSeparatorBorder(): Color {
754 755 756
		return this.theme.getColor(EDITOR_GROUP_BORDER) || this.theme.getColor(contrastBorder) || Color.transparent;
	}

757
	protected updateStyles(): void {
758
		this.container.style.backgroundColor = this.getColor(editorBackground);
759

760
		const separatorBorderStyle = { separatorBorder: this.gridSeparatorBorder, background: this.theme.getColor(EDITOR_PANE_BACKGROUND) || Color.transparent };
761
		this.gridWidget.style(separatorBorderStyle);
762
		this.centeredLayoutWidget.styles(separatorBorderStyle);
763 764 765 766
	}

	createContentArea(parent: HTMLElement): HTMLElement {

767
		// Container
768 769 770
		this.container = document.createElement('div');
		addClass(this.container, 'content');
		parent.appendChild(this.container);
771

772
		// Grid control with center layout
773
		this.doCreateGridControl();
J
Joao Moreno 已提交
774 775

		this.centeredLayoutWidget = this._register(new CenteredViewLayout(this.container, this.gridWidgetView, this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY]));
776

B
Benjamin Pasero 已提交
777
		// Drop support
778
		this._register(this.instantiationService.createInstance(EditorDropTarget, this, this.container));
779

780
		return this.container;
781 782
	}

I
isidor 已提交
783
	centerLayout(active: boolean): void {
784
		this.centeredLayoutWidget.activate(active);
I
isidor 已提交
785 786 787
	}

	isLayoutCentered(): boolean {
788
		return this.centeredLayoutWidget.isActive();
I
isidor 已提交
789 790
	}

791
	private doCreateGridControl(): void {
792

793 794
		// Grid Widget (with previous UI state)
		if (this.restorePreviousState) {
795
			this.doCreateGridControlWithPreviousState();
796 797 798 799 800
		}

		// Grid Widget (no previous UI state or failed to restore)
		if (!this.gridWidget) {
			const initialGroup = this.doCreateGroupView();
B
Benjamin Pasero 已提交
801
			this.doSetGridWidget(new SerializableGrid(initialGroup));
802 803 804 805 806 807 808 809 810 811 812 813

			// Ensure a group is active
			this.doSetGroupActive(initialGroup);
		}

		// Signal restored
		always(TPromise.join(this.groups.map(group => group.whenRestored)), () => this.whenRestoredComplete(void 0));

		// Update container
		this.updateContainer();
	}

814
	private doCreateGridControlWithPreviousState(): void {
815 816
		const uiState = this.doGetPreviousState();
		if (uiState && uiState.serializedGrid) {
817
			try {
818
				this.previousUIState = uiState;
819

820 821
				// MRU
				this.mostRecentActiveGroups = uiState.mostRecentActiveGroups;
B
Benjamin Pasero 已提交
822

823
				// Grid Widget
824
				this.doCreateGridControlWithState(uiState.serializedGrid, uiState.activeGroup);
825

826 827 828 829
				// Ensure last active group has focus
				this._activeGroup.focus();
			} catch (error) {
				if (this.gridWidget) {
B
Benjamin Pasero 已提交
830
					this.doSetGridWidget();
831 832
				}

833 834 835 836 837
				this.groupViews.forEach(group => group.dispose());
				this.groupViews.clear();
				this._activeGroup = void 0;
				this.mostRecentActiveGroups = [];

B
Benjamin Pasero 已提交
838
				this.gridError(error); // TODO@ben remove this safe guard once the grid is stable
839
			}
840
		}
841
	}
842

843
	private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void {
844

J
João Moreno 已提交
845 846 847 848 849 850 851 852 853
		// Determine group views to reuse if any
		let reuseGroupViews: IEditorGroupView[];
		if (editorGroupViewsToReuse) {
			reuseGroupViews = editorGroupViewsToReuse.slice(0); // do not modify original array
		} else {
			reuseGroupViews = [];
		}

		// Create new
J
Joao Moreno 已提交
854
		const gridWidget = SerializableGrid.deserialize(serializedGrid, {
J
João Moreno 已提交
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
			fromJSON: (serializedEditorGroup: ISerializedEditorGroup) => {
				let groupView: IEditorGroupView;
				if (reuseGroupViews.length > 0) {
					groupView = reuseGroupViews.shift();
				} else {
					groupView = this.doCreateGroupView(serializedEditorGroup);
				}

				if (groupView.id === activeGroupId) {
					this.doSetGroupActive(groupView);
				}

				return groupView;
			}
		}, { styles: { separatorBorder: this.gridSeparatorBorder } });
J
Joao Moreno 已提交
870 871 872

		// Set it
		this.doSetGridWidget(gridWidget);
J
João Moreno 已提交
873 874
	}

B
Benjamin Pasero 已提交
875
	private doSetGridWidget(gridWidget?: SerializableGrid<IEditorGroupView>): void {
J
Joao Moreno 已提交
876 877 878 879
		if (this.gridWidget) {
			this.gridWidget.dispose();
		}

B
Benjamin Pasero 已提交
880
		this.gridWidget = gridWidget;
J
Joao Moreno 已提交
881
		this.gridWidgetView.gridWidget = gridWidget;
B
Benjamin Pasero 已提交
882 883 884 885 886 887

		if (gridWidget) {
			this._onDidSizeConstraintsChange.input = gridWidget.onDidChange;
		}

		this.onDidSetGridWidget.fire();
J
João Moreno 已提交
888 889
	}

890
	private doGetPreviousState(): IEditorPartUIState {
891 892 893 894 895
		const legacyState = this.doGetPreviousLegacyState();
		if (legacyState) {
			return legacyState; // TODO@ben remove after a while
		}

896
		return this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] as IEditorPartUIState;
897 898
	}

899
	private doGetPreviousLegacyState(): IEditorPartUIState {
900 901
		const LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.uiState';
		const LEGACY_STACKS_MODEL_STORAGE_KEY = 'editorStacks.model';
902

903 904
		interface ILegacyEditorPartUIState {
			ratio: number[];
905
			groupOrientation: 'vertical' | 'horizontal';
906 907
		}

908 909 910 911
		interface ISerializedLegacyEditorStacksModel {
			groups: ISerializedEditorGroup[];
			active: number;
		}
912

913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
		let legacyUIState: ISerializedLegacyEditorStacksModel;
		const legacyUIStateRaw = this.storageService.get(LEGACY_STACKS_MODEL_STORAGE_KEY, StorageScope.WORKSPACE);
		if (legacyUIStateRaw) {
			try {
				legacyUIState = JSON.parse(legacyUIStateRaw);
			} catch (error) { /* ignore */ }
		}

		if (legacyUIState) {
			this.storageService.remove(LEGACY_STACKS_MODEL_STORAGE_KEY, StorageScope.WORKSPACE);
		}

		const legacyPartState = this.memento[LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY] as ILegacyEditorPartUIState;
		if (legacyPartState) {
			delete this.memento[LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY];
		}

		if (legacyUIState && Array.isArray(legacyUIState.groups) && legacyUIState.groups.length > 0) {
			const splitHorizontally = legacyPartState && legacyPartState.groupOrientation === 'horizontal';

933
			const legacyState: IEditorPartUIState = Object.create(null);
934

935 936 937
			const positionOneGroup = legacyUIState.groups[0];
			const positionTwoGroup = legacyUIState.groups[1];
			const positionThreeGroup = legacyUIState.groups[2];
938

939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954
			legacyState.activeGroup = legacyUIState.active;
			legacyState.mostRecentActiveGroups = [legacyUIState.active];

			if (positionTwoGroup || positionThreeGroup) {
				if (!positionThreeGroup) {
					legacyState.mostRecentActiveGroups.push(legacyState.activeGroup === 0 ? 1 : 0);
				} else {
					if (legacyState.activeGroup === 0) {
						legacyState.mostRecentActiveGroups.push(1, 2);
					} else if (legacyState.activeGroup === 1) {
						legacyState.mostRecentActiveGroups.push(0, 2);
					} else {
						legacyState.mostRecentActiveGroups.push(0, 1);
					}
				}
			}
955

956 957 958 959 960 961
			const toNode = function (group: ISerializedEditorGroup, size: number): ISerializedNode {
				return {
					data: group,
					size,
					type: 'leaf'
				};
962 963
			};

964
			const baseSize = 1200; // just some number because layout() was not called yet, but we only need the proportions
965

966 967 968 969 970 971 972 973
			// No split editor
			if (!positionTwoGroup) {
				legacyState.serializedGrid = {
					width: baseSize,
					height: baseSize,
					orientation: splitHorizontally ? Orientation.VERTICAL : Orientation.HORIZONTAL,
					root: toNode(positionOneGroup, baseSize)
				};
974 975
			}

976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998
			// Split editor (2 or 3 columns)
			else {
				const children: ISerializedNode[] = [];

				const size = positionThreeGroup ? baseSize / 3 : baseSize / 2;

				children.push(toNode(positionOneGroup, size));
				children.push(toNode(positionTwoGroup, size));

				if (positionThreeGroup) {
					children.push(toNode(positionThreeGroup, size));
				}

				legacyState.serializedGrid = {
					width: baseSize,
					height: baseSize,
					orientation: splitHorizontally ? Orientation.VERTICAL : Orientation.HORIZONTAL,
					root: {
						data: children,
						size: baseSize,
						type: 'branch'
					}
				};
999 1000
			}

1001
			return legacyState;
1002 1003
		}

1004
		return void 0;
1005 1006 1007
	}

	private updateContainer(): void {
1008
		toggleClass(this.container, 'empty', this.isEmpty());
B
Benjamin Pasero 已提交
1009 1010 1011 1012
	}

	private isEmpty(): boolean {
		return this.groupViews.size === 1 && this._activeGroup.isEmpty();
1013 1014
	}

B
Benjamin Pasero 已提交
1015
	// TODO@ben this should be removed once the gridwidget is stable
1016
	private gridError(error: Error): void {
1017
		console.error(error);
1018

1019 1020
		if (this.previousUIState) {
			console.error('Serialized Grid State: ', this.previousUIState);
1021 1022
		}

1023 1024 1025
		this.lifecycleService.when(LifecyclePhase.Running).then(() => {
			this.notificationService.prompt(Severity.Error, `Grid Issue: ${error}. Please report this error stack with reproducible steps.`, [{ label: 'Open DevTools', run: () => this.windowService.openDevTools() }]);
		});
1026 1027
	}

B
Benjamin Pasero 已提交
1028
	layout(dimension: Dimension): Dimension[] {
1029 1030
		const sizes = super.layout(dimension);

J
João Moreno 已提交
1031 1032 1033 1034 1035 1036 1037
		this.doLayout(sizes[1]);

		return sizes;
	}

	private doLayout(dimension: Dimension): void {
		this.dimension = dimension;
1038

1039
		// Layout Grid
1040
		try {
1041
			this.centeredLayoutWidget.layout(this.dimension.width, this.dimension.height);
1042 1043 1044
		} catch (error) {
			this.gridError(error);
		}
1045

1046
		// Event
1047
		this._onDidLayout.fire(dimension);
1048 1049
	}

1050 1051
	shutdown(): void {

1052
		// Persist grid UI state
1053
		if (this.gridWidget) {
1054
			const uiState: IEditorPartUIState = {
1055 1056 1057 1058 1059 1060
				serializedGrid: this.gridWidget.serialize(),
				activeGroup: this._activeGroup.id,
				mostRecentActiveGroups: this.mostRecentActiveGroups
			};

			if (this.isEmpty()) {
1061
				delete this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY];
1062
			} else {
1063
				this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] = uiState;
1064
			}
1065
		}
1066 1067 1068

		// Persist centered view state
		this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY] = this.centeredLayoutWidget.state;
1069

1070
		// Forward to all groups
1071
		this.groupViews.forEach(group => group.shutdown());
1072 1073 1074 1075 1076 1077 1078

		super.shutdown();
	}

	dispose(): void {

		// Forward to all groups
1079 1080
		this.groupViews.forEach(group => group.dispose());
		this.groupViews.clear();
1081

J
João Moreno 已提交
1082 1083
		// Grid widget
		if (this.gridWidget) {
J
Joao Moreno 已提交
1084
			this.gridWidget.dispose();
J
João Moreno 已提交
1085 1086
		}

1087 1088 1089
		super.dispose();
	}

1090
	//#endregion
J
João Moreno 已提交
1091
}