Skip to content

Commit

Permalink
Undo stack lost (fixes #10932)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed Aug 26, 2016
1 parent ecdfedb commit b872e3d
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 65 deletions.
51 changes: 25 additions & 26 deletions src/vs/workbench/parts/files/browser/fileTracker.ts
Expand Up @@ -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');
Expand All @@ -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);
}
Expand All @@ -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);
Expand All @@ -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
}

Expand All @@ -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) {
Expand All @@ -218,28 +218,27 @@ export class FileTracker implements IWorkbenchContribution {

// File Editor Input
if (input instanceof FileEditorInput) {
let fileInput = <FileEditorInput>input;
let fileInputResource = fileInput.getResource();
const fileInput = <FileEditorInput>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 = (<BaseTextEditor>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 = (<BaseTextEditor>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(<BaseEditor>editor, textModel.getResource())) {
codeEditor.restoreViewState(viewState);
Expand Down Expand Up @@ -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
}

Expand All @@ -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;
}
Expand All @@ -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 = <URI>arg;
const deletedResource = <URI>arg;
if (this.containsResource(input, deletedResource)) {
return input;
}
} else {
let updatedFiles = <FileChangesEvent>arg;
const updatedFiles = <FileChangesEvent>arg;
if (updatedFiles.contains(input.getResource(), FileChangeType.UPDATED)) {
return input;
}
Expand All @@ -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();
Expand Down
36 changes: 19 additions & 17 deletions src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts
Expand Up @@ -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<TextFileEditorModel>;

constructor(
Expand All @@ -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());
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -364,7 +363,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements IEncodin
this.cancelAutoSavePromises();

// Create new save promise and keep it
let promise: TPromise<void> = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
const promise: TPromise<void> = TPromise.timeout(this.autoSaveAfterMillies).then(() => {

// Only trigger save if the version id has not changed meanwhile
if (versionId === this.versionId) {
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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) });

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
Expand Down
24 changes: 2 additions & 22 deletions src/vs/workbench/parts/files/test/browser/fileEditorModel.test.ts
Expand Up @@ -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');
Expand All @@ -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();
Expand Down

0 comments on commit b872e3d

Please sign in to comment.