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

B
Benjamin Pasero 已提交
6
import 'vs/workbench/browser/parts/editor/editor.contribution';
7
import { IThemeService } from 'vs/platform/theme/common/themeService';
8
import { Part } from 'vs/workbench/browser/part';
J
Joao Moreno 已提交
9
import { Dimension, isAncestor, toggleClass, addClass, $ } from 'vs/base/browser/dom';
J
Joao Moreno 已提交
10
import { Event, Emitter, Relay } from 'vs/base/common/event';
B
Benjamin Pasero 已提交
11
import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry';
12
import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions, GroupsOrder, GroupChangeKind, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
13
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Joao Moreno 已提交
14
import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid';
15
import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorPartOptions } from 'vs/workbench/common/editor';
16
import { values } from 'vs/base/common/map';
B
Benjamin Pasero 已提交
17
import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme';
B
Benjamin Pasero 已提交
18
import { distinct, coalesce } from 'vs/base/common/arrays';
19
import { IEditorGroupsAccessor, IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartOptionsChangeEvent, IEditorPartCreationOptions } from 'vs/workbench/browser/parts/editor/editor';
20
import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView';
21
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
22
import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
23
import { assign } from 'vs/base/common/objects';
B
Benjamin Pasero 已提交
24
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
B
Benjamin Pasero 已提交
25
import { ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup';
26
import { EditorDropTarget, EditorDropTargetDelegate } from 'vs/workbench/browser/parts/editor/editorDropTarget';
27
import { Color } from 'vs/base/common/color';
I
isidor 已提交
28
import { CenteredViewLayout } from 'vs/base/browser/ui/centered/centeredViewLayout';
29
import { onUnexpectedError } from 'vs/base/common/errors';
30
import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
31
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
32
import { MementoObject } from 'vs/workbench/common/memento';
B
Benjamin Pasero 已提交
33
import { assertIsDefined } from 'vs/base/common/types';
34

35
interface IEditorPartUIState {
36
	serializedGrid: ISerializedGrid;
37 38 39 40
	activeGroup: GroupIdentifier;
	mostRecentActiveGroups: GroupIdentifier[];
}

J
Joao Moreno 已提交
41 42 43 44 45 46 47 48 49
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; }

B
Benjamin Pasero 已提交
50 51
	private _onDidChange = new Relay<{ width: number; height: number; } | undefined>();
	readonly onDidChange: Event<{ width: number; height: number; } | undefined> = this._onDidChange.event;
J
Joao Moreno 已提交
52

B
Benjamin Pasero 已提交
53
	private _gridWidget: Grid<T> | undefined;
J
Joao Moreno 已提交
54

B
Benjamin Pasero 已提交
55
	get gridWidget(): Grid<T> | undefined {
J
Joao Moreno 已提交
56 57 58
		return this._gridWidget;
	}

B
Benjamin Pasero 已提交
59
	set gridWidget(grid: Grid<T> | undefined) {
J
Joao Moreno 已提交
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
		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();
	}
}

83
export class EditorPart extends Part implements IEditorGroupsService, IEditorGroupsAccessor {
84

85
	_serviceBrand: undefined;
86

B
Benjamin Pasero 已提交
87
	private static readonly EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.state';
88
	private static readonly EDITOR_PART_CENTERED_VIEW_STORAGE_KEY = 'editorpart.centeredview';
89

90 91
	//#region Events

92 93
	private readonly _onDidLayout = this._register(new Emitter<Dimension>());
	readonly onDidLayout = this._onDidLayout.event;
94

95 96
	private readonly _onDidActiveGroupChange = this._register(new Emitter<IEditorGroupView>());
	readonly onDidActiveGroupChange = this._onDidActiveGroupChange.event;
97

98 99
	private readonly _onDidGroupIndexChange = this._register(new Emitter<IEditorGroupView>());
	readonly onDidGroupIndexChange = this._onDidGroupIndexChange.event;
100

101 102
	private readonly _onDidActivateGroup = this._register(new Emitter<IEditorGroupView>());
	readonly onDidActivateGroup = this._onDidActivateGroup.event;
B
Benjamin Pasero 已提交
103

104 105
	private readonly _onDidAddGroup = this._register(new Emitter<IEditorGroupView>());
	readonly onDidAddGroup = this._onDidAddGroup.event;
106

107 108
	private readonly _onDidRemoveGroup = this._register(new Emitter<IEditorGroupView>());
	readonly onDidRemoveGroup = this._onDidRemoveGroup.event;
109

110 111
	private readonly _onDidMoveGroup = this._register(new Emitter<IEditorGroupView>());
	readonly onDidMoveGroup = this._onDidMoveGroup.event;
B
Benjamin Pasero 已提交
112

B
Benjamin Pasero 已提交
113 114 115
	private onDidSetGridWidget = this._register(new Emitter<{ width: number; height: number; } | undefined>());
	private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; } | undefined>());
	get onDidSizeConstraintsChange(): Event<{ width: number; height: number; } | undefined> { return Event.any(this.onDidSetGridWidget.event, this._onDidSizeConstraintsChange.event); }
