editorStacksModel.ts 32.8 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';
9
import {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
import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle';
B
Benjamin Pasero 已提交
15
import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService';
16 17 18
import {dispose, IDisposable} from 'vs/base/common/lifecycle';
import {IEditorRegistry, Extensions} from 'vs/workbench/browser/parts/editor/baseEditor';
import {Registry} from 'vs/platform/platform';
19 20
import {Position, Direction} from 'vs/platform/editor/common/editor';
import {DiffEditorInput} from 'vs/workbench/common/editor/diffEditorInput';
B
Benjamin Pasero 已提交
21

22 23 24
// 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

25
export interface GroupEvent extends IGroupEvent {
26 27 28
	editor: EditorInput;
}

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

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

40
export interface ISerializedEditorInput {
41 42 43 44
	id: string;
	value: string;
}

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

B
Benjamin Pasero 已提交
52
export class EditorGroup implements IEditorGroup {
53 54 55 56

	private static IDS = 0;

	private _id: GroupIdentifier;
57 58
	private _label: string;

B
Benjamin Pasero 已提交
59 60
	private editors: EditorInput[];
	private mru: EditorInput[];
61
	private mapResourceToEditor: { [resource: string]: EditorInput };
B
Benjamin Pasero 已提交
62 63 64 65

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

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

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

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

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

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

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

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

		this.registerListeners();
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

243 244 245
			// 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 已提交
246 247
			}

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

				// Replace existing preview with this editor if we have a preview
252 253
				if (this.preview) {
					const indexOfPreview = this.indexOf(this.preview);
254 255 256 257
					if (targetIndex >= indexOfPreview) {
						targetIndex--;
					}

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

B
Benjamin Pasero 已提交
262 263 264
				this.preview = editor;
			}

265 266
			// Listeners
			this.hookEditorListeners(editor);
267

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

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

		// Existing editor
		else {

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

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

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

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

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

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

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

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

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

			// More than one editor
B
Benjamin Pasero 已提交
330 331
			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 已提交
332 333 334 335 336 337 338 339 340
			}

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

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

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

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

B
Benjamin Pasero 已提交
354
	public closeEditors(except: EditorInput, direction?: Direction): void {
B
Benjamin Pasero 已提交
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
		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 已提交
376
			this.mru.filter(e => !this.matches(e, except)).forEach(e => this.closeEditor(e));
B
Benjamin Pasero 已提交
377 378 379
		}
	}

380 381 382
	public closeAllEditors(): void {

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

B
Benjamin Pasero 已提交
387 388 389 390 391 392 393 394 395 396 397
	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
398
		this.fireEvent(this._onEditorMoved, editor, true);
B
Benjamin Pasero 已提交
399 400
	}

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

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

		this.active = editor;

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

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

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

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

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

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

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

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

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

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

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

B
Benjamin Pasero 已提交
458 459 460 461 462 463 464 465 466 467
	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 已提交
468
		return !this.matches(this.preview, editor);
B
Benjamin Pasero 已提交
469 470
	}

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

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

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

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

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

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

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

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

508
			// Replace
B
Benjamin Pasero 已提交
509
			else {
510 511 512
				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 已提交
513
			}
B
Benjamin Pasero 已提交
514 515 516
		}
	}

517
	private updateResourceMap(editor: EditorInput, remove: boolean): void {
518
		const resource = getUntitledOrFileResource(editor, true /* include diff editors */);
519 520 521 522 523
		if (resource) {
			this.mapResourceToEditor[resource.toString()] = remove ? void 0 : editor;
		}
	}

