editorStacksModel.ts 31.3 KB
Newer Older
B
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 Event, {Emitter} from 'vs/base/common/event';
B
Benjamin Pasero 已提交
9
import {IEditorRegistry, Extensions, EditorInput, getUntitledOrFileResource, IEditorStacksModel, IEditorGroup, IEditorIdentifier, IGroupEvent, GroupIdentifier, IStacksModelChangeEvent, IWorkbenchEditorConfiguration, EditorOpenPositioning} from 'vs/workbench/common/editor';
10
import URI from 'vs/base/common/uri';
11 12
import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
13
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
14 15 16
import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle';
import {dispose, IDisposable} from 'vs/base/common/lifecycle';
import {Registry} from 'vs/platform/platform';
17 18
import {Position, Direction} from 'vs/platform/editor/common/editor';
import {DiffEditorInput} from 'vs/workbench/common/editor/diffEditorInput';
B
Benjamin Pasero 已提交
19

20 21 22
// TODO@Ben currently only files and untitled editors are tracked with their resources in the stacks model
// Once the resource is a base concept of all editor inputs, every resource should be tracked for any editor

23
export interface GroupEvent extends IGroupEvent {
24 25 26
	editor: EditorInput;
}

27 28
export interface EditorIdentifier extends IEditorIdentifier {
	group: EditorGroup;
29
	editor: EditorInput;
30 31
}

B
Benjamin Pasero 已提交
32 33 34
export interface IEditorOpenOptions {
	pinned?: boolean;
	active?: boolean;
B
Benjamin Pasero 已提交
35
	index?: number;
B
Benjamin Pasero 已提交
36 37
}

38
export interface ISerializedEditorInput {
39 40 41 42
	id: string;
	value: string;
}

43
export interface ISerializedEditorGroup {
44 45 46 47 48 49
	label: string;
	editors: ISerializedEditorInput[];
	mru: number[];
	preview: number;
}

B
Benjamin Pasero 已提交
50
export class EditorGroup implements IEditorGroup {
51 52 53 54

	private static IDS = 0;

	private _id: GroupIdentifier;
55 56
	private _label: string;

B
Benjamin Pasero 已提交
57 58
	private editors: EditorInput[];
	private mru: EditorInput[];
59
	private mapResourceToEditorCount: { [resource: string]: number };
B
Benjamin Pasero 已提交
60 61 62 63

	private preview: EditorInput; // editor in preview state
	private active: EditorInput;  // editor in active state

B
Benjamin Pasero 已提交
64
	private toDispose: IDisposable[];
65
	private editorOpenPositioning: string;
B
Benjamin Pasero 已提交
66

B
Benjamin Pasero 已提交
67 68
	private _onEditorActivated: Emitter<EditorInput>;
	private _onEditorOpened: Emitter<EditorInput>;
69
	private _onEditorClosed: Emitter<GroupEvent>;
70
	private _onEditorDisposed: Emitter<EditorInput>;
71
	private _onEditorDirty: Emitter<EditorInput>;
B
Benjamin Pasero 已提交
72
	private _onEditorMoved: Emitter<EditorInput>;
B
Benjamin Pasero 已提交
73 74
	private _onEditorPinned: Emitter<EditorInput>;
	private _onEditorUnpinned: Emitter<EditorInput>;
75 76
	private _onEditorStateChanged: Emitter<EditorInput>;
	private _onEditorsStructureChanged: Emitter<EditorInput>;
B
Benjamin Pasero 已提交
77

78 79
	constructor(
		arg1: string | ISerializedEditorGroup,
80 81
		@IInstantiationService private instantiationService: IInstantiationService,
		@IConfigurationService private configurationService: IConfigurationService
82
	) {
83 84
		this._id = EditorGroup.IDS++;

B
Benjamin Pasero 已提交
85
		this.editors = [];
B
Benjamin Pasero 已提交
86
		this.mru = [];
B
Benjamin Pasero 已提交
87
		this.toDispose = [];
88
		this.mapResourceToEditorCount = Object.create(null);
B
Benjamin Pasero 已提交
89
		this.onConfigurationUpdated(configurationService.getConfiguration<IWorkbenchEditorConfiguration>());
B
Benjamin Pasero 已提交
90 91 92

		this._onEditorActivated = new Emitter<EditorInput>();
		this._onEditorOpened = new Emitter<EditorInput>();
93
		this._onEditorClosed = new Emitter<GroupEvent>();
94
		this._onEditorDisposed = new Emitter<EditorInput>();
95
		this._onEditorDirty = new Emitter<EditorInput>();
B
Benjamin Pasero 已提交
96
		this._onEditorMoved = new Emitter<EditorInput>();
B
Benjamin Pasero 已提交
97 98
		this._onEditorPinned = new Emitter<EditorInput>();
		this._onEditorUnpinned = new Emitter<EditorInput>();
99 100
		this._onEditorStateChanged = new Emitter<EditorInput>();
		this._onEditorsStructureChanged = new Emitter<EditorInput>();
B
Benjamin Pasero 已提交
101

102
		this.toDispose.push(this._onEditorActivated, this._onEditorOpened, this._onEditorClosed, this._onEditorDisposed, this._onEditorDirty, this._onEditorMoved, this._onEditorPinned, this._onEditorUnpinned, this._onEditorStateChanged, this._onEditorsStructureChanged);
103 104 105 106 107 108

		if (typeof arg1 === 'object') {
			this.deserialize(arg1);
		} else {
			this._label = arg1;
		}
109 110 111 112 113 114 115 116 117

		this.registerListeners();
	}

	private registerListeners(): void {
		this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(e.config)));
	}

	private onConfigurationUpdated(config: IWorkbenchEditorConfiguration): void {
118
		this.editorOpenPositioning = config.workbench.editor.openPositioning;
B
Benjamin Pasero 已提交
119 120
	}