116

117 118
	//#endregion

119 120
	private readonly workspaceMemento: MementoObject;
	private readonly globalMemento: MementoObject;
121

122
	private _partOptions: IEditorPartOptions;
123

124
	private groupViews: Map<GroupIdentifier, IEditorGroupView> = new Map<GroupIdentifier, IEditorGroupView>();
125
	private mostRecentActiveGroups: GroupIdentifier[] = [];
126

B
Benjamin Pasero 已提交
127 128 129 130 131
	private container: HTMLElement | undefined;

	private centeredLayoutWidget!: CenteredViewLayout;

	private gridWidget!: SerializableGrid<IEditorGroupView>;
J
Joao Moreno 已提交
132
	private gridWidgetView: GridWidgetView<IEditorGroupView>;
133

J
Johannes Rieken 已提交
134
	private _whenRestored: Promise<void>;
B
Benjamin Pasero 已提交
135
	private whenRestoredResolve: (() => void) | undefined;
136

137
	constructor(
138
		@IInstantiationService private readonly instantiationService: IInstantiationService,
139
		@IThemeService themeService: IThemeService,
140
		@IConfigurationService private readonly configurationService: IConfigurationService,
141
		@IStorageService storageService: IStorageService,
142
		@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService
143
	) {
144
		super(Parts.EDITOR_PART, { hasTitle: false }, themeService, storageService, layoutService);
145

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

148
		this._partOptions = getEditorPartOptions(this.configurationService.getValue<IWorkbenchEditorConfiguration>());
B
Benjamin Pasero 已提交
149 150 151

		this.workspaceMemento = this.getMemento(StorageScope.WORKSPACE);
		this.globalMemento = this.getMemento(StorageScope.GLOBAL);
152

153
		this._whenRestored = new Promise(resolve => (this.whenRestoredResolve = resolve));
154

155
		this.registerListeners();
156
	}
157

158
	//#region IEditorGroupsAccessor
159

160
	private enforcedPartOptions: IEditorPartOptions[] = [];
161

M
Matt Bierner 已提交
162
	private readonly _onDidEditorPartOptionsChange: Emitter<IEditorPartOptionsChangeEvent> = this._register(new Emitter<IEditorPartOptionsChangeEvent>());
163
	readonly onDidEditorPartOptionsChange: Event<IEditorPartOptionsChangeEvent> = this._onDidEditorPartOptionsChange.event;
164 165 166 167 168 169 170

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

	private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
		if (impactsEditorPartOptions(event)) {
171
			this.handleChangedPartOptions();
172 173 174
		}
	}

175 176 177 178 179 180 181 182 183 184 185 186 187
	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 });
	}

188
	get partOptions(): IEditorPartOptions {
189 190 191
		return this._partOptions;
	}

192
	enforcePartOptions(options: IEditorPartOptions): IDisposable {
193 194 195
		this.enforcedPartOptions.push(options);
		this.handleChangedPartOptions();

196 197 198 199
		return toDisposable(() => {
			this.enforcedPartOptions.splice(this.enforcedPartOptions.indexOf(options), 1);
			this.handleChangedPartOptions();
		});
200 201
	}

202 203
	//#endregion

204
	//#region IEditorGroupsService
205

B
Benjamin Pasero 已提交
206
	private _contentDimension!: Dimension;
J
Joao Moreno 已提交
207
	get contentDimension(): Dimension { return this._contentDimension; }
B
Benjamin Pasero 已提交
208

B
Benjamin Pasero 已提交
209
	private _activeGroup!: IEditorGroupView;
210
	get activeGroup(): IEditorGroupView {
211
		return this._activeGroup;
212 213
	}

214
	get groups(): IEditorGroupView[] {
215
		return values(this.groupViews);
216
	}
217

218 219 220 221
	get count(): number {
		return this.groupViews.size;
	}

222
	get orientation(): GroupOrientation {
B
Benjamin Pasero 已提交
223
		return (this.gridWidget && this.gridWidget.orientation === Orientation.VERTICAL) ? GroupOrientation.VERTICAL : GroupOrientation.HORIZONTAL;
224 225
	}

J
Johannes Rieken 已提交
226
	get whenRestored(): Promise<void> {
227 228 229
		return this._whenRestored;
	}

B
Benjamin Pasero 已提交
230 231 232 233
	get willRestoreEditors(): boolean {
		return !!this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY];
	}

234
	getGroups(order = GroupsOrder.CREATION_TIME): IEditorGroupView[] {
235
		switch (order) {
236 237 238
			case GroupsOrder.CREATION_TIME:
				return this.groups;

239
			case GroupsOrder.MOST_RECENTLY_ACTIVE:
B
Benjamin Pasero 已提交
240
				const mostRecentActive = coalesce(this.mostRecentActiveGroups.map(groupId => this.getGroup(groupId)));
241 242 243 244 245

				// 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]);

246
			case GroupsOrder.GRID_APPEARANCE:
247
				const views: IEditorGroupView[] = [];
248 249 250
				if (this.gridWidget) {
					this.fillGridNodes(views, this.gridWidget.getViews());
				}
251 252 253 254

				return views;
		}
	}