524
	public indexOf(candidate: EditorInput, editors = this.editors): number {
B
Benjamin Pasero 已提交
525 526
		if (!candidate) {
			return -1;
B
Benjamin Pasero 已提交
527 528
		}

B
Benjamin Pasero 已提交
529
		for (let i = 0; i < editors.length; i++) {
B
Benjamin Pasero 已提交
530
			if (this.matches(editors[i], candidate)) {
B
Benjamin Pasero 已提交
531 532 533 534 535 536
				return i;
			}
		}

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

538 539 540 541 542 543 544 545
	public contains(candidate: EditorInput): boolean;
	public contains(resource: URI): boolean;
	public contains(arg1: any): boolean {
		if (arg1 instanceof EditorInput) {
			return this.indexOf(arg1) >= 0;
		}

		return !!this.mapResourceToEditor[(<URI>arg1).toString()];
546 547
	}

B
Benjamin Pasero 已提交
548 549 550 551 552 553 554 555 556
	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 已提交
557
		this.mru.splice(mruIndex, 1);
B
Benjamin Pasero 已提交
558

B
Benjamin Pasero 已提交
559
		// Set editor to front
B
Benjamin Pasero 已提交
560 561
		this.mru.unshift(editor);
	}
B
Benjamin Pasero 已提交
562 563 564 565

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

	public serialize(): ISerializedEditorGroup {
568
		const registry = Registry.as<IEditorRegistry>(Extensions.Editors);
569 570 571 572 573 574

		// 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 已提交
575
		let serializablePreviewIndex: number;
576
		this.editors.forEach(e => {
577
			let factory = registry.getEditorInputFactory(e.getTypeId());
578 579 580
			if (factory) {
				let value = factory.serialize(e);
				if (typeof value === 'string') {
581
					serializedEditors.push({ id: e.getTypeId(), value });
582
					serializableEditors.push(e);
B
Benjamin Pasero 已提交
583 584 585 586

					if (this.preview === e) {
						serializablePreviewIndex = serializableEditors.length - 1;
					}
587 588 589 590
				}
			}
		});

B
Benjamin Pasero 已提交
591
		const serializableMru = this.mru.map(e => this.indexOf(e, serializableEditors)).filter(i => i >= 0);
592 593 594 595 596

		return {
			label: this.label,
			editors: serializedEditors,
			mru: serializableMru,
B
Benjamin Pasero 已提交
597
			preview: serializablePreviewIndex,
598 599 600 601
		};
	}

	private deserialize(data: ISerializedEditorGroup): void {
602
		const registry = Registry.as<IEditorRegistry>(Extensions.Editors);
603 604

		this._label = data.label;
605
		this.editors = data.editors.map(e => {
B
Benjamin Pasero 已提交
606 607 608
			const factory = registry.getEditorInputFactory(e.id);
			if (factory) {
				const editor = factory.deserialize(this.instantiationService, e.value);
609

B
Benjamin Pasero 已提交
610 611
				this.hookEditorListeners(editor);
				this.updateResourceMap(editor, false /* add */);
612

B
Benjamin Pasero 已提交
613 614 615 616 617
				return editor;
			}

			return null;
		}).filter(e => !!e);
618 619 620 621
		this.mru = data.mru.map(i => this.editors[i]);
		this.active = this.mru[0];
		this.preview = this.editors[data.preview];
	}
B
Benjamin Pasero 已提交
622 623

	public dispose(): void {
B
Benjamin Pasero 已提交
624
		dispose(this.toDispose);
B
Benjamin Pasero 已提交
625
	}
626 627 628 629 630
}

interface ISerializedEditorStacksModel {
	groups: ISerializedEditorGroup[];
	active: number;
B
Benjamin Pasero 已提交
631 632 633
}

export class EditorStacksModel implements IEditorStacksModel {
634 635 636 637

	private static STORAGE_KEY = 'editorStacks.model';

	private toDispose: IDisposable[];
B
Benjamin Pasero 已提交
638 639
	private loaded: boolean;

B
Benjamin Pasero 已提交
640
	private _groups: EditorGroup[];
641
	private _activeGroup: EditorGroup;
642
	private groupToIdentifier: { [id: number]: EditorGroup };
B
Benjamin Pasero 已提交
643 644 645

