提交 44f6625c 编写于 作者: B Benjamin Pasero

Change -> Undo no longer marks document non-dirty (fix #90973)

上级 77793108
......@@ -249,13 +249,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
//#region Load
async load(options?: ITextFileLoadOptions): Promise<TextFileEditorModel> {
this.logService.trace('[text file model] load() - enter', this.resource.toString());
this.logService.trace('[text file model] load() - enter', this.resource.toString(true));
// It is very important to not reload the model when the model is dirty.
// We also only want to reload the model from the disk if no save is pending
// to avoid data loss.
if (this.dirty || this.saveSequentializer.hasPending()) {
this.logService.trace('[text file model] load() - exit - without loading because model is dirty or being saved', this.resource.toString());
this.logService.trace('[text file model] load() - exit - without loading because model is dirty or being saved', this.resource.toString(true));
return this;
}
......@@ -359,7 +359,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private loadFromContent(content: ITextFileStreamContent, options?: ITextFileLoadOptions, fromBackup?: boolean): TextFileEditorModel {
this.logService.trace('[text file model] load() - resolved content', this.resource.toString());
this.logService.trace('[text file model] load() - resolved content', this.resource.toString(true));
// Update our resolved disk stat model
this.updateLastResolvedFileStat({
......@@ -395,6 +395,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.doCreateTextModel(content.resource, content.value, !!fromBackup);
}
// Ensure we track the latest saved version ID
this.updateSavedVersionId();
// Emit as event
this._onDidLoad.fire(options?.reason ?? TextFileLoadReason.OTHER);
......@@ -402,7 +405,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private doCreateTextModel(resource: URI, value: ITextBufferFactory, fromBackup: boolean): void {
this.logService.trace('[text file model] load() - created text editor model', this.resource.toString());
this.logService.trace('[text file model] load() - created text editor model', this.resource.toString(true));
// Create model
const textModel = this.createTextEditorModel(value, resource, this.preferredMode);
......@@ -417,7 +420,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private doUpdateTextModel(value: ITextBufferFactory): void {
this.logService.trace('[text file model] load() - updated text editor model', this.resource.toString());
this.logService.trace('[text file model] load() - updated text editor model', this.resource.toString(true));
// Update model value in a block that ignores content change events for dirty tracking
this.ignoreDirtyOnModelContentChange = true;
......@@ -426,9 +429,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
} finally {
this.ignoreDirtyOnModelContentChange = false;
}
// Ensure we track the latest saved version ID given that the contents changed
this.updateSavedVersionId();
}
private installModelListeners(model: ITextModel): void {
......@@ -442,11 +442,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private onModelContentChanged(model: ITextModel): void {
this.logService.trace(`[text file model] onModelContentChanged() - enter`, this.resource.toString());
this.logService.trace(`[text file model] onModelContentChanged() - enter`, this.resource.toString(true));
// In any case increment the version id because it tracks the textual content state of the model at all times
this.versionId++;
this.logService.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`, this.resource.toString());
this.logService.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`, this.resource.toString(true));
// We mark check for a dirty-state change upon model content change, unless:
// - explicitly instructed to ignore it (e.g. from model.load())
......@@ -456,7 +456,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// The contents changed as a matter of Undo and the version reached matches the saved one
// In this case we clear the dirty flag and emit a SAVED event to indicate this state.
if (model.getAlternativeVersionId() === this.bufferSavedVersionId) {
this.logService.trace('[text file model] onModelContentChanged() - model content changed back to last saved version', this.resource.toString());
this.logService.trace('[text file model] onModelContentChanged() - model content changed back to last saved version', this.resource.toString(true));
// Clear flags
const wasDirty = this.dirty;
......@@ -470,7 +470,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Otherwise the content has changed and we signal this as becoming dirty
else {
this.logService.trace('[text file model] onModelContentChanged() - model content changed and marked as dirty', this.resource.toString());
this.logService.trace('[text file model] onModelContentChanged() - model content changed and marked as dirty', this.resource.toString(true));
// Mark as dirty
this.setDirty(true);
......@@ -538,7 +538,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
if (this.isReadonly()) {
this.logService.trace('[text file model] save() - ignoring request for readonly resource', this.resource.toString());
this.logService.trace('[text file model] save() - ignoring request for readonly resource', this.resource.toString(true));
return false; // if model is readonly we do not attempt to save at all
}
......@@ -547,15 +547,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
(this.hasState(TextFileEditorModelState.CONFLICT) || this.hasState(TextFileEditorModelState.ERROR)) &&
(options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)
) {
this.logService.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error', this.resource.toString());
this.logService.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error', this.resource.toString(true));
return false; // if model is in save conflict or error, do not save unless save reason is explicit
}
// Actually do save and log
this.logService.trace('[text file model] save() - enter', this.resource.toString());
this.logService.trace('[text file model] save() - enter', this.resource.toString(true));
await this.doSave(options);
this.logService.trace('[text file model] save() - exit', this.resource.toString());
this.logService.trace('[text file model] save() - exit', this.resource.toString(true));
return true;
}
......@@ -566,7 +566,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
let versionId = this.versionId;
this.logService.trace(`[text file model] doSave(${versionId}) - enter with versionId ${versionId}`, this.resource.toString());
this.logService.trace(`[text file model] doSave(${versionId}) - enter with versionId ${versionId}`, this.resource.toString(true));
// Lookup any running pending save for this versionId and return it if found
//
......@@ -574,7 +574,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// while the save was not yet finished to disk
//
if (this.saveSequentializer.hasPending(versionId)) {
this.logService.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource.toString());
this.logService.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource.toString(true));
return this.saveSequentializer.pending;
}
......@@ -583,7 +583,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
//
// Scenario: user invoked save action even though the model is not dirty
if (!options.force && !this.dirty) {
this.logService.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource.toString());
this.logService.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource.toString(true));
return;
}
......@@ -597,7 +597,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// while the first save has not returned yet.
//
if (this.saveSequentializer.hasPending()) {
this.logService.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`, this.resource.toString());
this.logService.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`, this.resource.toString(true));
// Indicate to the save sequentializer that we want to
// cancel the pending operation so that ours can run
......@@ -629,7 +629,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
try {
await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token);
} catch (error) {
this.logService.error(`[text file model] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString());
this.logService.error(`[text file model] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true));
}
}
......@@ -680,7 +680,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Save to Disk. We mark the save operation as currently pending with
// the latest versionId because it might have changed from a save
// participant triggering
this.logService.trace(`[text file model] doSave(${versionId}) - before write()`, this.resource.toString());
this.logService.trace(`[text file model] doSave(${versionId}) - before write()`, this.resource.toString(true));
const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat);
const textFileEdiorModel = this;
return this.saveSequentializer.setPending(versionId, (async () => {
......@@ -703,17 +703,17 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void {
this.logService.trace(`[text file model] doSave(${versionId}) - after write()`, this.resource.toString());
this.logService.trace(`[text file model] doSave(${versionId}) - after write()`, this.resource.toString(true));
// Updated resolved stat with updated stat
this.updateLastResolvedFileStat(stat);
// Update dirty state unless model has changed meanwhile
if (versionId === this.versionId) {
this.logService.trace(`[text file model] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`, this.resource.toString());
this.logService.trace(`[text file model] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`, this.resource.toString(true));
this.setDirty(false);
} else {
this.logService.trace(`[text file model] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource.toString());
this.logService.trace(`[text file model] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource.toString(true));
}
// Emit Save Event
......@@ -721,7 +721,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private handleSaveError(error: Error, versionId: number, options: ITextFileSaveOptions): void {
this.logService.error(`[text file model] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString());
this.logService.error(`[text file model] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true));
// Return early if the save() call was made asking to
// handle the save error itself.
......
......@@ -17,6 +17,7 @@ import { timeout } from 'vs/base/common/async';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { assertIsDefined } from 'vs/base/common/types';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
class ServiceAccessor {
constructor(
......@@ -74,12 +75,12 @@ suite('Files - TextFileEditorModel', () => {
let onDidChangeDirtyCounter = 0;
model.onDidChangeDirty(() => onDidChangeDirtyCounter++);
model.textEditorModel?.setValue('bar');
model.updateTextEditorModel(createTextBufferFactory('bar'));
assert.equal(onDidChangeContentCounter, 1);
assert.equal(onDidChangeDirtyCounter, 1);
model.textEditorModel?.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
assert.equal(onDidChangeContentCounter, 2);
assert.equal(onDidChangeDirtyCounter, 1);
......@@ -98,7 +99,7 @@ suite('Files - TextFileEditorModel', () => {
assert.equal(accessor.workingCopyService.dirtyCount, 0);
model.textEditorModel!.setValue('bar');
model.updateTextEditorModel(createTextBufferFactory('bar'));
assert.ok(getLastModifiedTime(model) <= Date.now());
assert.ok(model.hasState(TextFileEditorModelState.DIRTY));
......@@ -161,7 +162,7 @@ suite('Files - TextFileEditorModel', () => {
await model.load();
model.textEditorModel!.setValue('bar');
model.updateTextEditorModel(createTextBufferFactory('bar'));
let saveErrorEvent = false;
model.onDidSaveError(e => saveErrorEvent = true);
......@@ -191,7 +192,7 @@ suite('Files - TextFileEditorModel', () => {
await model.load();
model.textEditorModel!.setValue('bar');
model.updateTextEditorModel(createTextBufferFactory('bar'));
let saveErrorEvent = false;
model.onDidSaveError(e => saveErrorEvent = true);
......@@ -288,7 +289,7 @@ suite('Files - TextFileEditorModel', () => {
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
assert.ok(model.isDirty());
assert.ok(model.hasState(TextFileEditorModelState.DIRTY));
......@@ -312,7 +313,7 @@ suite('Files - TextFileEditorModel', () => {
});
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
assert.ok(model.isDirty());
assert.equal(accessor.workingCopyService.dirtyCount, 1);
......@@ -345,7 +346,7 @@ suite('Files - TextFileEditorModel', () => {
});
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
assert.ok(model.isDirty());
assert.equal(accessor.workingCopyService.dirtyCount, 1);
......@@ -363,6 +364,16 @@ suite('Files - TextFileEditorModel', () => {
model.dispose();
});
test('Undo to saved state turns model non-dirty', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
model.updateTextEditorModel(createTextBufferFactory('Hello Text'));
assert.ok(model.isDirty());
model.textEditorModel!.undo();
assert.ok(!model.isDirty());
});
test('Load and undo turns model dirty', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
......@@ -385,7 +396,7 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(!model.isDirty()); // needs to be resolved
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
assert.ok(model.isDirty());
await model.revert({ soft: true });
......@@ -423,7 +434,7 @@ suite('Files - TextFileEditorModel', () => {
const model = instantiationService.createInstance(TestTextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
assert.ok(!model.isDirty());
await model.revert({ soft: true });
......@@ -467,7 +478,7 @@ suite('Files - TextFileEditorModel', () => {
const model1 = await input1.resolve() as TextFileEditorModel;
const model2 = await input2.resolve() as TextFileEditorModel;
model1.textEditorModel!.setValue('foo');
model1.updateTextEditorModel(createTextBufferFactory('foo'));
const m1Mtime = assertIsDefined(model1.getStat()).mtime;
const m2Mtime = assertIsDefined(model2.getStat()).mtime;
......@@ -477,7 +488,7 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt')));
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
model2.textEditorModel!.setValue('foo');
model2.updateTextEditorModel(createTextBufferFactory('foo'));
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
await timeout(10);
......@@ -497,7 +508,7 @@ suite('Files - TextFileEditorModel', () => {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
model.onDidSave(e => {
assert.equal(snapshotToString(model.createSnapshot()!), 'bar');
assert.equal(snapshotToString(model.createSnapshot()!), eventCounter === 1 ? 'bar' : 'foobar');
assert.ok(!model.isDirty());
eventCounter++;
});
......@@ -505,20 +516,22 @@ suite('Files - TextFileEditorModel', () => {
const participant = accessor.textFileService.files.addSaveParticipant({
participate: async model => {
assert.ok(model.isDirty());
model.textEditorModel!.setValue('bar');
(model as TextFileEditorModel).updateTextEditorModel(createTextBufferFactory('bar'));
assert.ok(model.isDirty());
eventCounter++;
}
});
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
assert.ok(model.isDirty());
await model.save();
assert.equal(eventCounter, 2);
participant.dispose();
model.textEditorModel!.setValue('bar');
model.updateTextEditorModel(createTextBufferFactory('foobar'));
assert.ok(model.isDirty());
await model.save();
assert.equal(eventCounter, 3);
......@@ -537,7 +550,7 @@ suite('Files - TextFileEditorModel', () => {
});
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
await model.save({ skipSaveParticipants: true });
assert.equal(eventCounter, 0);
......@@ -558,7 +571,7 @@ suite('Files - TextFileEditorModel', () => {
const participant = accessor.textFileService.files.addSaveParticipant({
participate: model => {
assert.ok(model.isDirty());
model.textEditorModel!.setValue('bar');
(model as TextFileEditorModel).updateTextEditorModel(createTextBufferFactory('bar'));
assert.ok(model.isDirty());
eventCounter++;
......@@ -567,7 +580,7 @@ suite('Files - TextFileEditorModel', () => {
});
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
const now = Date.now();
await model.save();
......@@ -588,7 +601,7 @@ suite('Files - TextFileEditorModel', () => {
});
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
await model.save();
......@@ -613,16 +626,16 @@ suite('Files - TextFileEditorModel', () => {
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
const p1 = model.save();
model.textEditorModel!.setValue('foo 1');
model.updateTextEditorModel(createTextBufferFactory('foo 1'));
const p2 = model.save();
model.textEditorModel!.setValue('foo 2');
model.updateTextEditorModel(createTextBufferFactory('foo 2'));
const p3 = model.save();
model.textEditorModel!.setValue('foo 3');
model.updateTextEditorModel(createTextBufferFactory('foo 3'));
const p4 = model.save();
await Promise.all([p1, p2, p3, p4]);
......@@ -671,7 +684,7 @@ suite('Files - TextFileEditorModel', () => {
});
await model.load();
model.textEditorModel!.setValue('foo');
model.updateTextEditorModel(createTextBufferFactory('foo'));
savePromise = model.save();
await savePromise;
......
......@@ -14,6 +14,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { toResource } from 'vs/base/test/common/utils';
import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
class ServiceAccessor {
constructor(
......@@ -198,11 +199,11 @@ suite('Files - TextFileEditorModelManager', () => {
const model2 = await manager.resolve(resource2, { encoding: 'utf8' });
assert.equal(loadedCounter, 2);
model1.textEditorModel!.setValue('changed');
model1.updateTextEditorModel(createTextBufferFactory('changed'));
model1.updatePreferredEncoding('utf16');
await model1.revert();
model1.textEditorModel!.setValue('changed again');
model1.updateTextEditorModel(createTextBufferFactory('changed again'));
await model1.save();
model1.dispose();
......@@ -239,7 +240,7 @@ suite('Files - TextFileEditorModelManager', () => {
const resource = toResource.call(this, '/path/index_something.txt');
const model = await manager.resolve(resource, { encoding: 'utf8' });
model.textEditorModel!.setValue('make dirty');
model.updateTextEditorModel(createTextBufferFactory('make dirty'));
manager.disposeModel((model as TextFileEditorModel));
assert.ok(!model.isDisposed());
model.revert({ soft: true });
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册