121 122 123 124
	public get id(): GroupIdentifier {
		return this._id;
	}

125 126 127 128
	public get label(): string {
		return this._label;
	}

129 130 131 132
	public set label(label: string) {
		this._label = label;
	}

133 134 135 136
	public get count(): number {
		return this.editors.length;
	}

B
Benjamin Pasero 已提交
137 138 139 140 141 142 143 144
	public get onEditorActivated(): Event<EditorInput> {
		return this._onEditorActivated.event;
	}

	public get onEditorOpened(): Event<EditorInput> {
		return this._onEditorOpened.event;
	}

145
	public get onEditorClosed(): Event<GroupEvent> {
B
Benjamin Pasero 已提交
146 147 148
		return this._onEditorClosed.event;
	}

149 150 151 152
	public get onEditorDisposed(): Event<EditorInput> {
		return this._onEditorDisposed.event;
	}

153 154 155 156
	public get onEditorDirty(): Event<EditorInput> {
		return this._onEditorDirty.event;
	}

B
Benjamin Pasero 已提交
157 158 159 160
	public get onEditorMoved(): Event<EditorInput> {
		return this._onEditorMoved.event;
	}

B
Benjamin Pasero 已提交
161 162 163 164 165 166 167 168
	public get onEditorPinned(): Event<EditorInput> {
		return this._onEditorPinned.event;
	}

	public get onEditorUnpinned(): Event<EditorInput> {
		return this._onEditorUnpinned.event;
	}

169 170 171 172 173 174
	public get onEditorStateChanged(): Event<EditorInput> {
		return this._onEditorStateChanged.event;
	}

	public get onEditorsStructureChanged(): Event<EditorInput> {
		return this._onEditorsStructureChanged.event;
175 176
	}

B
Benjamin Pasero 已提交
177 178
	public getEditors(mru?: boolean): EditorInput[] {
		return mru ? this.mru.slice(0) : this.editors.slice(0);
B
Benjamin Pasero 已提交
179 180
	}

181 182 183 184
	public getEditor(index: number): EditorInput {
		return this.editors[index];
	}

B
Benjamin Pasero 已提交
185 186 187 188 189
	public get activeEditor(): EditorInput {
		return this.active;
	}

	public isActive(editor: EditorInput): boolean {
B
Benjamin Pasero 已提交
190
		return this.matches(this.active, editor);
B
Benjamin Pasero 已提交
191 192 193 194 195 196 197
	}

	public get previewEditor(): EditorInput {
		return this.preview;
	}

	public isPreview(editor: EditorInput): boolean {
B
Benjamin Pasero 已提交
198
		return this.matches(this.preview, editor);
B
Benjamin Pasero 已提交
199 200 201 202 203 204
	}

	public openEditor(editor: EditorInput, options?: IEditorOpenOptions): void {
		const index = this.indexOf(editor);

		const makePinned = options && options.pinned;
B
Benjamin Pasero 已提交
205
		const makeActive = (options && options.active) || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor));
B
Benjamin Pasero 已提交
206 207 208

		// New editor
		if (index === -1) {
209 210
			let targetIndex: number;
			const indexOfActive = this.indexOf(this.active);
B
Benjamin Pasero 已提交
211

212 213 214 215
			// Insert into specific position
			if (options && typeof options.index === 'number') {
				targetIndex = options.index;
			}
B
Benjamin Pasero 已提交
216

217
			// Insert to the BEGINNING
218
			else if (this.editorOpenPositioning === EditorOpenPositioning.FIRST) {
219 220 221 222
				targetIndex = 0;
			}

			// Insert to the END
223
			else if (this.editorOpenPositioning === EditorOpenPositioning.LAST) {
224
				targetIndex = this.editors.length;
225
			}
B
Benjamin Pasero 已提交
226

227
			// Insert to the LEFT of active editor
228
			else if (this.editorOpenPositioning === EditorOpenPositioning.LEFT) {
229 230 231
				if (indexOfActive === 0 || !this.editors.length) {
					targetIndex = 0; // to the left becoming first editor in list
				} else {
232
					targetIndex = indexOfActive; // to the left of active editor
B
Benjamin Pasero 已提交
233
				}
234
			}
B
Benjamin Pasero 已提交
235

236 237 238 239 240
			// Insert to the RIGHT of active editor
			else {
				targetIndex = indexOfActive + 1;
			}

241 242 243
			// Insert into our list of editors if pinned or we have no preview editor
			if (makePinned || !this.preview) {
				this.splice(targetIndex, false, editor);
B
Benjamin Pasero 已提交
244 245
			}

246 247
			// Handle preview
			if (!makePinned) {
B
Benjamin Pasero 已提交
248 249

				// Replace existing preview with this editor if we have a preview
250 251
				if (this.preview) {
					const indexOfPreview = this.indexOf(this.preview);
252 253
					if (targetIndex > indexOfPreview) {
						targetIndex--; // accomodate for the fact that the preview editor closes
254 255
					}

B
Benjamin Pasero 已提交
256
					this.closeEditor(this.preview, !makeActive); // optimization to prevent multiple setActive() in one call
257
					this.splice(targetIndex, false, editor);
258
				}
B
Benjamin Pasero 已提交
259

B
Benjamin Pasero 已提交
260 261 262
				this.preview = editor;
			}

263 264
			// Listeners
			this.hookEditorListeners(editor);
265

B
Benjamin Pasero 已提交
266
			// Event
267
			this.fireEvent(this._onEditorOpened, editor, true);
B
Benjamin Pasero 已提交
268

269
			// Handle active
B
Benjamin Pasero 已提交
270
			if (makeActive) {
B
Benjamin Pasero 已提交
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
				this.setActive(editor);
			}
		}

		// Existing editor
		else {

			// Pin it
			if (makePinned) {
				this.pin(editor);
			}

			// Activate it
			if (makeActive) {
				this.setActive(editor);
			}
B
Benjamin Pasero 已提交
287 288 289 290 291

			// Respect index
			if (options && typeof options.index === 'number') {
				this.moveEditor(editor, options.index);
			}
B
Benjamin Pasero 已提交
292 293 294
		}
	}