255

256
	private fillGridNodes(target: IEditorGroupView[], node: GridBranchNode<IEditorGroupView> | GridNode<IEditorGroupView>): void {
257 258 259 260 261
		if (isGridBranchNode(node)) {
			node.children.forEach(child => this.fillGridNodes(target, child));
		} else {
			target.push(node.view);
		}
262 263
	}

B
Benjamin Pasero 已提交
264
	getGroup(identifier: GroupIdentifier): IEditorGroupView | undefined {
265
		return this.groupViews.get(identifier);
266
	}
267

B
Benjamin Pasero 已提交
268
	findGroup(scope: IFindGroupScope, source: IEditorGroupView | GroupIdentifier = this.activeGroup, wrap?: boolean): IEditorGroupView {
269 270 271

		// by direction
		if (typeof scope.direction === 'number') {
B
Benjamin Pasero 已提交
272
			return this.doFindGroupByDirection(scope.direction, source, wrap);
273 274 275
		}

		// by location
B
Benjamin Pasero 已提交
276 277 278 279 280
		if (typeof scope.location === 'number') {
			return this.doFindGroupByLocation(scope.location, source, wrap);
		}

		throw new Error('invalid arguments');
281 282
	}

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

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

290
		return neighbours[0];
291 292
	}

B
Benjamin Pasero 已提交
293
	private doFindGroupByLocation(location: GroupLocation, source: IEditorGroupView | GroupIdentifier, wrap?: boolean): IEditorGroupView {
294
		const sourceGroupView = this.assertGroupView(source);
B
Benjamin Pasero 已提交
295
		const groups = this.getGroups(GroupsOrder.GRID_APPEARANCE);
296 297 298 299 300 301 302 303
		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 已提交
304 305 306 307 308 309
				let nextGroup = groups[index + 1];
				if (!nextGroup && wrap) {
					nextGroup = this.doFindGroupByLocation(GroupLocation.FIRST, source);
				}

				return nextGroup;
310
			case GroupLocation.PREVIOUS:
B
Benjamin Pasero 已提交
311 312 313 314 315 316
				let previousGroup = groups[index - 1];
				if (!previousGroup && wrap) {
					previousGroup = this.doFindGroupByLocation(GroupLocation.LAST, source);
				}

				return previousGroup;
317 318 319
		}
	}

320
	activateGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
B
Benjamin Pasero 已提交
321 322
		const groupView = this.assertGroupView(group);
		this.doSetGroupActive(groupView);
323

324
		this._onDidActivateGroup.fire(groupView);
325 326 327
		return groupView;
	}

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

		return groupView;
	}

335
	getSize(group: IEditorGroupView | GroupIdentifier): { width: number, height: number } {
336 337
		const groupView = this.assertGroupView(group);

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

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

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

B
Benjamin Pasero 已提交
347
	arrangeGroups(arrangement: GroupsArrangement, target = this.activeGroup): void {
348 349 350 351
		if (this.count < 2) {
			return; // require at least 2 groups to show
		}

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

356 357 358 359 360
		switch (arrangement) {
			case GroupsArrangement.EVEN:
				this.gridWidget.distributeViewSizes();
				break;
			case GroupsArrangement.MINIMIZE_OTHERS:
B
Benjamin Pasero 已提交
361
				this.gridWidget.maximizeViewSize(target);
362 363
				break;
			case GroupsArrangement.TOGGLE:
B
Benjamin Pasero 已提交
364
				if (this.isGroupMaximized(target)) {
365 366 367 368 369 370
					this.arrangeGroups(GroupsArrangement.EVEN);
				} else {
					this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS);
				}

				break;
371
		}
372
	}
373

374 375 376 377 378 379 380 381 382
	private isGroupMaximized(targetGroup: IEditorGroupView): boolean {
		for (const group of this.groups) {
			if (group === targetGroup) {
				continue; // ignore target group
			}

			if (!group.isMinimized) {
				return false; // target cannot be maximized if one group is not minimized
			}
383
		}
384 385

		return true;
386 387
	}

388
	setGroupOrientation(orientation: GroupOrientation): void {
389 390 391 392
		if (!this.gridWidget) {
			return; // we have not been created yet
		}

393 394 395 396
		const newOrientation = (orientation === GroupOrientation.HORIZONTAL) ? Orientation.HORIZONTAL : Orientation.VERTICAL;
		if (this.gridWidget.orientation !== newOrientation) {
			this.gridWidget.orientation = newOrientation;
		}
397 398
	}