	private _onGroupOpened: Emitter<EditorGroup>;
	private _onGroupClosed: Emitter<EditorGroup>;
B
Benjamin Pasero 已提交
646
	private _onGroupMoved: Emitter<EditorGroup>;
B
Benjamin Pasero 已提交
647
	private _onGroupActivated: Emitter<EditorGroup>;
B
Benjamin Pasero 已提交
648
	private _onGroupDeactivated: Emitter<EditorGroup>;
649
	private _onGroupRenamed: Emitter<EditorGroup>;
650 651
	private _onEditorDisposed: Emitter<EditorIdentifier>;
	private _onEditorDirty: Emitter<EditorIdentifier>;
652
	private _onEditorClosed: Emitter<GroupEvent>;
653
	private _onModelChanged: Emitter<IStacksModelChangeEvent>;
B
Benjamin Pasero 已提交
654

655 656 657
	constructor(
		@IStorageService private storageService: IStorageService,
		@ILifecycleService private lifecycleService: ILifecycleService,
B
Benjamin Pasero 已提交
658
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
659 660 661 662
		@IInstantiationService private instantiationService: IInstantiationService
	) {
		this.toDispose = [];

B
Benjamin Pasero 已提交
663
		this._groups = [];
664
		this.groupToIdentifier = Object.create(null);
B
Benjamin Pasero 已提交
665

B
Benjamin Pasero 已提交
666 667 668
		this._onGroupOpened = new Emitter<EditorGroup>();
		this._onGroupClosed = new Emitter<EditorGroup>();
		this._onGroupActivated = new Emitter<EditorGroup>();
B
Benjamin Pasero 已提交
669
		this._onGroupDeactivated = new Emitter<EditorGroup>();
B
Benjamin Pasero 已提交
670
		this._onGroupMoved = new Emitter<EditorGroup>();
671
		this._onGroupRenamed = new Emitter<EditorGroup>();
672
		this._onModelChanged = new Emitter<IStacksModelChangeEvent>();
673 674
		this._onEditorDisposed = new Emitter<EditorIdentifier>();
		this._onEditorDirty = new Emitter<EditorIdentifier>();
675
		this._onEditorClosed = new Emitter<GroupEvent>();
676

677
		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 已提交
678

679 680 681 682 683
		this.registerListeners();
	}

	private registerListeners(): void {
		this.toDispose.push(this.lifecycleService.onShutdown(() => this.onShutdown()));
B
Benjamin Pasero 已提交
684 685 686 687 688 689 690 691 692 693 694 695 696 697
	}

	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 已提交
698 699 700 701
	public get onGroupDeactivated(): Event<EditorGroup> {
		return this._onGroupDeactivated.event;
	}

B
Benjamin Pasero 已提交
702 703 704 705
	public get onGroupMoved(): Event<EditorGroup> {
		return this._onGroupMoved.event;
	}

706 707 708 709
	public get onGroupRenamed(): Event<EditorGroup> {
		return this._onGroupRenamed.event;
	}

710
	public get onModelChanged(): Event<IStacksModelChangeEvent> {
711 712 713
		return this._onModelChanged.event;
	}

714
	public get onEditorDisposed(): Event<EditorIdentifier> {
715 716 717
		return this._onEditorDisposed.event;
	}

718
	public get onEditorDirty(): Event<EditorIdentifier> {
719 720 721
		return this._onEditorDirty.event;
	}

722 723 724 725
	public get onEditorClosed(): Event<GroupEvent> {
		return this._onEditorClosed.event;
	}

B
Benjamin Pasero 已提交
726
	public get groups(): EditorGroup[] {
B
Benjamin Pasero 已提交
727 728
		this.ensureLoaded();

B
Benjamin Pasero 已提交
729 730 731 732
		return this._groups.slice(0);
	}

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

735
		return this._activeGroup;
B
Benjamin Pasero 已提交
736 737
	}

B
Benjamin Pasero 已提交
738 739 740 741
	public isActive(group: EditorGroup): boolean {
		return this.activeGroup === group;
	}

742
	public getGroup(id: GroupIdentifier): EditorGroup {
B
Benjamin Pasero 已提交
743 744
		this.ensureLoaded();

745 746 747
		return this.groupToIdentifier[id];
	}

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

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