295
	private hookEditorListeners(editor: EditorInput): void {
296
		const unbind: IDisposable[] = [];
297 298

		// Re-emit disposal of editor input as our own event
299
		unbind.push(editor.addOneTimeDisposableListener('dispose', () => {
300 301 302
			if (this.indexOf(editor) >= 0) {
				this._onEditorDisposed.fire(editor);
			}
303 304 305 306
		}));

		// Re-Emit dirty state changes
		unbind.push(editor.onDidChangeDirty(() => {
307
			this.fireEvent(this._onEditorDirty, editor, false);
308
		}));
309 310

		// Clean up dispose listeners once the editor gets closed
311
		unbind.push(this.onEditorClosed(event => {
312
			if (event.editor.matches(editor)) {
313
				dispose(unbind);
314
			}
315
		}));
316 317
	}

B
Benjamin Pasero 已提交
318
	public closeEditor(editor: EditorInput, openNext = true): void {
B
Benjamin Pasero 已提交
319 320 321 322 323 324
		const index = this.indexOf(editor);
		if (index === -1) {
			return; // not found
		}

		// Active Editor closed
B
Benjamin Pasero 已提交
325
		if (openNext && this.matches(this.active, editor)) {
B
Benjamin Pasero 已提交
326 327

			// More than one editor
B
Benjamin Pasero 已提交
328 329
			if (this.mru.length > 1) {
				this.setActive(this.mru[1]); // active editor is always first in MRU, so pick second editor after as new active
B
Benjamin Pasero 已提交
330 331 332 333 334 335 336 337 338
			}

			// One Editor
			else {
				this.active = null;
			}
		}

		// Preview Editor closed
339
		let pinned = true;
B
Benjamin Pasero 已提交
340
		if (this.matches(this.preview, editor)) {
B
Benjamin Pasero 已提交
341
			this.preview = null;
342
			pinned = false;
B
Benjamin Pasero 已提交
343 344
		}

B
Benjamin Pasero 已提交
345
		// Remove from arrays
B
Benjamin Pasero 已提交
346
		this.splice(index, true);
B
Benjamin Pasero 已提交
347 348

		// Event
349
		this.fireEvent(this._onEditorClosed, { editor, pinned, index }, true);
B
Benjamin Pasero 已提交
350 351
	}

B
Benjamin Pasero 已提交
352
	public closeEditors(except: EditorInput, direction?: Direction): void {
B
Benjamin Pasero 已提交
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
		const index = this.indexOf(except);
		if (index === -1) {
			return; // not found
		}

		// Close to the left
		if (direction === Direction.LEFT) {
			for (let i = index - 1; i >= 0; i--) {
				this.closeEditor(this.editors[i]);
			}
		}

		// Close to the right
		else if (direction === Direction.RIGHT) {
			for (let i = this.editors.length - 1; i > index; i--) {
				this.closeEditor(this.editors[i]);
			}
		}

		// Both directions
		else {
B
Benjamin Pasero 已提交
374
			this.mru.filter(e => !this.matches(e, except)).forEach(e => this.closeEditor(e));
B
Benjamin Pasero 已提交
375 376 377
		}
	}

378 379 380
	public closeAllEditors(): void {

		// Optimize: close all non active editors first to produce less upstream work
B
Benjamin Pasero 已提交
381
		this.mru.filter(e => !this.matches(e, this.active)).forEach(e => this.closeEditor(e));
382 383 384
		this.closeEditor(this.active);
	}

B
Benjamin Pasero 已提交
385 386 387 388 389 390 391 392 393 394 395
	public moveEditor(editor: EditorInput, toIndex: number): void {
		const index = this.indexOf(editor);
		if (index < 0) {
			return;
		}

		// Move
		this.editors.splice(index, 1);
		this.editors.splice(toIndex, 0, editor);

		// Event
396
		this.fireEvent(this._onEditorMoved, editor, true);
B
Benjamin Pasero 已提交
397 398
	}

B
Benjamin Pasero 已提交
399 400 401 402 403 404
	public setActive(editor: EditorInput): void {
		const index = this.indexOf(editor);
		if (index === -1) {
			return; // not found
		}

B
Benjamin Pasero 已提交
405
		if (this.matches(this.active, editor)) {
B
Benjamin Pasero 已提交
406 407 408 409 410
			return; // already active
		}

		this.active = editor;

B
Benjamin Pasero 已提交
411
		// Bring to front in MRU list
B
Benjamin Pasero 已提交
412 413
		this.setMostRecentlyUsed(editor);

B
Benjamin Pasero 已提交
414
		// Event
415
		this.fireEvent(this._onEditorActivated, editor, false);
B
Benjamin Pasero 已提交
416 417 418
	}

	public pin(editor: EditorInput): void {
419 420 421 422 423
		const index = this.indexOf(editor);
		if (index === -1) {
			return; // not found
		}

B
Benjamin Pasero 已提交
424 425
		if (!this.isPreview(editor)) {
			return; // can only pin a preview editor
B
Benjamin Pasero 已提交
426 427 428 429 430 431
		}

		// Convert the preview editor to be a pinned editor
		this.preview = null;

		// Event
432
		this.fireEvent(this._onEditorPinned, editor, false);
B
Benjamin Pasero 已提交
433 434
	}