399
	applyLayout(layout: EditorGroupLayout): void {
B
Benjamin Pasero 已提交
400
		const restoreFocus = this.shouldRestoreFocus(this.container);
401 402

		// Determine how many groups we need overall
J
João Moreno 已提交
403
		let layoutGroupsCount = 0;
404 405 406 407 408
		function countGroups(groups: GroupLayoutArgument[]): void {
			groups.forEach(group => {
				if (Array.isArray(group.groups)) {
					countGroups(group.groups);
				} else {
J
João Moreno 已提交
409
					layoutGroupsCount++;
410 411 412 413 414 415
				}
			});
		}
		countGroups(layout.groups);

		// If we currently have too many groups, merge them into the last one
J
João Moreno 已提交
416 417 418 419 420
		let currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE);
		if (layoutGroupsCount < currentGroupViews.length) {
			const lastGroupInLayout = currentGroupViews[layoutGroupsCount - 1];
			currentGroupViews.forEach((group, index) => {
				if (index >= layoutGroupsCount) {
421 422 423 424
					this.mergeGroup(group, lastGroupInLayout);
				}
			});

J
João Moreno 已提交
425
			currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE);
426 427
		}

J
João Moreno 已提交
428
		const activeGroup = this.activeGroup;
429

J
João Moreno 已提交
430 431
		// Prepare grid descriptor to create new grid from
		const gridDescriptor = createSerializedGrid({
B
Benjamin Pasero 已提交
432 433
			orientation: this.toGridViewOrientation(
				layout.orientation,
B
Benjamin Pasero 已提交
434 435 436
				this.isTwoDimensionalGrid() ?
					this.gridWidget.orientation :			// preserve original orientation for 2-dimensional grids
					orthogonal(this.gridWidget.orientation) // otherwise flip (fix https://github.com/Microsoft/vscode/issues/52975)
B
Benjamin Pasero 已提交
437
			),
J
João Moreno 已提交
438 439
			groups: layout.groups
		});
440

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

J
João Moreno 已提交
444
		// Layout
J
Joao Moreno 已提交
445
		this.doLayout(this._contentDimension);
446

J
João Moreno 已提交
447 448 449
		// Update container
		this.updateContainer();

450
		// Events for groups that got added
J
João Moreno 已提交
451 452 453 454 455
		this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(groupView => {
			if (currentGroupViews.indexOf(groupView) === -1) {
				this._onDidAddGroup.fire(groupView);
			}
		});
456

B
Benjamin Pasero 已提交
457 458
		// Notify group index change given layout has changed
		this.notifyGroupIndexChange();
459

J
João Moreno 已提交
460
		// Restore focus as needed
B
Benjamin Pasero 已提交
461
		if (restoreFocus) {
462 463 464 465
			this._activeGroup.focus();
		}
	}

B
Benjamin Pasero 已提交
466 467 468 469 470
	private shouldRestoreFocus(target: Element | undefined): boolean {
		if (!target) {
			return false;
		}

B
Benjamin Pasero 已提交
471 472 473 474 475 476 477 478 479 480
		const activeElement = document.activeElement;

		if (activeElement === document.body) {
			return true; // always restore focus if nothing is focused currently
		}

		// otherwise check for the active element being an ancestor of the target
		return isAncestor(activeElement, target);
	}

B
Benjamin Pasero 已提交
481 482 483 484 485 486 487 488 489 490 491
	private isTwoDimensionalGrid(): boolean {
		const views = this.gridWidget.getViews();
		if (isGridBranchNode(views)) {
			// the grid is 2-dimensional if any children
			// of the grid is a branch node
			return views.children.some(child => isGridBranchNode(child));
		}

		return false;
	}

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

B
Benjamin Pasero 已提交
495
		const group = this.doAddGroup(locationView, direction);
496

B
Benjamin Pasero 已提交
497
		if (options?.activate) {
498 499 500 501
			this.doSetGroupActive(group);
		}

		return group;
B
Benjamin Pasero 已提交
502 503
	}

504
	private doAddGroup(locationView: IEditorGroupView, direction: GroupDirection, groupToCopy?: IEditorGroupView): IEditorGroupView {
B
Benjamin Pasero 已提交
505
		const newGroupView = this.doCreateGroupView(groupToCopy);
506 507

		// Add to grid widget
J
Joao Moreno 已提交
508
		this.gridWidget.addView(
J
Joao Moreno 已提交
509
			newGroupView,
510
			this.getSplitSizingStyle(),
B
Benjamin Pasero 已提交
511
			locationView,
512 513
			this.toGridViewDirection(direction),
		);
B
Benjamin Pasero 已提交
514

515 516 517
		// Update container
		this.updateContainer();

518 519 520
		// Event
		this._onDidAddGroup.fire(newGroupView);

B
Benjamin Pasero 已提交
521 522
		// Notify group index change given a new group was added
		this.notifyGroupIndexChange();
523

524
		return newGroupView;
525 526
	}

527 528 529 530
	private getSplitSizingStyle(): Sizing {
		return this._partOptions.splitSizing === 'split' ? Sizing.Split : Sizing.Distribute;
	}

