diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 280db81da30..d7ee25f928e 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -18,7 +18,6 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; /** * An editor input to be used for untitled text buffers. @@ -45,7 +44,6 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { @IInstantiationService private instantiationService: IInstantiationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IModeService private modeService: IModeService, - @IBackupFileService private backupFileService: IBackupFileService, @ITextFileService private textFileService: ITextFileService ) { super(); @@ -156,27 +154,14 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { return TPromise.as(this.cachedModel); } - // Otherwise Create Model and load, restoring from backup if necessary - return this.backupFileService.hasBackup(this.resource).then(hasBackup => { - if (hasBackup) { - const restoreResource = this.backupFileService.getBackupResource(this.resource); + // Otherwise Create Model and load + this.cachedModel = this.createModel(); - return this.textFileService.resolveTextContent(restoreResource).then(rawTextContent => rawTextContent.value.lines.join('\n')); - } - - return ''; - }).then(content => { - const model = this.createModel(content); - return model.load().then((resolvedModel: UntitledEditorModel) => { - this.cachedModel = resolvedModel; - - return this.cachedModel; - }); - }); + return this.cachedModel.load(); } - private createModel(content: string): UntitledEditorModel { - const model = this.instantiationService.createInstance(UntitledEditorModel, content, this.modeId, this.resource, this.hasAssociatedFilePath); + private createModel(): UntitledEditorModel { + const model = this.instantiationService.createInstance(UntitledEditorModel, this.modeId, this.resource, this.hasAssociatedFilePath); // re-emit some events from the model this.toUnbind.push(model.onDidChangeContent(() => this._onDidModelChangeContent.fire())); diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index b431eda418c..7d439df2b39 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -18,6 +18,8 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IMode } from 'vs/editor/common/modes'; import Event, { Emitter } from 'vs/base/common/event'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export class UntitledEditorModel extends StringEditorModel implements IEncodingSupport { @@ -39,18 +41,19 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS private hasAssociatedFilePath: boolean; constructor( - value: string, modeId: string, resource: URI, hasAssociatedFilePath: boolean, @IModeService modeService: IModeService, @IModelService modelService: IModelService, + @IBackupFileService private backupFileService: IBackupFileService, + @ITextFileService private textFileService: ITextFileService, @IConfigurationService private configurationService: IConfigurationService ) { - super(value, modeId, resource, modeService, modelService); + super('', modeId, resource, modeService, modelService); this.hasAssociatedFilePath = hasAssociatedFilePath; - this.dirty = hasAssociatedFilePath || !!value; // untitled associated to file path are dirty right away as well as untitled with content + this.dirty = false; this._onDidChangeContent = new Emitter(); this._onDidChangeDirty = new Emitter(); @@ -59,13 +62,6 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS this.contentChangeEventScheduler = new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY); this.registerListeners(); - - // Indicate dirty state to listeners - if (this.dirty) { - setTimeout(() => { - this._onDidChangeDirty.fire(); - }, 0 /* prevent race condition between creating input and emitting dirty event */); - } } public get onDidChangeContent(): Event { @@ -132,31 +128,53 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS return this.dirty; } + private setDirty(dirty: boolean): void { + if (this.dirty === dirty) { + return; + } + + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + public getResource(): URI { return this.resource; } public revert(): void { - this.dirty = false; - - // Events - this._onDidChangeDirty.fire(); + this.setDirty(false); // Handle content change event buffered this.contentChangeEventScheduler.schedule(); } public load(): TPromise { - return super.load().then((model) => { - const configuration = this.configurationService.getConfiguration(); - // Encoding - this.configuredEncoding = configuration && configuration.files && configuration.files.encoding; + // Check for backups first + return this.backupFileService.hasBackup(this.resource).then(hasBackup => { + if (hasBackup) { + return this.textFileService.resolveTextContent(this.backupFileService.getBackupResource(this.resource)).then(rawTextContent => rawTextContent.value.lines.join('\n')); + } - // Listen to content changes - this.textModelChangeListener = this.textEditorModel.onDidChangeContent(e => this.onModelContentChanged()); + return null; + }).then(backupContent => { + if (backupContent) { + this.setValue(backupContent); + } - return model; + this.setDirty(this.hasAssociatedFilePath || !!backupContent); // untitled associated to file path are dirty right away as well as untitled with content + + return super.load().then(model => { + const configuration = this.configurationService.getConfiguration(); + + // Encoding + this.configuredEncoding = configuration && configuration.files && configuration.files.encoding; + + // Listen to content changes + this.textModelChangeListener = this.textEditorModel.onDidChangeContent(e => this.onModelContentChanged()); + + return model; + }); }); } @@ -165,16 +183,12 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS // mark the untitled editor as non-dirty once its content becomes empty and we do // not have an associated path set. we never want dirty indicator in that case. if (!this.hasAssociatedFilePath && this.textEditorModel.getLineCount() === 1 && this.textEditorModel.getLineContent(1) === '') { - if (this.dirty) { - this.dirty = false; - this._onDidChangeDirty.fire(); - } + this.setDirty(false); } - // turn dirty if we were not - else if (!this.dirty) { - this.dirty = true; - this._onDidChangeDirty.fire(); + // turn dirty otherwise + else { + this.setDirty(true); } // Handle content change event buffered diff --git a/src/vs/workbench/parts/backup/common/backupRestorer.ts b/src/vs/workbench/parts/backup/common/backupRestorer.ts index c3a1fbe566f..e5ad4cca66d 100644 --- a/src/vs/workbench/parts/backup/common/backupRestorer.ts +++ b/src/vs/workbench/parts/backup/common/backupRestorer.ts @@ -5,12 +5,16 @@ 'use strict'; +import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import errors = require('vs/base/common/errors'); +import { IBackupService, IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { FileEditorInput } from 'vs/workbench/parts/files/common/files'; // TODO@ben TODO@tyriar this should restore any backup that exists on disk and not rely // on the editors to be restored already in the stacks model. For that a method is needed @@ -22,7 +26,10 @@ export class BackupRestorer implements IWorkbenchContribution { constructor( @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IEnvironmentService private environmentService: IEnvironmentService, - @IPartService private partService: IPartService + @IPartService private partService: IPartService, + @IBackupService private backupService: IBackupService, + @IBackupFileService private backupFileService: IBackupFileService, + @IEditorGroupService private groupService: IEditorGroupService ) { if (!this.environmentService.isExtensionDevelopment) { this.restoreBackups(); @@ -36,6 +43,27 @@ export class BackupRestorer implements IWorkbenchContribution { // Resolve all untitled so that their backups get loaded TPromise.join(this.untitledEditorService.getAll().map(untitled => untitled.resolve())).done(null, errors.onUnexpectedError); + + // TODO@Ben enable generally once we can get a list of backups quickly + if (this.backupService.isHotExitEnabled) { + const fileResources: { [resource: string]: FileEditorInput } = Object.create(null); + this.groupService.getStacksModel().groups.forEach(group => { + const editors = group.getEditors(); + editors.forEach(editor => { + if (editor instanceof FileEditorInput) { + fileResources[editor.getResource().toString()] = editor; + } + }); + }); + + TPromise.join(Object.keys(fileResources).map(resource => { + return this.backupFileService.hasBackup(URI.parse(resource)).then(hasBackup => { + if (hasBackup) { + return fileResources[resource].resolve(); + } + }); + })).done(null, errors.onUnexpectedError); + } }); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 8e8b3f96617..62926fe1b31 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -298,7 +298,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.createTextEditorModel(fileContent, content.resource).then(() => { this.createTextEditorModelPromise = null; - this.setDirty(backupExists); // Ensure we are not tracking a stale state + if (backupExists) { + this.makeDirty(); + } else { + this.setDirty(false); // Ensure we are not tracking a stale state + } + this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); return this; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 9664e242f17..53879b2dda3 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -212,32 +212,8 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { model = this.instantiationService.createInstance(TextFileEditorModel, resource, encoding); modelPromise = model.load(); - // Install state change listener - this.mapResourceToStateChangeListener[resource.toString()] = model.onDidStateChange(state => { - const event = new TextFileModelChangeEvent(model, state); - switch (state) { - case StateChange.DIRTY: - this._onModelDirty.fire(event); - break; - case StateChange.SAVE_ERROR: - this._onModelSaveError.fire(event); - break; - case StateChange.SAVED: - this._onModelSaved.fire(event); - break; - case StateChange.REVERTED: - this._onModelReverted.fire(event); - break; - case StateChange.ENCODING: - this._onModelEncodingChanged.fire(event); - break; - } - }); - - // Install model content change listener - this.mapResourceToModelContentChangeListener[resource.toString()] = model.onDidContentChange(e => { - this._onModelContentChanged.fire(new TextFileModelChangeEvent(model, e)); - }); + // Make known to manager (if not already known) + this.add(resource, model); } // Store pending loads to avoid race conditions @@ -245,9 +221,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return modelPromise.then(model => { - // Make known to manager (if not already known) - this.add(resource, model); - // Remove from pending loads this.mapResourceToPendingModelLoaders[resource.toString()] = null; @@ -276,6 +249,33 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return; // already cached } + // Install state change listener + this.mapResourceToStateChangeListener[resource.toString()] = model.onDidStateChange(state => { + const event = new TextFileModelChangeEvent(model, state); + switch (state) { + case StateChange.DIRTY: + this._onModelDirty.fire(event); + break; + case StateChange.SAVE_ERROR: + this._onModelSaveError.fire(event); + break; + case StateChange.SAVED: + this._onModelSaved.fire(event); + break; + case StateChange.REVERTED: + this._onModelReverted.fire(event); + break; + case StateChange.ENCODING: + this._onModelEncodingChanged.fire(event); + break; + } + }); + + // Install model content change listener + this.mapResourceToModelContentChangeListener[resource.toString()] = model.onDidContentChange(e => { + this._onModelContentChanged.fire(new TextFileModelChangeEvent(model, e)); + }); + // dispose any previously stored dispose listener for this resource const disposeListener = this.mapResourceToDisposeListener[resource.toString()]; if (disposeListener) { diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts index 561e8d8bb59..1f12cda9f4d 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts @@ -10,7 +10,6 @@ import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { EditorModel } from 'vs/workbench/common/editor'; import { join, basename } from 'vs/base/common/paths'; import { workbenchInstantiationService, TestEditorGroupService, createFileInput, onError } from 'vs/test/utils/servicesTestUtils'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; @@ -57,9 +56,9 @@ suite('Files - TextFileEditorModelManager', () => { test('add, remove, clear, get, getAll', function () { const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); - const model1 = new EditorModel(); - const model2 = new EditorModel(); - const model3 = new EditorModel(); + const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random1.txt'), 'utf8'); + const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random2.txt'), 'utf8'); + const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random3.txt'), 'utf8'); manager.add(URI.file('/test.html'), model1); manager.add(URI.file('/some/other.html'), model2); @@ -93,6 +92,10 @@ suite('Files - TextFileEditorModelManager', () => { manager.clear(); result = manager.getAll(); assert.strictEqual(0, result.length); + + model1.dispose(); + model2.dispose(); + model3.dispose(); }); test('loadOrCreate', function (done) { @@ -125,9 +128,9 @@ suite('Files - TextFileEditorModelManager', () => { test('removed from cache when model disposed', function () { const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); - const model1 = new EditorModel(); - const model2 = new EditorModel(); - const model3 = new EditorModel(); + const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random1.txt'), 'utf8'); + const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random2.txt'), 'utf8'); + const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random3.txt'), 'utf8'); manager.add(URI.file('/test.html'), model1); manager.add(URI.file('/some/other.html'), model2); @@ -137,6 +140,9 @@ suite('Files - TextFileEditorModelManager', () => { model1.dispose(); assert(!manager.get(URI.file('/test.html'))); + + model2.dispose(); + model3.dispose(); }); test('disposes model when not open anymore', function () {