B
Benjamin Pasero 已提交
435
	public unpin(editor: EditorInput): void {
436 437 438 439
		const index = this.indexOf(editor);
		if (index === -1) {
			return; // not found
		}
440

B
Benjamin Pasero 已提交
441 442 443 444 445 446 447 448 449
		if (!this.isPinned(editor)) {
			return; // can only unpin a pinned editor
		}

		// Set new
		const oldPreview = this.preview;
		this.preview = editor;

		// Event
450
		this.fireEvent(this._onEditorUnpinned, editor, false);
B
Benjamin Pasero 已提交
451 452 453 454 455

		// Close old preview editor if any
		this.closeEditor(oldPreview);
	}

B
Benjamin Pasero 已提交
456 457 458 459 460 461 462 463 464 465
	public isPinned(editor: EditorInput): boolean {
		const index = this.indexOf(editor);
		if (index === -1) {
			return false; // editor not found
		}

		if (!this.preview) {
			return true; // no preview editor
		}

B
Benjamin Pasero 已提交
466
		return !this.matches(this.preview, editor);
B
Benjamin Pasero 已提交
467 468
	}

469
	private fireEvent(emitter: Emitter<EditorInput | GroupEvent>, arg2: EditorInput | GroupEvent, isStructuralChange: boolean): void {
470
		emitter.fire(arg2);
471 472 473 474 475 476

		if (isStructuralChange) {
			this._onEditorsStructureChanged.fire(arg2 instanceof EditorInput ? arg2 : arg2.editor);
		} else {
			this._onEditorStateChanged.fire(arg2 instanceof EditorInput ? arg2 : arg2.editor);
		}
477 478
	}

B
Benjamin Pasero 已提交
479
	private splice(index: number, del: boolean, editor?: EditorInput): void {
480
		const editorToDeleteOrReplace = this.editors[index];
B
Benjamin Pasero 已提交
481

B
Benjamin Pasero 已提交
482
		const args: any[] = [index, del ? 1 : 0];
483 484 485
		if (editor) {
			args.push(editor);
		}
B
Benjamin Pasero 已提交
486

487
		// Perform on editors array
488
		this.editors.splice.apply(this.editors, args);
B
Benjamin Pasero 已提交
489

490
		// Add
B
Benjamin Pasero 已提交
491
		if (!del && editor) {
492 493
			this.mru.push(editor); // make it LRU editor
			this.updateResourceMap(editor, false /* add */); // add new to resource map
B
Benjamin Pasero 已提交
494 495
		}

B
Benjamin Pasero 已提交
496
		// Remove / Replace
B
Benjamin Pasero 已提交
497
		else {
B
Benjamin Pasero 已提交
498 499
			const indexInMRU = this.indexOf(editorToDeleteOrReplace, this.mru);

500
			// Remove
B
Benjamin Pasero 已提交
501
			if (del && !editor) {
502 503
				this.mru.splice(indexInMRU, 1); // remove from MRU
				this.updateResourceMap(editorToDeleteOrReplace, true /* delete */); // remove from resource map
B
Benjamin Pasero 已提交
504 505
			}

506
			// Replace
B
Benjamin Pasero 已提交
507
			else {
508 509 510
				this.mru.splice(indexInMRU, 1, editor); // replace MRU at location
				this.updateResourceMap(editor, false /* add */); // add new to resource map
				this.updateResourceMap(editorToDeleteOrReplace, true /* delete */); // remove replaced from resource map
B
Benjamin Pasero 已提交
511
			}
B
Benjamin Pasero 已提交
512 513 514
		}
	}

515
	private updateResourceMap(editor: EditorInput, remove: boolean): void {
516
		const resource = getUntitledOrFileResource(editor, true /* include diff editors */);
517
		if (resource) {
518 519 520 521 522 523 524 525 526 527 528 529 530 531

			// It is possible to have the same resource opened twice (once as normal input and once as diff input)
			// So we need to do ref counting on the resource to provide the correct picture
			let counter = this.mapResourceToEditorCount[resource.toString()] || 0;
			let newCounter;
			if (remove) {
				if (counter > 1) {
					newCounter = counter - 1;
				}
			} else {
				newCounter = counter + 1;
			}

			this.mapResourceToEditorCount[resource.toString()] = newCounter;
532 533 534
		}
	}

535
	public indexOf(candidate: EditorInput, editors = this.editors): number {
B
Benjamin Pasero 已提交
536 537
		if (!candidate) {
			return -1;
B
Benjamin Pasero 已提交
538 539
		}

B
Benjamin Pasero 已提交
540
		for (let i = 0; i < editors.length; i++) {
B
Benjamin Pasero 已提交
541
			if (this.matches(editors[i], candidate)) {
B
Benjamin Pasero 已提交
542 543 544 545 546 547
				return i;
			}
		}

		return -1;
	}
B
Benjamin Pasero 已提交
548

549 550 551 552 553 554 555
	public contains(candidate: EditorInput): boolean;
	public contains(resource: URI): boolean;
	public contains(arg1: any): boolean {
		if (arg1 instanceof EditorInput) {
			return this.indexOf(arg1) >= 0;
		}

556 557 558
		const counter = this.mapResourceToEditorCount[(<URI>arg1).toString()];

		return typeof counter === 'number' && counter > 0;
559 560
	}

B
Benjamin Pasero 已提交
561 562 563 564 565 566 567 568 569
	private setMostRecentlyUsed(editor: EditorInput): void {
		const index = this.indexOf(editor);
		if (index === -1) {
			return; // editor not found
		}

		const mruIndex = this.indexOf(editor, this.mru);

		// Remove old index
B
Benjamin Pasero 已提交
570
		this.mru.splice(mruIndex, 1);
B
Benjamin Pasero 已提交
571

B
Benjamin Pasero 已提交
572
		// Set editor to front
B
Benjamin Pasero 已提交
573 574
		this.mru.unshift(editor);
	}
B
Benjamin Pasero 已提交
575 576 577 578

	private matches(editorA: EditorInput, editorB: EditorInput): boolean {
		return !!editorA && !!editorB && editorA.matches(editorB);
	}