B
Benjamin Pasero 已提交
531
	private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroup | null): IEditorGroupView {
532

533
		// Create group view
534 535
		let groupView: IEditorGroupView;
		if (from instanceof EditorGroupView) {
B
Benjamin Pasero 已提交
536
			groupView = EditorGroupView.createCopy(from, this, this.count, this.instantiationService);
537
		} else if (isSerializedEditorGroup(from)) {
B
Benjamin Pasero 已提交
538
			groupView = EditorGroupView.createFromSerialized(from, this, this.count, this.instantiationService);
539
		} else {
B
Benjamin Pasero 已提交
540
			groupView = EditorGroupView.createNew(this, this.count, this.instantiationService);
541 542
		}

543 544
		// Keep in map
		this.groupViews.set(groupView.id, groupView);
545

546
		// Track focus
547 548
		let groupDisposables = new DisposableStore();
		groupDisposables.add(groupView.onDidFocus(() => {
549 550
			this.doSetGroupActive(groupView);
		}));
551

552
		// Track editor change
553
		groupDisposables.add(groupView.onDidGroupChange(e => {
554 555 556 557 558 559 560
			switch (e.kind) {
				case GroupChangeKind.EDITOR_ACTIVE:
					this.updateContainer();
					break;
				case GroupChangeKind.GROUP_INDEX:
					this._onDidGroupIndexChange.fire(groupView);
					break;
561
			}
562
		}));
563

564
		// Track dispose
J
Joao Moreno 已提交
565
		Event.once(groupView.onWillDispose)(() => {
566
			dispose(groupDisposables);
567 568 569
			this.groupViews.delete(groupView.id);
			this.doUpdateMostRecentActive(groupView);
		});
570

571
		return groupView;
572 573
	}

574
	private doSetGroupActive(group: IEditorGroupView): void {
575 576 577 578 579
		if (this._activeGroup === group) {
			return; // return if this is already the active group
		}

		const previousActiveGroup = this._activeGroup;
580
		this._activeGroup = group;
581

582 583 584
		// Update list of most recently active groups
		this.doUpdateMostRecentActive(group, true);

585 586 587 588 589 590 591 592
		// Mark previous one as inactive
		if (previousActiveGroup) {
			previousActiveGroup.setActive(false);
		}

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

593
		// Maximize the group if it is currently minimized
B
Benjamin Pasero 已提交
594 595 596 597 598 599 600
		this.doRestoreGroup(group);

		// Event
		this._onDidActiveGroupChange.fire(group);
	}

	private doRestoreGroup(group: IEditorGroupView): void {
601
		if (this.gridWidget) {
J
Joao Moreno 已提交
602
			const viewSize = this.gridWidget.getViewSize(group);
603
			if (viewSize.width === group.minimumWidth || viewSize.height === group.minimumHeight) {
B
Benjamin Pasero 已提交
604
				this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS, group);
605 606
			}
		}
607 608
	}

609
	private doUpdateMostRecentActive(group: IEditorGroupView, makeMostRecentlyActive?: boolean): void {
610
		const index = this.mostRecentActiveGroups.indexOf(group.id);
611 612

		// Remove from MRU list
613
		if (index !== -1) {
614
			this.mostRecentActiveGroups.splice(index, 1);
615 616
		}

617
		// Add to front as needed
618
		if (makeMostRecentlyActive) {
619
			this.mostRecentActiveGroups.unshift(group.id);
620 621 622
		}
	}

B
Benjamin Pasero 已提交
623 624 625 626 627 628 629 630 631
	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;
		}
	}

B
Benjamin Pasero 已提交
632
	private toGridViewOrientation(orientation: GroupOrientation, fallback: Orientation): Orientation {
J
João Moreno 已提交
633 634 635 636 637 638 639
		if (typeof orientation === 'number') {
			return orientation === GroupOrientation.HORIZONTAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
		}

		return fallback;
	}

640
	removeGroup(group: IEditorGroupView | GroupIdentifier): void {
B
Benjamin Pasero 已提交
641
		const groupView = this.assertGroupView(group);
B
Benjamin Pasero 已提交
642 643
		if (this.groupViews.size === 1) {
			return; // Cannot remove the last root group
644
		}
645

B
Benjamin Pasero 已提交
646
		// Remove empty group
647
		if (groupView.isEmpty) {
B
Benjamin Pasero 已提交
648 649 650 651
			return this.doRemoveEmptyGroup(groupView);
		}

		// Remove group with editors
652
		this.doRemoveGroupWithEditors(groupView);
B
Benjamin Pasero 已提交
653 654
	}

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

658
		let lastActiveGroup: IEditorGroupView;
B
Benjamin Pasero 已提交
659 660 661 662 663 664 665 666
		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.
667
		this.mergeGroup(groupView, lastActiveGroup);
B
Benjamin Pasero 已提交
668 669
	}

670
	private doRemoveEmptyGroup(groupView: IEditorGroupView): void {
B
Benjamin Pasero 已提交
671
		const restoreFocus = this.shouldRestoreFocus(this.container);
672

673 674
		// Activate next group if the removed one was active
		if (this._activeGroup === groupView) {
675 676 677
			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);
678
		}
679

680
		// Remove from grid widget & dispose
681
		this.gridWidget.removeView(groupView, this.getSplitSizingStyle());
682
		groupView.dispose();
683

684 685
		// 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)
B
Benjamin Pasero 已提交
686
		if (restoreFocus) {
B
Benjamin Pasero 已提交
687
			this._activeGroup.focus();
688 689
		}

B
Benjamin Pasero 已提交
690 691
		// Notify group index change given a group was removed
		this.notifyGroupIndexChange();
