From b872e3dea1facc41e79fd03d97edd1ee31f7083a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 26 Aug 2016 12:48:21 +0200 Subject: [PATCH] Undo stack lost (fixes #10932) --- .../parts/files/browser/fileTracker.ts | 51 +++++++++---------- .../common/editors/textFileEditorModel.ts | 36 ++++++------- .../test/browser/fileEditorModel.test.ts | 24 +-------- 3 files changed, 46 insertions(+), 65 deletions(-) diff --git a/src/vs/workbench/parts/files/browser/fileTracker.ts b/src/vs/workbench/parts/files/browser/fileTracker.ts index 8e5b1edf3a921..2262a1f16143d 100644 --- a/src/vs/workbench/parts/files/browser/fileTracker.ts +++ b/src/vs/workbench/parts/files/browser/fileTracker.ts @@ -149,7 +149,7 @@ export class FileTracker implements IWorkbenchContribution { } private updateActivityBadge(): void { - let dirtyCount = this.textFileService.getDirty().length; + const dirtyCount = this.textFileService.getDirty().length; this.lastDirtyCount = dirtyCount; if (dirtyCount > 0) { this.activityService.showActivity(VIEWLET_ID, new NumberBadge(dirtyCount, num => nls.localize('dirtyFiles', "{0} unsaved files", dirtyCount)), 'explorer-viewlet-label'); @@ -166,15 +166,15 @@ export class FileTracker implements IWorkbenchContribution { // Handle moves specially when file is opened if (e.gotMoved()) { - let before = e.getBefore(); - let after = e.getAfter(); + const before = e.getBefore(); + const after = e.getAfter(); this.handleMovedFileInOpenedEditors(before ? before.resource : null, after ? after.resource : null, after ? after.mime : null); } // Dispose all known inputs passed on resource if deleted or moved - let oldFile = e.getBefore(); - let movedTo = e.gotMoved() && e.getAfter() && e.getAfter().resource; + const oldFile = e.getBefore(); + const movedTo = e.gotMoved() && e.getAfter() && e.getAfter().resource; if (e.gotMoved() || e.gotDeleted()) { this.handleDeleteOrMove(oldFile.resource, movedTo); } @@ -183,7 +183,7 @@ export class FileTracker implements IWorkbenchContribution { private onFileChanges(e: FileChangesEvent): void { // Dispose inputs that got deleted - let allDeleted = e.getDeleted(); + const allDeleted = e.getDeleted(); if (allDeleted && allDeleted.length > 0) { allDeleted.forEach(deleted => { this.handleDeleteOrMove(deleted.resource); @@ -195,12 +195,12 @@ export class FileTracker implements IWorkbenchContribution { e.getUpdated() .map(u => CACHE.get(u.resource)) .filter(model => { - let canDispose = this.canDispose(model); + const canDispose = this.canDispose(model); if (!canDispose) { return false; } - if (Date.now() - model.getLastDirtyTime() < FileTracker.FILE_CHANGE_UPDATE_DELAY) { + if (Date.now() - model.getLastSaveTime() < FileTracker.FILE_CHANGE_UPDATE_DELAY) { return false; // this is a weak check to see if the change came from outside the editor or not } @@ -209,7 +209,7 @@ export class FileTracker implements IWorkbenchContribution { .forEach(model => CACHE.dispose(model.getResource())); // Update inputs that got updated - let editors = this.editorService.getVisibleEditors(); + const editors = this.editorService.getVisibleEditors(); editors.forEach(editor => { let input = editor.input; if (input instanceof DiffEditorInput) { @@ -218,28 +218,27 @@ export class FileTracker implements IWorkbenchContribution { // File Editor Input if (input instanceof FileEditorInput) { - let fileInput = input; - let fileInputResource = fileInput.getResource(); + const fileInput = input; + const fileInputResource = fileInput.getResource(); // Input got added or updated, so check for model and update // Note: we also consider the added event because it could be that a file was added // and updated right after. if (e.contains(fileInputResource, FileChangeType.UPDATED) || e.contains(fileInputResource, FileChangeType.ADDED)) { - let textModel = CACHE.get(fileInputResource); + const textModel = CACHE.get(fileInputResource); - // Text file: check for last dirty time + // Text file: check for last save time if (textModel) { - let state = textModel.getState(); // We only ever update models that are in good saved state - if (state === ModelState.SAVED) { - let lastDirtyTime = textModel.getLastDirtyTime(); + if (textModel.getState() === ModelState.SAVED) { + const lastSaveTime = textModel.getLastSaveTime(); // Force a reopen of the input if this change came in later than our wait interval before we consider it - if (Date.now() - lastDirtyTime > FileTracker.FILE_CHANGE_UPDATE_DELAY) { - let codeEditor = (editor).getControl(); - let viewState = codeEditor.saveViewState(); - let currentMtime = textModel.getLastModifiedTime(); // optimize for the case where the file did actually not change + if (Date.now() - lastSaveTime > FileTracker.FILE_CHANGE_UPDATE_DELAY) { + const codeEditor = (editor).getControl(); + const viewState = codeEditor.saveViewState(); + const currentMtime = textModel.getLastModifiedTime(); // optimize for the case where the file did actually not change textModel.load().done(() => { if (textModel.getLastModifiedTime() !== currentMtime && this.isEditorShowingPath(editor, textModel.getResource())) { codeEditor.restoreViewState(viewState); @@ -292,7 +291,7 @@ export class FileTracker implements IWorkbenchContribution { if (oldResource.toString() === resource.toString()) { reopenFileResource = newResource; // file got moved } else { - let index = resource.fsPath.indexOf(oldResource.fsPath); + const index = resource.fsPath.indexOf(oldResource.fsPath); reopenFileResource = URI.file(paths.join(newResource.fsPath, resource.fsPath.substr(index + oldResource.fsPath.length + 1))); // parent folder got moved } @@ -310,8 +309,8 @@ export class FileTracker implements IWorkbenchContribution { private getMatchingFileEditorInputFromDiff(input: DiffEditorInput, arg: any): FileEditorInput { // First try modifiedInput - let modifiedInput = input.modifiedInput; - let res = this.getMatchingFileEditorInputFromInput(modifiedInput, arg); + const modifiedInput = input.modifiedInput; + const res = this.getMatchingFileEditorInputFromInput(modifiedInput, arg); if (res) { return res; } @@ -325,12 +324,12 @@ export class FileTracker implements IWorkbenchContribution { private getMatchingFileEditorInputFromInput(input: EditorInput, arg: any): FileEditorInput { if (input instanceof FileEditorInput) { if (arg instanceof URI) { - let deletedResource = arg; + const deletedResource = arg; if (this.containsResource(input, deletedResource)) { return input; } } else { - let updatedFiles = arg; + const updatedFiles = arg; if (updatedFiles.contains(input.getResource(), FileChangeType.UPDATED)) { return input; } @@ -346,7 +345,7 @@ export class FileTracker implements IWorkbenchContribution { } // Add existing clients matching resource - let inputsContainingPath: EditorInput[] = FileEditorInput.getAll(resource); + const inputsContainingPath: EditorInput[] = FileEditorInput.getAll(resource); // Collect from history and opened editors and see which ones to pick const candidates = this.historyService.getHistory(); diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts index b61118ced2e1f..1d64d816920ff 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts @@ -80,7 +80,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin private disposed: boolean; private inConflictResolutionMode: boolean; private inErrorMode: boolean; - private lastDirtyTime: number; + private lastSaveTime: number; private createTextEditorModelPromise: TPromise; constructor( @@ -107,7 +107,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin this.dirty = false; this.autoSavePromises = []; this.versionId = 0; - this.lastDirtyTime = 0; + this.lastSaveTime = 0; this.mapPendingSaveToVersionId = {}; this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration()); @@ -164,7 +164,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin this.cancelAutoSavePromises(); // Unset flags - let undo = this.setDirty(false); + const undo = this.setDirty(false); // Reload return this.load(true /* force */).then(() => { @@ -215,7 +215,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin this.telemetryService.publicLog('fileGet', { mimeType: content.mime, ext: paths.extname(this.resource.fsPath), path: anonymize(this.resource.fsPath) }); // Update our resolved disk stat model - let resolvedStat: IFileStat = { + const resolvedStat: IFileStat = { resource: this.resource, name: content.name, mtime: content.mtime, @@ -228,7 +228,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin this.updateVersionOnDiskStat(resolvedStat); // Keep the original encoding to not loose it when saving - let oldEncoding = this.contentEncoding; + const oldEncoding = this.contentEncoding; this.contentEncoding = content.encoding; // Handle events if encoding changed @@ -347,9 +347,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin private makeDirty(e?: IModelContentChangedEvent): void { // Track dirty state and version id - let wasDirty = this.dirty; + const wasDirty = this.dirty; this.setDirty(true); - this.lastDirtyTime = Date.now(); // Emit as Event if we turned dirty if (!wasDirty) { @@ -364,7 +363,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin this.cancelAutoSavePromises(); // Create new save promise and keep it - let promise: TPromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => { + const promise: TPromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => { // Only trigger save if the version id has not changed meanwhile if (versionId === this.versionId) { @@ -403,7 +402,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin diag('doSave(' + versionId + ') - enter with versionId ' + versionId, this.resource, new Date()); // Lookup any running pending save for this versionId and return it if found - let pendingSave = this.mapPendingSaveToVersionId[versionId]; + const pendingSave = this.mapPendingSaveToVersionId[versionId]; if (pendingSave) { diag('doSave(' + versionId + ') - exit - found a pending save for versionId ' + versionId, this.resource, new Date()); @@ -461,6 +460,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin }).then((stat: IFileStat) => { diag('doSave(' + versionId + ') - after updateContent()', this.resource, new Date()); + // Remember when this model was saved last + this.lastSaveTime = Date.now(); + // Telemetry this.telemetryService.publicLog('filePUT', { mimeType: stat.mime, ext: paths.extname(this.versionOnDiskStat.resource.fsPath) }); @@ -500,10 +502,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin } private setDirty(dirty: boolean): () => void { - let wasDirty = this.dirty; - let wasInConflictResolutionMode = this.inConflictResolutionMode; - let wasInErrorMode = this.inErrorMode; - let oldBufferSavedVersionId = this.bufferSavedVersionId; + const wasDirty = this.dirty; + const wasInConflictResolutionMode = this.inConflictResolutionMode; + const wasInErrorMode = this.inErrorMode; + const oldBufferSavedVersionId = this.bufferSavedVersionId; if (!dirty) { this.dirty = false; @@ -578,10 +580,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin } /** - * Returns the time in millies when this working copy was edited by the user. + * Returns the time in millies when this working copy was saved by the user. */ - public getLastDirtyTime(): number { - return this.lastDirtyTime; + public getLastSaveTime(): number { + return this.lastSaveTime; } /** @@ -729,7 +731,7 @@ export class TextFileEditorModelCache { } public dispose(resource: URI): void { - let model = this.get(resource); + const model = this.get(resource); if (model) { if (model.isDirty()) { return; // we never dispose dirty models to avoid data loss diff --git a/src/vs/workbench/parts/files/test/browser/fileEditorModel.test.ts b/src/vs/workbench/parts/files/test/browser/fileEditorModel.test.ts index 6d7b44579d68f..bc95c0d50f448 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorModel.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorModel.test.ts @@ -185,28 +185,6 @@ suite('Files - TextFileEditorModel', () => { }); }); - test('Dirty tracking', function (done) { - let resource = toResource('/path/index_async.txt'); - let i1 = instantiationService.createInstance(FileEditorInput, resource, 'text/plain', 'utf8'); - - i1.resolve().then((m1: TextFileEditorModel) => { - let dirty = m1.getLastDirtyTime(); - assert.ok(!dirty); - - m1.textEditorModel.setValue('foo'); - - assert.ok(m1.isDirty()); - assert.ok(m1.getLastDirtyTime() > dirty); - - assert.ok(textFileService.isDirty(resource)); - assert.equal(textFileService.getDirty().length, 1); - - m1.dispose(); - - done(); - }); - }); - test('save() and isDirty() - proper with check for mtimes', function (done) { let c1 = instantiationService.createInstance(FileEditorInput, toResource('/path/index_async2.txt'), 'text/plain', 'utf8'); let c2 = instantiationService.createInstance(FileEditorInput, toResource('/path/index_async.txt'), 'text/plain', 'utf8'); @@ -233,6 +211,8 @@ suite('Files - TextFileEditorModel', () => { assert.ok(!textFileService.isDirty(toResource('/path/index_async2.txt'))); assert.ok(m1.getLastModifiedTime() > m1Mtime); assert.ok(m2.getLastModifiedTime() > m2Mtime); + assert.ok(m1.getLastSaveTime() > m1Mtime); + assert.ok(m2.getLastSaveTime() > m2Mtime); m1.dispose(); m2.dispose();