579 580

	public serialize(): ISerializedEditorGroup {
581
		const registry = Registry.as<IEditorRegistry>(Extensions.Editors);
582 583 584 585 586 587

		// Serialize all editor inputs so that we can store them.
		// Editors that cannot be serialized need to be ignored
		// from mru, active and preview if any.
		let serializableEditors: EditorInput[] = [];
		let serializedEditors: ISerializedEditorInput[] = [];
B
Benjamin Pasero 已提交
588
		let serializablePreviewIndex: number;
589
		this.editors.forEach(e => {
590
			let factory = registry.getEditorInputFactory(e.getTypeId());
591 592 593
			if (factory) {
				let value = factory.serialize(e);
				if (typeof value === 'string') {
594
					serializedEditors.push({ id: e.getTypeId(), value });
595
					serializableEditors.push(e);
B
Benjamin Pasero 已提交
596 597 598 599

					if (this.preview === e) {
						serializablePreviewIndex = serializableEditors.length - 1;
					}
600 601 602 603
				}
			}
		});

B
Benjamin Pasero 已提交
604
		const serializableMru = this.mru.map(e => this.indexOf(e, serializableEditors)).filter(i => i >= 0);
605 606 607 608 609

		return {
			label: this.label,
			editors: serializedEditors,
			mru: serializableMru,
B
Benjamin Pasero 已提交
610
			preview: serializablePreviewIndex,
611 612 613 614
		};
	}

	private deserialize(data: ISerializedEditorGroup): void {
615
		const registry = Registry.as<IEditorRegistry>(Extensions.Editors);
616 617

		this._label = data.label;
618
		this.editors = data.editors.map(e => {
B
Benjamin Pasero 已提交
619 620 621
			const factory = registry.getEditorInputFactory(e.id);
			if (factory) {
				const editor = factory.deserialize(this.instantiationService, e.value);
622

B
Benjamin Pasero 已提交
623 624
				this.hookEditorListeners(editor);
				this.updateResourceMap(editor, false /* add */);
625

B
Benjamin Pasero 已提交
626 627 628 629 630
				return editor;
			}

			return null;
		}).filter(e => !!e);
631 632 633 634
		this.mru = data.mru.map(i => this.editors[i]);
		this.active = this.mru[0];
		this.preview = this.editors[data.preview];
	}
B
Benjamin Pasero 已提交
635 636

	public dispose(): void {
B
Benjamin Pasero 已提交
637
		dispose(this.toDispose);
B
Benjamin Pasero 已提交
638
	}
639 640 641 642 643
}

interface ISerializedEditorStacksModel {
	groups: ISerializedEditorGroup[];
	active: number;
B
Benjamin Pasero 已提交
644 645 646
}

export class EditorStacksModel implements IEditorStacksModel {
647 648 649 650

	private static STORAGE_KEY = 'editorStacks.model';

	private toDispose: IDisposable[];
B
Benjamin Pasero 已提交
651 652
	private loaded: boolean;

B
Benjamin Pasero 已提交
653
	private _groups: EditorGroup[];
654
	private _activeGroup: EditorGroup;
655
	private groupToIdentifier: { [id: number]: EditorGroup };
B
Benjamin Pasero 已提交
656 657 658

	private _onGroupOpened: Emitter<EditorGroup>;
	private _onGroupClosed: Emitter<EditorGroup>;
B
Benjamin Pasero 已提交
659
	private _onGroupMoved: Emitter<EditorGroup>;
B
Benjamin Pasero 已提交
660
	private _onGroupActivated: Emitter<EditorGroup>;
B
Benjamin Pasero 已提交
661
	private _onGroupDeactivated: Emitter<EditorGroup>;
662
	private _onGroupRenamed: Emitter<EditorGroup>;
663 664
	private _onEditorDisposed: Emitter<EditorIdentifier>;
	private _onEditorDirty: Emitter<EditorIdentifier>;
665
	private _onEditorClosed: Emitter<GroupEvent>;
666
	private _onModelChanged: Emitter<IStacksModelChangeEvent>;
B
Benjamin Pasero 已提交
667

668
	constructor(
669
		private restoreFromStorage: boolean,
670 671 672 673 674 675
		@IStorageService private storageService: IStorageService,
		@ILifecycleService private lifecycleService: ILifecycleService,
		@IInstantiationService private instantiationService: IInstantiationService
	) {
		this.toDispose = [];

B
Benjamin Pasero 已提交
676
		this._groups = [];
677
		this.groupToIdentifier = Object.create(null);
B
Benjamin Pasero 已提交
678

B
Benjamin Pasero 已提交
679 680 681
		this._onGroupOpened = new Emitter<EditorGroup>();
		this._onGroupClosed = new Emitter<EditorGroup>();
		this._onGroupActivated = new Emitter<EditorGroup>();
B
Benjamin Pasero 已提交
682
		this._onGroupDeactivated = new Emitter<EditorGroup>();
B
Benjamin Pasero 已提交
683
		this._onGroupMoved = new Emitter<EditorGroup>();
684
		this._onGroupRenamed = new Emitter<EditorGroup>();
685
		this._onModelChanged = new Emitter<IStacksModelChangeEvent>();
686 687
		this._onEditorDisposed = new Emitter<EditorIdentifier>();
		this._onEditorDirty = new Emitter<EditorIdentifier>();
688
		this._onEditorClosed = new Emitter<GroupEvent>();
689

690
		this.toDispose.push(this._onGroupOpened, this._onGroupClosed, this._onGroupActivated, this._onGroupDeactivated, this._onGroupMoved, this._onGroupRenamed, this._onModelChanged, this._onEditorDisposed, this._onEditorDirty, this._onEditorClosed);
B
Benjamin Pasero 已提交
691

692 693 694 695 696
		this.registerListeners();
	}