692

693 694
		// Update container
		this.updateContainer();
695

696
		// Event
697 698
		this._onDidRemoveGroup.fire(groupView);
	}
699

700
	moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
701 702
		const sourceView = this.assertGroupView(group);
		const targetView = this.assertGroupView(location);
703

704 705
		if (sourceView.id === targetView.id) {
			throw new Error('Cannot move group into its own');
B
Benjamin Pasero 已提交
706 707
		}

B
Benjamin Pasero 已提交
708
		const restoreFocus = this.shouldRestoreFocus(sourceView.element);
709

710
		// Move through grid widget API
711
		this.gridWidget.moveView(sourceView, this.getSplitSizingStyle(), targetView, this.toGridViewDirection(direction));
B
Benjamin Pasero 已提交
712 713 714

		// 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)
B
Benjamin Pasero 已提交
715
		if (restoreFocus) {
B
Benjamin Pasero 已提交
716
			sourceView.focus();
B
Benjamin Pasero 已提交
717 718 719
		}

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

B
Benjamin Pasero 已提交
722 723 724
		// Notify group index change given a group was moved
		this.notifyGroupIndexChange();

725
		return sourceView;
B
Benjamin Pasero 已提交
726 727
	}

728
	copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
B
Benjamin Pasero 已提交
729 730 731
		const groupView = this.assertGroupView(group);
		const locationView = this.assertGroupView(location);

B
Benjamin Pasero 已提交
732
		const restoreFocus = this.shouldRestoreFocus(groupView.element);
733 734 735 736 737

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

		// Restore focus if we had it
B
Benjamin Pasero 已提交
738
		if (restoreFocus) {
B
Benjamin Pasero 已提交
739
			copiedGroupView.focus();
740 741 742
		}

		return copiedGroupView;
B
Benjamin Pasero 已提交
743 744
	}

745
	mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroupView {
B
Benjamin Pasero 已提交
746 747 748
		const sourceView = this.assertGroupView(group);
		const targetView = this.assertGroupView(target);

B
Benjamin Pasero 已提交
749
		// Move/Copy editors over into target
B
Benjamin Pasero 已提交
750
		let index = (options && typeof options.index === 'number') ? options.index : targetView.count;
B
Benjamin Pasero 已提交
751
		sourceView.editors.forEach(editor => {
752
			const inactive = !sourceView.isActive(editor) || this._activeGroup !== sourceView;
753 754
			const copyOptions: ICopyEditorOptions = { index, inactive, preserveFocus: inactive };

B
Benjamin Pasero 已提交
755
			if (options?.mode === MergeGroupMode.COPY_EDITORS) {
756 757 758 759
				sourceView.copyEditor(editor, targetView, copyOptions);
			} else {
				sourceView.moveEditor(editor, targetView, copyOptions);
			}
B
Benjamin Pasero 已提交
760 761 762 763

			index++;
		});

764
		// Remove source if the view is now empty and not already removed
765
		if (sourceView.isEmpty && !sourceView.disposed /* could have been disposed already via workbench.editor.closeEmptyGroups setting */) {
766 767 768 769
			this.removeGroup(sourceView);
		}

		return targetView;
B
Benjamin Pasero 已提交
770 771
	}

772
	private assertGroupView(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
B
Benjamin Pasero 已提交
773
		let groupView: IEditorGroupView | undefined;
774
		if (typeof group === 'number') {
B
Benjamin Pasero 已提交
775 776 777
			groupView = this.getGroup(group);
		} else {
			groupView = group;
B
Benjamin Pasero 已提交
778 779
		}

B
Benjamin Pasero 已提交
780
		if (!groupView) {
B
Benjamin Pasero 已提交
781
			throw new Error('Invalid editor group provided!');
782 783
		}

B
Benjamin Pasero 已提交
784
		return groupView;
785 786
	}

787 788
	//#endregion

789
	//#region Part
790

791 792
	// TODO @sbatten @joao find something better to prevent editor taking over #79897
	get minimumWidth(): number { return Math.min(this.centeredLayoutWidget.minimumWidth, this.layoutService.getMaximumEditorDimensions().width); }
I
isidor 已提交
793
	get maximumWidth(): number { return this.centeredLayoutWidget.maximumWidth; }
794
	get minimumHeight(): number { return Math.min(this.centeredLayoutWidget.minimumHeight, this.layoutService.getMaximumEditorDimensions().height); }
I
isidor 已提交
795
	get maximumHeight(): number { return this.centeredLayoutWidget.maximumHeight; }
B
Benjamin Pasero 已提交
796

S
SteVen Batten 已提交
797 798
	readonly snap = true;

S
SteVen Batten 已提交
799
	get onDidChange(): Event<IViewSize | undefined> { return Event.any(this.centeredLayoutWidget.onDidChange, this.onDidSetGridWidget.event); }
J
Joao Moreno 已提交
800 801
	readonly priority: LayoutPriority = LayoutPriority.High;

J
João Moreno 已提交
802
	private get gridSeparatorBorder(): Color {
803 804 805
		return this.theme.getColor(EDITOR_GROUP_BORDER) || this.theme.getColor(contrastBorder) || Color.transparent;
	}