753 754 755 756 757
		// Direct index provided
		if (typeof index === 'number') {
			this._groups[index] = group;
		}

B
Benjamin Pasero 已提交
758
		// First group
759
		else if (!this._activeGroup) {
B
Benjamin Pasero 已提交
760 761 762
			this._groups.push(group);
		}

763
		// Subsequent group (open to the right of active one)
B
Benjamin Pasero 已提交
764
		else {
765
			this._groups.splice(this.indexOf(this._activeGroup) + 1, 0, group);
B
Benjamin Pasero 已提交
766 767 768
		}

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

771
		// Activate if we are first or set to activate groups
772
		if (!this._activeGroup || activate) {
773 774
			this.setActive(group);
		}
B
Benjamin Pasero 已提交
775 776 777 778

		return group;
	}

779
	public renameGroup(group: EditorGroup, label: string): void {
B
Benjamin Pasero 已提交
780 781
		this.ensureLoaded();

B
Benjamin Pasero 已提交
782
		if (group.label !== label) {
B
Benjamin Pasero 已提交
783
			group.label = label;
784
			this.fireEvent(this._onGroupRenamed, group, false);
B
Benjamin Pasero 已提交
785
		}
B
Benjamin Pasero 已提交
786
	}
787

B
Benjamin Pasero 已提交
788
	public closeGroup(group: EditorGroup): void {
B
Benjamin Pasero 已提交
789 790
		this.ensureLoaded();

B
Benjamin Pasero 已提交
791 792 793 794 795 796
		const index = this.indexOf(group);
		if (index < 0) {
			return; // group does not exist
		}

		// Active group closed: Find a new active one to the right
797
		if (group === this._activeGroup) {
B
Benjamin Pasero 已提交
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812

			// 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 {
813
				this._activeGroup = null;
B
Benjamin Pasero 已提交
814 815 816
			}
		}

B
Benjamin Pasero 已提交
817
		// Close Editors in Group first and dispose then
818
		group.closeAllEditors();
B
Benjamin Pasero 已提交
819
		group.dispose();
820

B
Benjamin Pasero 已提交
821 822
		// Splice from groups
		this._groups.splice(index, 1);
823
		this.groupToIdentifier[group.id] = void 0;
B
Benjamin Pasero 已提交
824

825
		// Events
826
		this.fireEvent(this._onGroupClosed, group, true);
827 828 829
		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 已提交
830 831
	}

B
Benjamin Pasero 已提交
832
	public closeGroups(except?: EditorGroup): void {
B
Benjamin Pasero 已提交
833
		this.ensureLoaded();
834 835

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

		// Close active unless configured to skip
839 840
		if (this._activeGroup !== except) {
			this.closeGroup(this._activeGroup);
B
Benjamin Pasero 已提交
841
		}
842 843
	}

B
Benjamin Pasero 已提交
844
	public setActive(group: EditorGroup): void {
B
Benjamin Pasero 已提交
845 846
		this.ensureLoaded();

847
		if (this._activeGroup === group) {
848 849 850
			return;
		}

B
Benjamin Pasero 已提交
851
		const oldActiveGroup = this._activeGroup;
852
		this._activeGroup = group;
B
Benjamin Pasero 已提交
853

854
		this.fireEvent(this._onGroupActivated, group, false);
B
Benjamin Pasero 已提交
855 856 857
		if (oldActiveGroup) {
			this.fireEvent(this._onGroupDeactivated, oldActiveGroup, false);
		}
B
Benjamin Pasero 已提交
858 859
	}

B
Benjamin Pasero 已提交
860
	public moveGroup(group: EditorGroup, toIndex: number): void {
B
Benjamin Pasero 已提交
861 862
		this.ensureLoaded();

B
Benjamin Pasero 已提交
863 864 865 866 867 868 869 870 871 872
		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 已提交
873 874 875
		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 已提交
876 877
	}

B
Benjamin Pasero 已提交
878 879 880
	private indexOf(group: EditorGroup): number {
		return this._groups.indexOf(group);
	}
881

882 883
	public positionOfGroup(group: IEditorGroup): Position;
	public positionOfGroup(group: EditorGroup): Position;