	private registerListeners(): void {
		this.toDispose.push(this.lifecycleService.onShutdown(() => this.onShutdown()));
B
Benjamin Pasero 已提交
697 698 699 700 701 702 703 704 705 706 707 708 709 710
	}

	public get onGroupOpened(): Event<EditorGroup> {
		return this._onGroupOpened.event;
	}

	public get onGroupClosed(): Event<EditorGroup> {
		return this._onGroupClosed.event;
	}

	public get onGroupActivated(): Event<EditorGroup> {
		return this._onGroupActivated.event;
	}

B
Benjamin Pasero 已提交
711 712 713 714
	public get onGroupDeactivated(): Event<EditorGroup> {
		return this._onGroupDeactivated.event;
	}

B
Benjamin Pasero 已提交
715 716 717 718
	public get onGroupMoved(): Event<EditorGroup> {
		return this._onGroupMoved.event;
	}

719 720 721 722
	public get onGroupRenamed(): Event<EditorGroup> {
		return this._onGroupRenamed.event;
	}

723
	public get onModelChanged(): Event<IStacksModelChangeEvent> {
724 725 726
		return this._onModelChanged.event;
	}

727
	public get onEditorDisposed(): Event<EditorIdentifier> {
728 729 730
		return this._onEditorDisposed.event;
	}

731
	public get onEditorDirty(): Event<EditorIdentifier> {
732 733 734
		return this._onEditorDirty.event;
	}

735 736 737 738
	public get onEditorClosed(): Event<GroupEvent> {
		return this._onEditorClosed.event;
	}

B
Benjamin Pasero 已提交
739
	public get groups(): EditorGroup[] {
B
Benjamin Pasero 已提交
740 741
		this.ensureLoaded();

B
Benjamin Pasero 已提交
742 743 744 745
		return this._groups.slice(0);
	}

	public get activeGroup(): EditorGroup {
B
Benjamin Pasero 已提交
746 747
		this.ensureLoaded();

748
		return this._activeGroup;
B
Benjamin Pasero 已提交
749 750
	}

B
Benjamin Pasero 已提交
751 752 753 754
	public isActive(group: EditorGroup): boolean {
		return this.activeGroup === group;
	}

755
	public getGroup(id: GroupIdentifier): EditorGroup {
B
Benjamin Pasero 已提交
756 757
		this.ensureLoaded();

758 759 760
		return this.groupToIdentifier[id];
	}

761
	public openGroup(label: string, activate = true, index?: number): EditorGroup {
B
Benjamin Pasero 已提交
762 763
		this.ensureLoaded();

764
		const group = this.doCreateGroup(label);
B
Benjamin Pasero 已提交
765

766 767 768 769 770
		// Direct index provided
		if (typeof index === 'number') {
			this._groups[index] = group;
		}

B
Benjamin Pasero 已提交
771
		// First group
772
		else if (!this._activeGroup) {
B
Benjamin Pasero 已提交
773 774 775
			this._groups.push(group);
		}

776
		// Subsequent group (open to the right of active one)
B
Benjamin Pasero 已提交
777
		else {
778
			this._groups.splice(this.indexOf(this._activeGroup) + 1, 0, group);
B
Benjamin Pasero 已提交
779 780 781
		}

		// Event
782
		this.fireEvent(this._onGroupOpened, group, true);
B
Benjamin Pasero 已提交
783

784
		// Activate if we are first or set to activate groups
785
		if (!this._activeGroup || activate) {
786 787
			this.setActive(group);
		}
B
Benjamin Pasero 已提交
788 789 790 791

		return group;
	}

792
	public renameGroup(group: EditorGroup, label: string): void {
B
Benjamin Pasero 已提交
793 794
		this.ensureLoaded();

B
Benjamin Pasero 已提交
795
		if (group.label !== label) {
B
Benjamin Pasero 已提交
796
			group.label = label;
797
			this.fireEvent(this._onGroupRenamed, group, false);
B
Benjamin Pasero 已提交
798
		}
B
Benjamin Pasero 已提交
799
	}
800

B
Benjamin Pasero 已提交
801
	public closeGroup(group: EditorGroup): void {
B
Benjamin Pasero 已提交
802 803
		this.ensureLoaded();

B
Benjamin Pasero 已提交
804 805 806 807 808 809
		const index = this.indexOf(group);
		if (index < 0) {
			return; // group does not exist
		}

		// Active group closed: Find a new active one to the right
810
		if (group === this._activeGroup) {
B
Benjamin Pasero 已提交
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825

			// More than one group
			if (this._groups.length > 1) {
				let newActiveGroup: EditorGroup;
				if (this._groups.length > index + 1) {
					newActiveGroup = this._groups[index + 1]; // make next group to the right active
				} else {
					newActiveGroup = this._groups[index - 1]; // make next group to the left active
				}

				this.setActive(newActiveGroup);
			}

			// One group
			else {
826
				this._activeGroup = null;
B
Benjamin Pasero 已提交
827 828 829
			}
		}

B
Benjamin Pasero 已提交
830
		// Close Editors in Group first and dispose then
831
		group.closeAllEditors();
B
Benjamin Pasero 已提交
832
		group.dispose();
833

B
Benjamin Pasero 已提交
834 835
		// Splice from groups
		this._groups.splice(index, 1);
836
		this.groupToIdentifier[group.id] = void 0;
B
Benjamin Pasero 已提交
837

838
		// Events
839
		this.fireEvent(this._onGroupClosed, group, true);
840 841 842
		for (let i = index; i < this._groups.length; i++) {
			this.fireEvent(this._onGroupMoved, this._groups[i], true); // send move event for groups to the right that moved to the left into the closed group position
		}
B
Benjamin Pasero 已提交
843 844
	}

B
Benjamin Pasero 已提交
845
	public closeGroups(except?: EditorGroup): void {
B
Benjamin Pasero 已提交
846
		this.ensureLoaded();
847 848

		// Optimize: close all non active groups first to produce less upstream work
849
		this.groups.filter(g => g !== this._activeGroup && g !== except).forEach(g => this.closeGroup(g));
B
Benjamin Pasero 已提交
850 851

		// Close active unless configured to skip
852 853
		if (this._activeGroup !== except) {
			this.closeGroup(this._activeGroup);
B
Benjamin Pasero 已提交
854
		}
855 856
	}

B
Benjamin Pasero 已提交
857
	public setActive(group: EditorGroup): void {
B
Benjamin Pasero 已提交
858 859
		this.ensureLoaded();

860
		if (this._activeGroup === group) {
861 862 863
			return;
		}

B
Benjamin Pasero 已提交
864
		const oldActiveGroup = this._activeGroup;
865
		this._activeGroup = group;
B
Benjamin Pasero 已提交
866

867
		this.fireEvent(this._onGroupActivated, group, false);
B
Benjamin Pasero 已提交
868 869 870
		if (oldActiveGroup) {
			this.fireEvent(this._onGroupDeactivated, oldActiveGroup, false);
		}
B
Benjamin Pasero 已提交
871 872
	}

B
Benjamin Pasero 已提交
873
	public moveGroup(group: EditorGroup, toIndex: number): void {
B
Benjamin Pasero 已提交
874 875
		this.ensureLoaded();

B
Benjamin Pasero 已提交
876 877 878 879 880 881 882 883 884 885
		const index = this.indexOf(group);
		if (index < 0) {
			return;
		}

		// Move
		this._groups.splice(index, 1);
		this._groups.splice(toIndex, 0, group);

		// Event
B
Benjamin Pasero 已提交
886 887 888
		for (let i = Math.min(index, toIndex); i <= Math.max(index, toIndex) && i < this._groups.length; i++) {
			this.fireEvent(this._onGroupMoved, this._groups[i], true); // send move event for groups to the right that moved to the left into the closed group position
		}
B
Benjamin Pasero 已提交
889 890
	}

B
Benjamin Pasero 已提交
891 892 893
	private indexOf(group: EditorGroup): number {
		return this._groups.indexOf(group);
	}
894

895 896
	public positionOfGroup(group: IEditorGroup): Position;
	public positionOfGroup(group: EditorGroup): Position;
B
Benjamin Pasero 已提交
897 898 899
	public positionOfGroup(group: EditorGroup): Position {
		return this.indexOf(group);
	}
B
Benjamin Pasero 已提交
900 901