806
	updateStyles(): void {
B
Benjamin Pasero 已提交
807 808
		const container = assertIsDefined(this.container);
		container.style.backgroundColor = this.getColor(editorBackground) || '';
809

810
		const separatorBorderStyle = { separatorBorder: this.gridSeparatorBorder, background: this.theme.getColor(EDITOR_PANE_BACKGROUND) || Color.transparent };
811
		this.gridWidget.style(separatorBorderStyle);
812
		this.centeredLayoutWidget.styles(separatorBorderStyle);
813 814
	}

815
	createContentArea(parent: HTMLElement, options?: IEditorPartCreationOptions): HTMLElement {
816

817
		// Container
818
		this.element = parent;
819 820 821
		this.container = document.createElement('div');
		addClass(this.container, 'content');
		parent.appendChild(this.container);
822

823
		// Grid control with center layout
824
		this.doCreateGridControl(options);
J
Joao Moreno 已提交
825 826

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

B
Benjamin Pasero 已提交
828
		// Drop support
829
		this._register(this.createEditorDropTarget(this.container, {}));
830

831
		return this.container;
832 833
	}

I
isidor 已提交
834
	centerLayout(active: boolean): void {
835
		this.centeredLayoutWidget.activate(active);
B
Benjamin Pasero 已提交
836

837
		this._activeGroup.focus();
I
isidor 已提交
838 839 840
	}

	isLayoutCentered(): boolean {
841
		return this.centeredLayoutWidget.isActive();
I
isidor 已提交
842 843
	}

844
	private doCreateGridControl(options?: IEditorPartCreationOptions): void {
845

846
		// Grid Widget (with previous UI state)
B
Benjamin Pasero 已提交
847
		let restoreError = false;
848
		if (!options || options.restorePreviousState) {
B
Benjamin Pasero 已提交
849
			restoreError = !this.doCreateGridControlWithPreviousState();
850 851 852
		}

		// Grid Widget (no previous UI state or failed to restore)
B
Benjamin Pasero 已提交
853
		if (!this.gridWidget || restoreError) {
854
			const initialGroup = this.doCreateGroupView();
B
Benjamin Pasero 已提交
855
			this.doSetGridWidget(new SerializableGrid(initialGroup));
856 857 858 859 860 861

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

		// Signal restored
B
Benjamin Pasero 已提交
862 863 864 865 866
		Promise.all(this.groups.map(group => group.whenRestored)).finally(() => {
			if (this.whenRestoredResolve) {
				this.whenRestoredResolve();
			}
		});
867 868 869

		// Update container
		this.updateContainer();
B
Benjamin Pasero 已提交
870 871 872

		// Notify group index change we created the entire grid
		this.notifyGroupIndexChange();
873 874
	}

B
Benjamin Pasero 已提交
875
	private doCreateGridControlWithPreviousState(): boolean {
876
		const uiState: IEditorPartUIState = this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY];
B
Benjamin Pasero 已提交
877
		if (uiState?.serializedGrid) {
878
			try {
879

880 881
				// MRU
				this.mostRecentActiveGroups = uiState.mostRecentActiveGroups;
882

883 884
				// Grid Widget
				this.doCreateGridControlWithState(uiState.serializedGrid, uiState.activeGroup);
885

886 887 888
				// Ensure last active group has focus
				this._activeGroup.focus();
			} catch (error) {
889

B
Benjamin Pasero 已提交
890 891
				// Log error
				onUnexpectedError(new Error(`Error restoring editor grid widget: ${error} (with state: ${JSON.stringify(uiState)})`));
892

B
Benjamin Pasero 已提交
893 894 895 896
				// Clear any state we have from the failing restore
				this.groupViews.forEach(group => group.dispose());
				this.groupViews.clear();
				this.mostRecentActiveGroups = [];
897

B
Benjamin Pasero 已提交
898 899
				return false; // failure
			}
900 901
		}

B
Benjamin Pasero 已提交
902
		return true; // success
903 904
	}

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

J
João Moreno 已提交
907 908 909 910 911 912 913 914 915
		// 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
B
Benjamin Pasero 已提交
916
		const groupViews: IEditorGroupView[] = [];