B
Benjamin Pasero 已提交
884 885 886
	public positionOfGroup(group: EditorGroup): Position {
		return this.indexOf(group);
	}
B
Benjamin Pasero 已提交
887 888

	public groupAt(position: Position): EditorGroup {
889 890
		this.ensureLoaded();

B
Benjamin Pasero 已提交
891 892
		return this._groups[position];
	}
B
Benjamin Pasero 已提交
893

B
Benjamin Pasero 已提交
894
	public next(): IEditorIdentifier {
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
		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 已提交
920
	public previous(): IEditorIdentifier {
921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
		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) };
	}

946
	private save(): void {
947 948 949 950 951 952
		const serialized = this.serialize();

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

	private serialize(): ISerializedEditorStacksModel {
953

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

957
		// Only consider active index if we do not have empty groups
B
Benjamin Pasero 已提交
958 959 960 961 962 963 964
		let serializableActiveIndex: number;
		if (serializableGroups.length > 0) {
			if (serializableGroups.length === this._groups.length) {
				serializableActiveIndex = this.indexOf(this._activeGroup);
			} else {
				serializableActiveIndex = 0;
			}
965 966
		}

967
		return {
968
			groups: serializableGroups,
969
			active: serializableActiveIndex
970 971 972
		};
	}

973
	private fireEvent(emitter: Emitter<EditorGroup>, group: EditorGroup, isStructuralChange: boolean): void {
974
		emitter.fire(group);
975
		this._onModelChanged.fire({ group, structural: isStructuralChange });
976 977
	}

B
Benjamin Pasero 已提交
978 979 980 981 982 983 984
	private ensureLoaded(): void {
		if (!this.loaded) {
			this.loaded = true;
			this.load();
		}
	}

985
	private load(): void {
B
Benjamin Pasero 已提交
986 987 988 989 990
		const options = this.contextService.getOptions();
		if ((options.filesToCreate && options.filesToCreate.length) || (options.filesToOpen && options.filesToOpen.length) || (options.filesToDiff && options.filesToDiff.length)) {
			return; // do not load from last session if the user explicitly asks to open a set of files
		}

991 992 993 994
		const modelRaw = this.storageService.get(EditorStacksModel.STORAGE_KEY, StorageScope.WORKSPACE);
		if (modelRaw) {
			const serialized: ISerializedEditorStacksModel = JSON.parse(modelRaw);

B
Benjamin Pasero 已提交
995
			// TODO@Ben remove this once stacks are stable; prevent bad stored state
996
			const invalidId = this.doValidate(serialized);
B
Benjamin Pasero 已提交
997
			if (invalidId) {
B
Benjamin Pasero 已提交
998 999
				console.warn(`Ignoring invalid stacks model (Error code: ${invalidId}): ${JSON.stringify(serialized)}`);
				console.warn(serialized);
B
Benjamin Pasero 已提交
1000 1001 1002
				return;
			}

1003
			this._groups = serialized.groups.map(s => this.doCreateGroup(s));
1004
			this._activeGroup = this._groups[serialized.active];
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
		} else {
			this.migrate();
		}
	}

	// TODO@Ben migration
	private migrate(): void {
		const LEGACY_EDITOR_STATE_STORAGE_KEY = 'memento/workbench.parts.editor';
		const legacyModelRaw = this.storageService.get(LEGACY_EDITOR_STATE_STORAGE_KEY, StorageScope.WORKSPACE);
		if (legacyModelRaw) {
			try {
				const legacyModel = JSON.parse(legacyModelRaw);
				const state = legacyModel['editorpart.editorState'];
				const editorsRaw: { inputId: string; inputValue: string }[] = state.editors;

				const registry = Registry.as<IEditorRegistry>(Extensions.Editors);
B
Benjamin Pasero 已提交
1021 1022 1023 1024 1025 1026 1027 1028
				const editors = editorsRaw.map(editorRaw => {
					const factory = registry.getEditorInputFactory(editorRaw.inputId);
					if (factory) {
						return factory.deserialize(this.instantiationService, editorRaw.inputValue);
					}

					return null;
				}).filter(editor => !!editor);
1029 1030 1031

				if (editors.length > 0) {
					const leftGroup = this.openGroup('', true);
1032
					leftGroup.openEditor(editors[0], { active: true, pinned: true });
1033 1034 1035 1036
				}

				if (editors.length > 1) {
					const centerGroup = this.openGroup('', true);
1037
					centerGroup.openEditor(editors[1], { active: true, pinned: true });
1038 1039 1040 1041
				}

				if (editors.length > 2) {
					const rightGroup = this.openGroup('', true);
1042
					rightGroup.openEditor(editors[2], { active: true, pinned: true });
1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
				}

				this.storageService.remove(LEGACY_EDITOR_STATE_STORAGE_KEY, StorageScope.WORKSPACE);
			} catch (error) {
				console.warn('Unable to migrate previous editor state', error, legacyModelRaw);

				// Reset
				this._groups = [];
				this._activeGroup = void 0;
				this.groupToIdentifier = Object.create(null);
			}
1054 1055 1056
		}
	}

1057
	private doValidate(serialized: ISerializedEditorStacksModel): number {
B
Benjamin Pasero 已提交
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
		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
		}

1070
		if (serialized.groups.some(g => !g.editors.length)) {
B
Benjamin Pasero 已提交
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
			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;
	}

1089 1090 1091
	private doCreateGroup(arg1: string | ISerializedEditorGroup): EditorGroup {
		const group = this.instantiationService.createInstance(EditorGroup, arg1);

1092 1093
		this.groupToIdentifier[group.id] = group;

1094
		// Funnel editor changes in the group through our event aggregator
1095
		const unbind: IDisposable[] = [];
1096 1097
		unbind.push(group.onEditorsStructureChanged(editor => this._onModelChanged.fire({ group, editor, structural: true })));
		unbind.push(group.onEditorStateChanged(editor => this._onModelChanged.fire({ group, editor })));
1098 1099 1100 1101
		unbind.push(group.onEditorClosed(event => {
			this.handleOnEditorClosed(event);
			this._onEditorClosed.fire(event);
		}));
1102 1103 1104
		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 => {
1105
			if (g === group) {
1106
				dispose(unbind);
1107
			}
1108
		}));