	public groupAt(position: Position): EditorGroup {
902 903
		this.ensureLoaded();

B
Benjamin Pasero 已提交
904 905
		return this._groups[position];
	}
B
Benjamin Pasero 已提交
906

B
Benjamin Pasero 已提交
907
	public next(): IEditorIdentifier {
908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
		this.ensureLoaded();

		if (!this.activeGroup) {
			return null;
		}

		const index = this.activeGroup.indexOf(this.activeGroup.activeEditor);

		// Return next in group
		if (index + 1 < this.activeGroup.count) {
			return { group: this.activeGroup, editor: this.activeGroup.getEditor(index + 1) };
		}

		// Return first in next group
		const indexOfGroup = this.indexOf(this.activeGroup);
		const nextGroup = this.groups[indexOfGroup + 1];
		if (nextGroup) {
			return { group: nextGroup, editor: nextGroup.getEditor(0) };
		}

		// Return first in first group
		const firstGroup = this.groups[0];
		return { group: firstGroup, editor: firstGroup.getEditor(0) };
	}

B
Benjamin Pasero 已提交
933
	public previous(): IEditorIdentifier {
934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958
		this.ensureLoaded();

		if (!this.activeGroup) {
			return null;
		}

		const index = this.activeGroup.indexOf(this.activeGroup.activeEditor);

		// Return previous in group
		if (index > 0) {
			return { group: this.activeGroup, editor: this.activeGroup.getEditor(index - 1) };
		}

		// Return last in previous group
		const indexOfGroup = this.indexOf(this.activeGroup);
		const previousGroup = this.groups[indexOfGroup - 1];
		if (previousGroup) {
			return { group: previousGroup, editor: previousGroup.getEditor(previousGroup.count - 1) };
		}

		// Return last in last group
		const lastGroup = this.groups[this.groups.length - 1];
		return { group: lastGroup, editor: lastGroup.getEditor(lastGroup.count - 1) };
	}

959
	private save(): void {
960 961 962 963 964 965
		const serialized = this.serialize();

		this.storageService.store(EditorStacksModel.STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE);
	}