J
Joao Moreno 已提交
917
		const gridWidget = SerializableGrid.deserialize(serializedGrid, {
J
Joao Moreno 已提交
918
			fromJSON: (serializedEditorGroup: ISerializedEditorGroup | null) => {
J
João Moreno 已提交
919 920
				let groupView: IEditorGroupView;
				if (reuseGroupViews.length > 0) {
B
Benjamin Pasero 已提交
921
					groupView = reuseGroupViews.shift()!;
J
João Moreno 已提交
922 923 924 925
				} else {
					groupView = this.doCreateGroupView(serializedEditorGroup);
				}

B
Benjamin Pasero 已提交
926 927
				groupViews.push(groupView);

J
João Moreno 已提交
928 929 930 931 932 933 934
				if (groupView.id === activeGroupId) {
					this.doSetGroupActive(groupView);
				}

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

B
Benjamin Pasero 已提交
936 937 938 939 940 941 942 943 944 945 946 947
		// If the active group was not found when restoring the grid
		// make sure to make at least one group active. We always need
		// an active group.
		if (!this._activeGroup) {
			this.doSetGroupActive(groupViews[0]);
		}

		// Validate MRU group views matches grid widget state
		if (this.mostRecentActiveGroups.some(groupId => !this.getGroup(groupId))) {
			this.mostRecentActiveGroups = groupViews.map(group => group.id);
		}

J
Joao Moreno 已提交
948 949
		// Set it
		this.doSetGridWidget(gridWidget);
J
João Moreno 已提交
950 951
	}

B
Benjamin Pasero 已提交
952
	private doSetGridWidget(gridWidget: SerializableGrid<IEditorGroupView>): void {
J
Joao Moreno 已提交
953 954 955 956
		if (this.gridWidget) {
			this.gridWidget.dispose();
		}

B
Benjamin Pasero 已提交
957
		this.gridWidget = gridWidget;
J
Joao Moreno 已提交
958
		this.gridWidgetView.gridWidget = gridWidget;
B
Benjamin Pasero 已提交
959

B
Benjamin Pasero 已提交
960
		this._onDidSizeConstraintsChange.input = gridWidget.onDidChange;
B
Benjamin Pasero 已提交
961

962
		this.onDidSetGridWidget.fire(undefined);
J
João Moreno 已提交
963 964
	}

965
	private updateContainer(): void {
B
Benjamin Pasero 已提交
966 967
		const container = assertIsDefined(this.container);
		toggleClass(container, 'empty', this.isEmpty);
B
Benjamin Pasero 已提交
968 969
	}

B
Benjamin Pasero 已提交
970 971
	private notifyGroupIndexChange(): void {
		this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach((group, index) => group.notifyIndexChanged(index));
972 973
	}

974
	private get isEmpty(): boolean {
975
		return this.groupViews.size === 1 && this._activeGroup.isEmpty;
976 977
	}

B
Benjamin Pasero 已提交
978
	layout(width: number, height: number): void {
979

B
Benjamin Pasero 已提交
980 981
		// Layout contents
		const contentAreaSize = super.layoutContents(width, height).contentSize;
J
João Moreno 已提交
982

B
Benjamin Pasero 已提交
983 984
		// Layout editor container
		this.doLayout(contentAreaSize);
J
João Moreno 已提交
985 986 987
	}

	private doLayout(dimension: Dimension): void {
J
Joao Moreno 已提交
988
		this._contentDimension = dimension;
989

990
		// Layout Grid
J
Joao Moreno 已提交
991
		this.centeredLayoutWidget.layout(this._contentDimension.width, this._contentDimension.height);
992

993
		// Event
994
		this._onDidLayout.fire(dimension);
995 996
	}

B
Benjamin Pasero 已提交
997
	protected saveState(): void {
998

999
		// Persist grid UI state
1000
		if (this.gridWidget) {
1001
			const uiState: IEditorPartUIState = {
1002 1003 1004 1005 1006
				serializedGrid: this.gridWidget.serialize(),
				activeGroup: this._activeGroup.id,
				mostRecentActiveGroups: this.mostRecentActiveGroups
			};

1007
			if (this.isEmpty) {
B
Benjamin Pasero 已提交
1008
				delete this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY];
1009
			} else {
B
Benjamin Pasero 已提交
1010
				this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] = uiState;
1011
			}
1012
		}
1013 1014

		// Persist centered view state
B
Benjamin Pasero 已提交
1015 1016 1017 1018 1019 1020
		const centeredLayoutState = this.centeredLayoutWidget.state;
		if (this.centeredLayoutWidget.isDefault(centeredLayoutState)) {
			delete this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY];
		} else {
			this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY] = centeredLayoutState;
		}
1021

B
Benjamin Pasero 已提交
1022
		super.saveState();
1023 1024 1025 1026 1027
	}

	dispose(): void {

		// Forward to all groups
1028 1029
		this.groupViews.forEach(group => group.dispose());
		this.groupViews.clear();
1030

J
João Moreno 已提交
1031 1032
		// Grid widget
		if (this.gridWidget) {
J
Joao Moreno 已提交
1033
			this.gridWidget.dispose();
J
João Moreno 已提交
1034 1035
		}

1036 1037 1038
		super.dispose();
	}

1039
	//#endregion
1040 1041 1042 1043 1044 1045

	toJSON(): object {
		return {
			type: Parts.EDITOR_PART
		};
	}
B
Benjamin Pasero 已提交
1046 1047 1048 1049 1050 1051 1052 1053

	//#region TODO@matt this should move into some kind of service

	createEditorDropTarget(container: HTMLElement, delegate: EditorDropTargetDelegate): IDisposable {
		return this.instantiationService.createInstance(EditorDropTarget, this, container, delegate);
	}

	//#endregion
J
João Moreno 已提交
1054
}
1055

J
Joao Moreno 已提交
1056
registerSingleton(IEditorGroupsService, EditorPart);