1109 1110 1111 1112

		return group;
	}

1113
	private handleOnEditorClosed(event: GroupEvent): void {
1114 1115 1116 1117 1118 1119 1120 1121
		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 已提交
1122
				[editor.originalInput, editor.modifiedInput].forEach(editor => {
1123 1124 1125 1126 1127
					if (!this.isOpen(editor)) {
						editor.close();
					}
				});
			}
1128 1129 1130
		}
	}

1131 1132 1133 1134 1135 1136 1137 1138
	public isOpen(resource: URI): boolean;
	public isOpen(editor: EditorInput): boolean;
	public isOpen(arg1: any): boolean {
		if (arg1 instanceof EditorInput) {
			return this._groups.some(g => g.indexOf(arg1) >= 0);
		}

		return this._groups.some(group => group.contains(<URI>arg1));
1139 1140
	}

1141 1142 1143 1144 1145
	private onShutdown(): void {
		this.save();

		dispose(this.toDispose);
	}
1146

1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157
	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!');
		}
	}

1158
	public toString(): string {
B
Benjamin Pasero 已提交
1159 1160
		this.ensureLoaded();

1161 1162
		const lines: string[] = [];

B
Benjamin Pasero 已提交
1163 1164 1165 1166
		if (!this.groups.length) {
			return '<No Groups>';
		}

1167
		this.groups.forEach(g => {
B
Benjamin Pasero 已提交
1168 1169
			let label = `Group: ${g.label}`;

1170
			if (this._activeGroup === g) {
B
Benjamin Pasero 已提交
1171
				label = `${label} [active]`;
1172 1173
			}

B
Benjamin Pasero 已提交
1174 1175
			lines.push(label);

1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192
			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 已提交
1193
}