	private serialize(): ISerializedEditorStacksModel {
966

967
		// Exclude now empty groups (can happen if an editor cannot be serialized)
968
		let serializableGroups = this._groups.map(g => g.serialize()).filter(g => g.editors.length > 0);
969

970
		// Only consider active index if we do not have empty groups
B
Benjamin Pasero 已提交
971 972 973 974 975 976 977
		let serializableActiveIndex: number;
		if (serializableGroups.length > 0) {
			if (serializableGroups.length === this._groups.length) {
				serializableActiveIndex = this.indexOf(this._activeGroup);
			} else {
				serializableActiveIndex = 0;
			}
978 979
		}

980
		return {
981
			groups: serializableGroups,
982
			active: serializableActiveIndex
983 984 985
		};
	}

986
	private fireEvent(emitter: Emitter<EditorGroup>, group: EditorGroup, isStructuralChange: boolean): void {
987
		emitter.fire(group);
988
		this._onModelChanged.fire({ group, structural: isStructuralChange });
989 990
	}

B
Benjamin Pasero 已提交
991 992 993 994 995 996 997
	private ensureLoaded(): void {
		if (!this.loaded) {
			this.loaded = true;
			this.load();
		}
	}

998
	private load(): void {
999
		if (!this.restoreFromStorage) {
B
Benjamin Pasero 已提交
1000 1001 1002
			return; // do not load from last session if the user explicitly asks to open a set of files
		}

1003 1004 1005 1006
		const modelRaw = this.storageService.get(EditorStacksModel.STORAGE_KEY, StorageScope.WORKSPACE);
		if (modelRaw) {
			const serialized: ISerializedEditorStacksModel = JSON.parse(modelRaw);

1007
			const invalidId = this.doValidate(serialized);
B
Benjamin Pasero 已提交
1008
			if (invalidId) {
B
Benjamin Pasero 已提交
1009 1010
				console.warn(`Ignoring invalid stacks model (Error code: ${invalidId}): ${JSON.stringify(serialized)}`);
				console.warn(serialized);
B
Benjamin Pasero 已提交
1011 1012 1013
				return;
			}

1014
			this._groups = serialized.groups.map(s => this.doCreateGroup(s));
1015
			this._activeGroup = this._groups[serialized.active];
1016 1017 1018
		}
	}

1019
	private doValidate(serialized: ISerializedEditorStacksModel): number {
B
Benjamin Pasero 已提交
1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
		if (!serialized.groups.length && typeof serialized.active === 'number') {
			return 1; // Invalid active (we have no groups, but an active one)
		}

		if (serialized.groups.length && !serialized.groups[serialized.active]) {
			return 2; // Invalid active (we cannot find the active one in group)
		}

		if (serialized.groups.length > 3) {
			return 3; // Too many groups
		}

1032
		if (serialized.groups.some(g => !g.editors.length)) {
B
Benjamin Pasero 已提交
1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
			return 4; // Some empty groups
		}

		if (serialized.groups.some(g => g.editors.length !== g.mru.length)) {
			return 5; // MRU out of sync with editors
		}

		if (serialized.groups.some(g => typeof g.preview === 'number' && !g.editors[g.preview])) {
			return 6; // Invalid preview editor
		}

		if (serialized.groups.some(g => !g.label)) {
			return 7; // Group without label
		}

		return 0;
	}

1051 1052 1053
	private doCreateGroup(arg1: string | ISerializedEditorGroup): EditorGroup {
		const group = this.instantiationService.createInstance(EditorGroup, arg1);

1054 1055
		this.groupToIdentifier[group.id] = group;

1056
		// Funnel editor changes in the group through our event aggregator
1057
		const unbind: IDisposable[] = [];
1058 1059
		unbind.push(group.onEditorsStructureChanged(editor => this._onModelChanged.fire({ group, editor, structural: true })));
		unbind.push(group.onEditorStateChanged(editor => this._onModelChanged.fire({ group, editor })));
1060 1061 1062 1063
		unbind.push(group.onEditorClosed(event => {
			this.handleOnEditorClosed(event);
			this._onEditorClosed.fire(event);
		}));
1064 1065 1066
		unbind.push(group.onEditorDisposed(editor => this._onEditorDisposed.fire({ editor, group })));
		unbind.push(group.onEditorDirty(editor => this._onEditorDirty.fire({ editor, group })));
		unbind.push(this.onGroupClosed(g => {
1067
			if (g === group) {
1068
				dispose(unbind);
1069
			}
1070
		}));
1071 1072 1073 1074

		return group;
	}

1075
	private handleOnEditorClosed(event: GroupEvent): void {
1076 1077 1078 1079 1080 1081 1082 1083
		const editor = event.editor;

		// Close the editor when it is no longer open in any group
		if (!this.isOpen(editor)) {
			editor.close();

			// Also take care of diff editor inputs that wrap around 2 editors
			if (editor instanceof DiffEditorInput) {
B
Benjamin Pasero 已提交
1084
				[editor.originalInput, editor.modifiedInput].forEach(editor => {
1085 1086 1087 1088 1089
					if (!this.isOpen(editor)) {
						editor.close();
					}
				});
			}
1090 1091 1092
		}
	}

1093 1094 1095
	public isOpen(resource: URI): boolean;
	public isOpen(editor: EditorInput): boolean;
	public isOpen(arg1: any): boolean {
1096 1097
		return this._groups.some(group => group.contains(arg1));
	}
1098

1099 1100 1101 1102
	public count(resource: URI): number;
	public count(editor: EditorInput): number;
	public count(arg1: any): number {
		return this._groups.filter(group => group.contains(arg1)).length;
1103 1104
	}

1105 1106 1107 1108 1109
	private onShutdown(): void {
		this.save();

		dispose(this.toDispose);
	}
1110

1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
	public validate(): void {
		const serialized = this.serialize();
		const invalidId = this.doValidate(serialized);
		if (invalidId) {
			console.warn(`Ignoring invalid stacks model (Error code: ${invalidId}): ${JSON.stringify(serialized)}`);
			console.warn(serialized);
		} else {
			console.log('Stacks Model OK!');
		}
	}

1122
	public toString(): string {
B
Benjamin Pasero 已提交
1123 1124
		this.ensureLoaded();

1125 1126
		const lines: string[] = [];

B
Benjamin Pasero 已提交
1127 1128 1129 1130
		if (!this.groups.length) {
			return '<No Groups>';
		}

1131
		this.groups.forEach(g => {
B
Benjamin Pasero 已提交
1132 1133
			let label = `Group: ${g.label}`;

1134
			if (this._activeGroup === g) {
B
Benjamin Pasero 已提交
1135
				label = `${label} [active]`;
1136 1137
			}

B
Benjamin Pasero 已提交
1138 1139
			lines.push(label);

1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156
			g.getEditors().forEach(e => {
				let label = `\t${e.getName()}`;

				if (g.previewEditor === e) {
					label = `${label} [preview]`;
				}

				if (g.activeEditor === e) {
					label = `${label} [active]`;
				}

				lines.push(label);
			});
		});

		return lines.join('\n');
	}
B
Benjamin Pasero 已提交
1157
}