mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-16 22:14:20 -05:00
Add an option to disable closing open files that are removed from the directory (#21962)
This commit is contained in:
@@ -905,6 +905,7 @@ export interface IWorkbenchEditorConfiguration {
|
||||
showIcons: boolean;
|
||||
enablePreview: boolean;
|
||||
enablePreviewFromQuickOpen: boolean;
|
||||
closeOnExternalFileDelete: boolean;
|
||||
openPositioning: 'left' | 'right' | 'first' | 'last';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -132,6 +132,11 @@ configurationRegistry.registerConfiguration({
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': nls.localize('activityBarVisibility', "Controls the visibility of the activity bar in the workbench.")
|
||||
},
|
||||
'workbench.editor.closeOnExternalFileDelete': {
|
||||
'type': 'boolean',
|
||||
'description': nls.localize('closeOnExternalFileDelete', "Controls if editors showing a file should close automatically when the file is deleted or renamed by some other process. Disabling this will keep the editor open as dirty on such an event. Note that deleting from within the application will always close the editor and that dirty files will never close to preserve your data."),
|
||||
'default': true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -212,9 +212,6 @@ class ResolveSaveConflictMessage implements IMessageWithAction {
|
||||
|
||||
return this.editorService.openEditor({ leftResource: URI.from({ scheme: CONFLICT_RESOLUTION_SCHEME, path: resource.fsPath }), rightResource: resource, label: editorLabel, options: { pinned: true } }).then(() => {
|
||||
|
||||
// We have to bring the model into conflict resolution mode to prevent subsequent save erros when the user makes edits
|
||||
this.model.setConflictResolutionMode();
|
||||
|
||||
// Inform user
|
||||
pendingResolveSaveConflictMessages.push(this.messageService.show(Severity.Info, nls.localize('userGuide', "Use the actions in the editor tool bar to either **undo** your changes or **overwrite** the content on disk with your changes")));
|
||||
});
|
||||
|
||||
@@ -159,8 +159,8 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
|
||||
}
|
||||
|
||||
const state = model.getState();
|
||||
if (state === ModelState.CONFLICT || state === ModelState.ERROR) {
|
||||
return true; // always indicate dirty state if we are in conflict or error state
|
||||
if (state === ModelState.CONFLICT || state === ModelState.ORPHAN || state === ModelState.ERROR) {
|
||||
return true; // always indicate dirty state if we are in conflict, orphan or error state
|
||||
}
|
||||
|
||||
if (this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
|
||||
@@ -199,6 +199,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
|
||||
});
|
||||
}
|
||||
|
||||
public isResolved(): boolean {
|
||||
return !!this.textFileService.models.get(this.resource);
|
||||
}
|
||||
|
||||
public getTelemetryDescriptor(): { [key: string]: any; } {
|
||||
const descriptor = super.getTelemetryDescriptor();
|
||||
descriptor['resource'] = telemetryURIDescriptor(this.getResource());
|
||||
@@ -209,7 +213,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
|
||||
public dispose(): void {
|
||||
|
||||
// Listeners
|
||||
dispose(this.toUnbind);
|
||||
this.toUnbind = dispose(this.toUnbind);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import errors = require('vs/base/common/errors');
|
||||
import URI from 'vs/base/common/uri';
|
||||
import paths = require('vs/base/common/paths');
|
||||
import { IEditor, IEditorViewState, isCommonCodeEditor } from 'vs/editor/common/editorCommon';
|
||||
import { toResource, IEditorStacksModel, SideBySideEditorInput, IEditorGroup } from 'vs/workbench/common/editor';
|
||||
import { toResource, IEditorStacksModel, SideBySideEditorInput, IEditorGroup, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
|
||||
import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files';
|
||||
import { ITextFileService, ModelState, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent, isEqual, indexOf } from 'vs/platform/files/common/files';
|
||||
import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent, isEqual, indexOf, isParent } from 'vs/platform/files/common/files';
|
||||
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
@@ -20,10 +21,14 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { once } from 'vs/base/common/event';
|
||||
|
||||
export class FileEditorTracker implements IWorkbenchContribution {
|
||||
private stacks: IEditorStacksModel;
|
||||
private toUnbind: IDisposable[];
|
||||
protected closeOnExternalFileDelete: boolean;
|
||||
private mapResourceToUndoDirtyFromExternalDelete: { [resource: string]: () => void };
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
|
||||
@@ -31,10 +36,14 @@ export class FileEditorTracker implements IWorkbenchContribution {
|
||||
@ILifecycleService private lifecycleService: ILifecycleService,
|
||||
@IEditorGroupService private editorGroupService: IEditorGroupService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IEnvironmentService private environmentService: IEnvironmentService
|
||||
@IEnvironmentService private environmentService: IEnvironmentService,
|
||||
@IConfigurationService private configurationService: IConfigurationService
|
||||
) {
|
||||
this.toUnbind = [];
|
||||
this.stacks = editorGroupService.getStacksModel();
|
||||
this.mapResourceToUndoDirtyFromExternalDelete = Object.create(null);
|
||||
|
||||
this.onConfigurationUpdated(configurationService.getConfiguration<IWorkbenchEditorConfiguration>());
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
@@ -53,6 +62,17 @@ export class FileEditorTracker implements IWorkbenchContribution {
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
|
||||
// Configuration
|
||||
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(e.config)));
|
||||
}
|
||||
|
||||
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
|
||||
if (configuration.workbench && configuration.workbench.editor && typeof configuration.workbench.editor.closeOnExternalFileDelete === 'boolean') {
|
||||
this.closeOnExternalFileDelete = configuration.workbench.editor.closeOnExternalFileDelete;
|
||||
} else {
|
||||
this.closeOnExternalFileDelete = true; // default
|
||||
}
|
||||
}
|
||||
|
||||
// Note: there is some duplication with the other file event handler below. Since we cannot always rely on the disk events
|
||||
@@ -68,94 +88,145 @@ export class FileEditorTracker implements IWorkbenchContribution {
|
||||
|
||||
// Handle deletes
|
||||
if (e.operation === FileOperation.DELETE || e.operation === FileOperation.MOVE) {
|
||||
this.handleDeletes(e.resource, e.target ? e.target.resource : void 0);
|
||||
this.handleDeletes(e.resource, false, e.target ? e.target.resource : void 0);
|
||||
}
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
|
||||
// Handle added
|
||||
if (e.gotAdded()) {
|
||||
this.handleAdded(e);
|
||||
}
|
||||
|
||||
// Handle updates
|
||||
this.handleUpdates(e);
|
||||
|
||||
// Handle deletes
|
||||
if (e.gotDeleted()) {
|
||||
this.handleDeletes(e);
|
||||
this.handleDeletes(e, true);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeletes(arg1: URI | FileChangesEvent, movedTo?: URI): void {
|
||||
const fileInputs = this.getOpenedFileInputs();
|
||||
fileInputs.forEach(input => {
|
||||
if (input.isDirty()) {
|
||||
return; // we never dispose dirty files
|
||||
}
|
||||
private handleAdded(e: FileChangesEvent): void {
|
||||
|
||||
// Flag models as saved that are identical to disk contents
|
||||
// (only if we do not dispose from external deletes and caused them to be dirty)
|
||||
if (!this.closeOnExternalFileDelete) {
|
||||
const dirtyFileEditors = this.getOpenedFileEditors(true /* dirty only */);
|
||||
dirtyFileEditors.forEach(editor => {
|
||||
const resource = editor.getResource();
|
||||
|
||||
// See if we have a stored undo operation for this editor
|
||||
const undo = this.mapResourceToUndoDirtyFromExternalDelete[resource.toString()];
|
||||
if (undo) {
|
||||
|
||||
// file showing in editor was added
|
||||
if (e.contains(resource, FileChangeType.ADDED)) {
|
||||
undo();
|
||||
this.mapResourceToUndoDirtyFromExternalDelete[resource.toString()] = void 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
|
||||
const nonDirtyFileEditors = this.getOpenedFileEditors(false /* non dirty only */);
|
||||
nonDirtyFileEditors.forEach(editor => {
|
||||
const resource = editor.getResource();
|
||||
|
||||
// Special case: a resource was renamed to the same path with different casing. Since our paths
|
||||
// API is treating the paths as equal (they are on disk), we end up disposing the input we just
|
||||
// renamed. The workaround is to detect that we do not dispose any input we are moving the file to
|
||||
if (movedTo && movedTo.fsPath === input.getResource().fsPath) {
|
||||
if (movedTo && movedTo.fsPath === resource.fsPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let matches = false;
|
||||
if (arg1 instanceof FileChangesEvent) {
|
||||
matches = arg1.contains(input.getResource(), FileChangeType.DELETED);
|
||||
matches = arg1.contains(resource, FileChangeType.DELETED);
|
||||
} else {
|
||||
matches = paths.isEqualOrParent(input.getResource().toString(), arg1.toString());
|
||||
matches = isEqual(resource.fsPath, arg1.fsPath) || isParent(resource.fsPath, arg1.fsPath);
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
// TODO@Ben this is for debugging https://github.com/Microsoft/vscode/issues/13665
|
||||
if (this.environmentService.verbose) {
|
||||
this.fileService.existsFile(input.getResource()).done(exists => {
|
||||
if (!exists) {
|
||||
input.dispose();
|
||||
console.warn(`[13665] The file ${input.getResource().fsPath} actually does not exist anymore.`);
|
||||
setTimeout(() => {
|
||||
this.fileService.existsFile(input.getResource()).done(exists => {
|
||||
console.warn(`[13665] The file ${input.getResource().fsPath} after 2 seconds exists: ${exists}`);
|
||||
}, error => {
|
||||
console.error(`[13665] Error checking existance for ${input.getResource().fsPath} after 2 seconds!`, error);
|
||||
});
|
||||
}, 2000);
|
||||
} else {
|
||||
console.warn(`[13665] The file ${input.getResource().fsPath} actually still exists!`);
|
||||
}
|
||||
}, error => {
|
||||
console.error(`[13665] Error checking existance for ${input.getResource().fsPath}`, error);
|
||||
input.dispose();
|
||||
});
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle deletes in opened editors depending on:
|
||||
// - the user has not disabled the setting closeOnExternalFileDelete
|
||||
// - the file change is local or external
|
||||
// - the input is not resolved (we need to dispose because we cannot restore otherwise since we do not have the contents)
|
||||
if (this.closeOnExternalFileDelete || !isExternal || !editor.isResolved()) {
|
||||
|
||||
// We have received reports of users seeing delete events even though the file still
|
||||
// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
|
||||
// Since we do not want to close an editor without reason, we have to check if the
|
||||
// file is really gone and not just a faulty file event (TODO@Ben revisit when we
|
||||
// have a more stable file watcher in place for this scenario).
|
||||
// This only applies to external file events, so we need to check for the isExternal
|
||||
// flag.
|
||||
let checkExists: TPromise<boolean>;
|
||||
if (isExternal) {
|
||||
checkExists = TPromise.timeout(100).then(() => this.fileService.existsFile(resource));
|
||||
} else {
|
||||
input.dispose();
|
||||
checkExists = TPromise.as(false);
|
||||
}
|
||||
|
||||
checkExists.done(exists => {
|
||||
if (!exists && !editor.isDisposed()) {
|
||||
editor.dispose();
|
||||
} else if (this.environmentService.verbose) {
|
||||
console.warn(`File exists even though we received a delete event: ${resource.toString()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise we want to keep the editor open and mark it as dirty since its underlying resource was deleted
|
||||
else {
|
||||
const model = this.textFileService.models.get(resource);
|
||||
if (model && model.getState() === ModelState.SAVED) {
|
||||
const undo = model.setOrphaned();
|
||||
this.mapResourceToUndoDirtyFromExternalDelete[resource.toString()] = undo;
|
||||
once(model.onDispose)(() => {
|
||||
this.mapResourceToUndoDirtyFromExternalDelete[resource.toString()] = void 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getOpenedFileInputs(): FileEditorInput[] {
|
||||
const inputs: FileEditorInput[] = [];
|
||||
private getOpenedFileEditors(dirtyState: boolean): FileEditorInput[] {
|
||||
const editors: FileEditorInput[] = [];
|
||||
|
||||
const stacks = this.editorGroupService.getStacksModel();
|
||||
stacks.groups.forEach(group => {
|
||||
group.getEditors().forEach(input => {
|
||||
if (input instanceof FileEditorInput) {
|
||||
inputs.push(input);
|
||||
} else if (input instanceof SideBySideEditorInput) {
|
||||
const master = input.master;
|
||||
const details = input.details;
|
||||
group.getEditors().forEach(editor => {
|
||||
if (editor instanceof FileEditorInput) {
|
||||
if (!!editor.isDirty() === dirtyState) {
|
||||
editors.push(editor);
|
||||
}
|
||||
} else if (editor instanceof SideBySideEditorInput) {
|
||||
const master = editor.master;
|
||||
const details = editor.details;
|
||||
|
||||
if (master instanceof FileEditorInput) {
|
||||
inputs.push(master);
|
||||
if (!!master.isDirty() === dirtyState) {
|
||||
editors.push(master);
|
||||
}
|
||||
}
|
||||
|
||||
if (details instanceof FileEditorInput) {
|
||||
inputs.push(details);
|
||||
if (!!details.isDirty() === dirtyState) {
|
||||
editors.push(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return inputs;
|
||||
return editors;
|
||||
}
|
||||
|
||||
private handleMovedFileInOpenedEditors(oldResource: URI, newResource: URI): void {
|
||||
@@ -166,7 +237,7 @@ export class FileEditorTracker implements IWorkbenchContribution {
|
||||
const resource = input.getResource();
|
||||
|
||||
// Update Editor if file (or any parent of the input) got renamed or moved
|
||||
if (paths.isEqualOrParent(resource.fsPath, oldResource.fsPath)) {
|
||||
if (isEqual(resource.fsPath, oldResource.fsPath) || isParent(resource.fsPath, oldResource.fsPath)) {
|
||||
let reopenFileResource: URI;
|
||||
if (oldResource.toString() === resource.toString()) {
|
||||
reopenFileResource = newResource; // file got moved
|
||||
|
||||
@@ -59,10 +59,12 @@ suite('Files - FileEditorInput', () => {
|
||||
|
||||
input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar.html'), void 0);
|
||||
|
||||
const inputToResolve = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), void 0);
|
||||
const sameOtherInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), void 0);
|
||||
const inputToResolve: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), void 0);
|
||||
const sameOtherInput: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), void 0);
|
||||
|
||||
return inputToResolve.resolve(true).then(resolved => {
|
||||
assert.ok(inputToResolve.isResolved());
|
||||
|
||||
const resolvedModelA = resolved;
|
||||
return inputToResolve.resolve(true).then(resolved => {
|
||||
assert(resolvedModelA === resolved); // OK: Resolved Model cached globally per input
|
||||
|
||||
@@ -18,6 +18,14 @@ import { EditorStacksModel } from 'vs/workbench/common/editor/editorStacksModel'
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { FileOperation, FileOperationEvent, FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { once } from 'vs/base/common/event';
|
||||
|
||||
class TestFileEditorTracker extends FileEditorTracker {
|
||||
|
||||
setCloseOnExternalFileDelete(value: boolean): void {
|
||||
this.closeOnExternalFileDelete = value;
|
||||
}
|
||||
}
|
||||
|
||||
function toResource(path) {
|
||||
return URI.file(join('C:\\', new Buffer(this.test.fullTitle()).toString('base64'), path));
|
||||
@@ -80,7 +88,7 @@ suite('Files - FileEditorTracker', () => {
|
||||
tracker.dispose();
|
||||
});
|
||||
|
||||
test('disposes when resource gets deleted - remote file changes', function () {
|
||||
test('disposes when resource gets deleted - remote file changes', function (done) {
|
||||
const stacks = accessor.editorGroupService.getStacksModel() as EditorStacksModel;
|
||||
const group = stacks.openGroup('first', true);
|
||||
|
||||
@@ -95,21 +103,76 @@ suite('Files - FileEditorTracker', () => {
|
||||
assert.ok(!input.isDisposed());
|
||||
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }]));
|
||||
assert.ok(input.isDisposed());
|
||||
group.closeEditor(input);
|
||||
|
||||
input = instantiationService.createInstance(FileEditorInput, resource, void 0);
|
||||
once(input.onDispose)(() => {
|
||||
assert.ok(input.isDisposed());
|
||||
group.closeEditor(input);
|
||||
|
||||
input = instantiationService.createInstance(FileEditorInput, resource, void 0);
|
||||
group.openEditor(input);
|
||||
|
||||
const other = toResource.call(this, '/foo/barfoo');
|
||||
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: other, type: FileChangeType.DELETED }]));
|
||||
assert.ok(!input.isDisposed());
|
||||
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: parent, type: FileChangeType.DELETED }]));
|
||||
|
||||
once(input.onDispose)(() => {
|
||||
assert.ok(input.isDisposed());
|
||||
|
||||
tracker.dispose();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('marks dirty when resource gets deleted and undirty when added again - remote file changes - closeOnExternalFileDelete = false', function (done) {
|
||||
const stacks = accessor.editorGroupService.getStacksModel() as EditorStacksModel;
|
||||
const group = stacks.openGroup('first', true);
|
||||
|
||||
const tracker = instantiationService.createInstance(TestFileEditorTracker);
|
||||
tracker.setCloseOnExternalFileDelete(false);
|
||||
assert.ok(tracker);
|
||||
|
||||
const resource = toResource.call(this, '/foo/bar/updatefile.js');
|
||||
let input = instantiationService.createInstance(FileEditorInput, resource, void 0);
|
||||
group.openEditor(input);
|
||||
|
||||
const other = toResource.call(this, '/foo/barfoo');
|
||||
input.resolve().then(() => {
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }]));
|
||||
assert.equal(input.isDirty(), true);
|
||||
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: other, type: FileChangeType.DELETED }]));
|
||||
assert.ok(!input.isDisposed());
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.ADDED }]));
|
||||
assert.equal(input.isDirty(), false);
|
||||
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: parent, type: FileChangeType.DELETED }]));
|
||||
assert.ok(input.isDisposed());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
tracker.dispose();
|
||||
test('marks dirty when resource gets deleted and undirty when added again unless model changed meanwhile - remote file changes - closeOnExternalFileDelete = false', function (done) {
|
||||
const stacks = accessor.editorGroupService.getStacksModel() as EditorStacksModel;
|
||||
const group = stacks.openGroup('first', true);
|
||||
|
||||
const tracker = instantiationService.createInstance(TestFileEditorTracker);
|
||||
tracker.setCloseOnExternalFileDelete(false);
|
||||
assert.ok(tracker);
|
||||
|
||||
const resource = toResource.call(this, '/foo/bar/updatefile.js');
|
||||
let input = instantiationService.createInstance(FileEditorInput, resource, void 0);
|
||||
group.openEditor(input);
|
||||
|
||||
input.resolve().then(model => {
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }]));
|
||||
assert.equal(input.isDirty(), true);
|
||||
|
||||
model.textEditorModel.setValue('This is cool');
|
||||
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.ADDED }]));
|
||||
assert.equal(input.isDirty(), true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('file change event updates model', function (done) {
|
||||
|
||||
@@ -60,13 +60,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
private contentChangeEventScheduler: RunOnceScheduler;
|
||||
private saveSequentializer: SaveSequentializer;
|
||||
private disposed: boolean;
|
||||
private inConflictResolutionMode: boolean;
|
||||
private inErrorMode: boolean;
|
||||
private lastSaveAttemptTime: number;
|
||||
private createTextEditorModelPromise: TPromise<TextFileEditorModel>;
|
||||
private _onDidContentChange: Emitter<StateChange>;
|
||||
private _onDidStateChange: Emitter<StateChange>;
|
||||
|
||||
private inConflictMode: boolean;
|
||||
private inOrphanMode: boolean;
|
||||
private inErrorMode: boolean;
|
||||
|
||||
constructor(
|
||||
resource: URI,
|
||||
preferredEncoding: string,
|
||||
@@ -173,16 +175,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
TextFileEditorModel.saveParticipant = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* When set, will disable any saving (including auto save) until the model is loaded again. This allows to resolve save conflicts
|
||||
* without running into subsequent save errors when editing the model.
|
||||
*/
|
||||
public setConflictResolutionMode(): void {
|
||||
diag('setConflictResolutionMode() - enabled conflict resolution mode', this.resource, new Date());
|
||||
|
||||
this.inConflictResolutionMode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards any local changes and replaces the model with the contents of the version on disk.
|
||||
*
|
||||
@@ -431,7 +423,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// In this case we clear the dirty flag and emit a SAVED event to indicate this state.
|
||||
// Note: we currently only do this check when auto-save is turned off because there you see
|
||||
// a dirty indicator that you want to get rid of when undoing to the saved version.
|
||||
if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
|
||||
// Note: if the model is in orphan mode, we cannot clear the dirty indicator because there
|
||||
// is no version on disk after all.
|
||||
if (!this.autoSaveAfterMilliesEnabled && !this.inOrphanMode && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
|
||||
diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date());
|
||||
|
||||
// Clear flags
|
||||
@@ -453,7 +447,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
|
||||
// Start auto save process unless we are in conflict resolution mode and unless it is disabled
|
||||
if (this.autoSaveAfterMilliesEnabled) {
|
||||
if (!this.inConflictResolutionMode) {
|
||||
if (!this.inConflictMode && !this.inOrphanMode) {
|
||||
this.doAutoSave(this.versionId);
|
||||
} else {
|
||||
diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
|
||||
@@ -642,9 +636,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}, error => {
|
||||
diag(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource, new Date());
|
||||
|
||||
// Flag as error state
|
||||
// Flag as error state in the model
|
||||
this.inErrorMode = true;
|
||||
|
||||
// Look out for a save conflict
|
||||
if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
|
||||
this.inConflictMode = true;
|
||||
}
|
||||
|
||||
// Show to user
|
||||
this.onSaveError(error);
|
||||
|
||||
@@ -656,13 +655,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
|
||||
private setDirty(dirty: boolean): () => void {
|
||||
const wasDirty = this.dirty;
|
||||
const wasInConflictResolutionMode = this.inConflictResolutionMode;
|
||||
const wasInConflictMode = this.inConflictMode;
|
||||
const wasInOrphanMode = this.inOrphanMode;
|
||||
const wasInErrorMode = this.inErrorMode;
|
||||
const oldBufferSavedVersionId = this.bufferSavedVersionId;
|
||||
|
||||
if (!dirty) {
|
||||
this.dirty = false;
|
||||
this.inConflictResolutionMode = false;
|
||||
this.inConflictMode = false;
|
||||
this.inOrphanMode = false;
|
||||
this.inErrorMode = false;
|
||||
|
||||
// we remember the models alternate version id to remember when the version
|
||||
@@ -680,7 +681,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// Return function to revert this call
|
||||
return () => {
|
||||
this.dirty = wasDirty;
|
||||
this.inConflictResolutionMode = wasInConflictResolutionMode;
|
||||
this.inConflictMode = wasInConflictMode;
|
||||
this.inOrphanMode = wasInOrphanMode;
|
||||
this.inErrorMode = wasInErrorMode;
|
||||
this.bufferSavedVersionId = oldBufferSavedVersionId;
|
||||
};
|
||||
@@ -737,10 +739,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
* Returns the state this text text file editor model is in with regards to changes and saving.
|
||||
*/
|
||||
public getState(): ModelState {
|
||||
if (this.inConflictResolutionMode) {
|
||||
if (this.inConflictMode) {
|
||||
return ModelState.CONFLICT;
|
||||
}
|
||||
|
||||
if (this.inOrphanMode) {
|
||||
return ModelState.ORPHAN;
|
||||
}
|
||||
|
||||
if (this.inErrorMode) {
|
||||
return ModelState.ERROR;
|
||||
}
|
||||
@@ -778,7 +784,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
this.makeDirty();
|
||||
}
|
||||
|
||||
if (!this.inConflictResolutionMode) {
|
||||
if (!this.inConflictMode) {
|
||||
this.save({ overwriteEncoding: true }).done(null, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
@@ -846,9 +852,49 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
return this.lastResolvedDiskStat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this model an orphan, indicating that the file on disk no longer exists. Returns
|
||||
* a function to undo this and go back to the previous state.
|
||||
*/
|
||||
public setOrphaned(): () => void {
|
||||
|
||||
// Only saved models can turn into orphans
|
||||
if (this.getState() !== ModelState.SAVED) {
|
||||
return () => { };
|
||||
}
|
||||
|
||||
// Mark as dirty
|
||||
const undo = this.setDirty(true);
|
||||
|
||||
// Mark as oprhaned
|
||||
this.inOrphanMode = true;
|
||||
|
||||
// Emit as Event if we turned dirty
|
||||
this._onDidStateChange.fire(StateChange.DIRTY);
|
||||
|
||||
// Return undo function
|
||||
const currentVersionId = this.versionId;
|
||||
return () => {
|
||||
|
||||
// Leave orphan mode
|
||||
this.inOrphanMode = false;
|
||||
|
||||
// Undo is only valid if version is the one we left with
|
||||
if (this.versionId === currentVersionId) {
|
||||
|
||||
// Revert
|
||||
undo();
|
||||
|
||||
// Events
|
||||
this._onDidStateChange.fire(StateChange.SAVED);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposed = true;
|
||||
this.inConflictResolutionMode = false;
|
||||
this.inConflictMode = false;
|
||||
this.inOrphanMode = false;
|
||||
this.inErrorMode = false;
|
||||
|
||||
this.toDispose = dispose(this.toDispose);
|
||||
|
||||
@@ -13,10 +13,8 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer
|
||||
import { ModelState, ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { FileOperation, FileOperationEvent, FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
|
||||
|
||||
export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
|
||||
private toUnbind: IDisposable[];
|
||||
|
||||
private _onModelDisposed: Emitter<URI>;
|
||||
@@ -41,8 +39,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
constructor(
|
||||
@ILifecycleService private lifecycleService: ILifecycleService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IEditorGroupService private editorGroupService: IEditorGroupService,
|
||||
@IFileService private fileService: IFileService
|
||||
@IEditorGroupService private editorGroupService: IEditorGroupService
|
||||
) {
|
||||
this.toUnbind = [];
|
||||
|
||||
@@ -77,10 +74,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
|
||||
this.toUnbind.push(this.editorGroupService.getStacksModel().onEditorClosed(() => this.onEditorClosed()));
|
||||
|
||||
// File changes
|
||||
this.toUnbind.push(this.fileService.onAfterOperation(e => this.onFileOperation(e)));
|
||||
this.toUnbind.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
}
|
||||
@@ -93,46 +86,41 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
this.disposeUnusedModels();
|
||||
}
|
||||
|
||||
private disposeModelIfPossible(resource: URI): void {
|
||||
const model = this.get(resource);
|
||||
if (this.canDispose(model)) {
|
||||
private disposeUnusedModels(): void {
|
||||
|
||||
// To not grow our text file model cache infinitly, we dispose models that
|
||||
// are not showing up in any opened editor.
|
||||
// TODO@Ben this is a workaround until we have adopted model references from
|
||||
// the resolver service (https://github.com/Microsoft/vscode/issues/17888)
|
||||
|
||||
this.getAll(void 0, model => this.canDispose(model)).forEach(model => {
|
||||
model.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private onFileOperation(e: FileOperationEvent): void {
|
||||
if (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE) {
|
||||
this.disposeModelIfPossible(e.resource); // dispose models of moved or deleted files
|
||||
}
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
|
||||
// Dispose inputs that got deleted
|
||||
e.getDeleted().forEach(deleted => {
|
||||
this.disposeModelIfPossible(deleted.resource);
|
||||
});
|
||||
}
|
||||
|
||||
private canDispose(textModel: ITextFileEditorModel): boolean {
|
||||
if (!textModel) {
|
||||
private canDispose(model: ITextFileEditorModel): boolean {
|
||||
if (!model) {
|
||||
return false; // we need data!
|
||||
}
|
||||
|
||||
if (textModel.isDisposed()) {
|
||||
if (model.isDisposed()) {
|
||||
return false; // already disposed
|
||||
}
|
||||
|
||||
if (textModel.textEditorModel && textModel.textEditorModel.isAttachedToEditor()) {
|
||||
return false; // never dispose when attached to editor
|
||||
if (this.mapResourceToPendingModelLoaders[model.getResource().toString()]) {
|
||||
return false; // not yet loaded
|
||||
}
|
||||
|
||||
if (textModel.getState() !== ModelState.SAVED) {
|
||||
return false; // never dispose unsaved models
|
||||
if (model.getState() !== ModelState.SAVED) {
|
||||
return false; // not saved
|
||||
}
|
||||
|
||||
if (this.mapResourceToPendingModelLoaders[textModel.getResource().toString()]) {
|
||||
return false; // never dispose models that we are about to load at the same time
|
||||
if (model.textEditorModel && model.textEditorModel.isAttachedToEditor()) {
|
||||
return false; // never dispose when attached to editor (e.g. viewzones)
|
||||
}
|
||||
|
||||
if (this.editorGroupService.getStacksModel().isOpen(model.getResource())) {
|
||||
return false; // never dispose when opened inside an editor (e.g. tabs)
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -299,7 +287,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
});
|
||||
}
|
||||
|
||||
public getAll(resource?: URI): ITextFileEditorModel[] {
|
||||
public getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] {
|
||||
if (resource) {
|
||||
const res = this.mapResourceToModel[resource.toString()];
|
||||
|
||||
@@ -309,7 +297,10 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
const keys = Object.keys(this.mapResourceToModel);
|
||||
const res: ITextFileEditorModel[] = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
res.push(this.mapResourceToModel[keys[i]]);
|
||||
const model = this.mapResourceToModel[keys[i]];
|
||||
if (!filter || filter(model)) {
|
||||
res.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
@@ -378,21 +369,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
this.mapResourceToModelContentChangeListener = Object.create(null);
|
||||
}
|
||||
|
||||
private disposeUnusedModels(): void {
|
||||
|
||||
// To not grow our text file model cache infinitly, we dispose models that
|
||||
// are not showing up in any opened editor.
|
||||
|
||||
// Get all cached file models
|
||||
this.getAll()
|
||||
|
||||
// Only models that are not open inside the editor area
|
||||
.filter(model => !this.editorGroupService.getStacksModel().isOpen(model.getResource()))
|
||||
|
||||
// Dispose
|
||||
.forEach(model => this.disposeModelIfPossible(model.getResource()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.toUnbind = dispose(this.toUnbind);
|
||||
}
|
||||
|
||||
@@ -479,8 +479,8 @@ export abstract class TextFileService implements ITextFileService {
|
||||
private doSaveAllFiles(arg1?: any /* URI[] */, reason?: SaveReason): TPromise<ITextFileOperationResult> {
|
||||
const dirtyFileModels = this.getDirtyFileModels(Array.isArray(arg1) ? arg1 : void 0 /* Save All */)
|
||||
.filter(model => {
|
||||
if (model.getState() === ModelState.CONFLICT && (reason === SaveReason.AUTO || reason === SaveReason.FOCUS_CHANGE || reason === SaveReason.WINDOW_CHANGE)) {
|
||||
return false; // if model is in save conflict, do not save when reason is auto save, since saving needs manual resolution by the user
|
||||
if ((model.getState() === ModelState.CONFLICT || model.getState() === ModelState.ORPHAN) && (reason === SaveReason.AUTO || reason === SaveReason.FOCUS_CHANGE || reason === SaveReason.WINDOW_CHANGE)) {
|
||||
return false; // if model is in an orphan or in save conflict, do not save unless save reason is explicit or not provided at all
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -40,7 +40,23 @@ export enum ModelState {
|
||||
SAVED,
|
||||
DIRTY,
|
||||
PENDING_SAVE,
|
||||
|
||||
/**
|
||||
* A model is in conflict mode when changes cannot be saved because the
|
||||
* underlying file has changed. Models in conflict mode are always dirty.
|
||||
*/
|
||||
CONFLICT,
|
||||
|
||||
/**
|
||||
* A model is in orphan state when the underlying file has been deleted.
|
||||
* Models in orphan mode are always dirty.
|
||||
*/
|
||||
ORPHAN,
|
||||
|
||||
/**
|
||||
* Any error that happens during a save that is not causing the CONFLICT state.
|
||||
* Models in error mode are always diry.
|
||||
*/
|
||||
ERROR
|
||||
}
|
||||
|
||||
@@ -176,10 +192,10 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
|
||||
|
||||
revert(soft?: boolean): TPromise<void>;
|
||||
|
||||
setConflictResolutionMode(): void;
|
||||
|
||||
getValue(): string;
|
||||
|
||||
setOrphaned(): () => void;
|
||||
|
||||
isDirty(): boolean;
|
||||
|
||||
isResolved(): boolean;
|
||||
|
||||
@@ -210,31 +210,6 @@ suite('Files - TextFileEditorModel', () => {
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Conflict Resolution Mode', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.load().done(() => {
|
||||
model.setConflictResolutionMode();
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.ok(model.isDirty());
|
||||
assert.equal(model.getState(), ModelState.CONFLICT);
|
||||
|
||||
return model.revert().then(() => {
|
||||
model.textEditorModel.setValue('bar');
|
||||
assert.ok(model.isDirty());
|
||||
|
||||
return model.save().then(() => {
|
||||
assert.ok(!model.isDirty());
|
||||
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Auto Save triggered when model changes', function (done) {
|
||||
let eventCounter = 0;
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8');
|
||||
@@ -382,6 +357,25 @@ suite('Files - TextFileEditorModel', () => {
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('setOrphaned basics', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
return model.load().then(() => {
|
||||
let undo = model.setOrphaned();
|
||||
assert.equal(model.isDirty(), true);
|
||||
undo();
|
||||
assert.equal(model.isDirty(), false);
|
||||
|
||||
// can not undo when model changed meanwhile
|
||||
undo = model.setOrphaned();
|
||||
model.textEditorModel.setValue('foo');
|
||||
undo();
|
||||
assert.equal(model.isDirty(), true);
|
||||
|
||||
done();
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('SaveSequentializer - pending basics', function (done) {
|
||||
const sequentializer = new SaveSequentializer();
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ 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 { join, basename } from 'vs/base/common/paths';
|
||||
import { join } from 'vs/base/common/paths';
|
||||
import { workbenchInstantiationService, TestEditorGroupService, createFileInput, TestFileService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { onError } from 'vs/base/test/common/utils';
|
||||
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { FileOperation, FileOperationEvent, FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
|
||||
export class TestTextFileEditorModelManager extends TextFileEditorModelManager {
|
||||
@@ -25,17 +25,6 @@ export class TestTextFileEditorModelManager extends TextFileEditorModelManager {
|
||||
}
|
||||
}
|
||||
|
||||
function toStat(resource: URI) {
|
||||
return {
|
||||
resource,
|
||||
isDirectory: false,
|
||||
hasChildren: false,
|
||||
name: basename(resource.fsPath),
|
||||
mtime: Date.now(),
|
||||
etag: 'etag'
|
||||
};
|
||||
}
|
||||
|
||||
class ServiceAccessor {
|
||||
constructor(
|
||||
@IEditorGroupService public editorGroupService: TestEditorGroupService,
|
||||
@@ -179,93 +168,6 @@ suite('Files - TextFileEditorModelManager', () => {
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
test('local file changes dispose model - delete', function () {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource = toResource('/path/index.txt');
|
||||
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8');
|
||||
manager.add(resource, model);
|
||||
|
||||
assert.ok(!model.isDisposed());
|
||||
|
||||
// delete operation
|
||||
accessor.fileService.fireAfterOperation(new FileOperationEvent(resource, FileOperation.DELETE));
|
||||
|
||||
assert.ok(model.isDisposed());
|
||||
|
||||
model.dispose();
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
test('local file changes dispose model - move', function () {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource = toResource('/path/index.txt');
|
||||
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8');
|
||||
manager.add(resource, model);
|
||||
|
||||
assert.ok(!model.isDisposed());
|
||||
|
||||
// move operation
|
||||
accessor.fileService.fireAfterOperation(new FileOperationEvent(resource, FileOperation.MOVE, toStat(toResource('/path/index_moved.txt'))));
|
||||
|
||||
assert.ok(model.isDisposed());
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
test('file event delete dispose model', function () {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource = toResource('/path/index.txt');
|
||||
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8');
|
||||
manager.add(resource, model);
|
||||
|
||||
assert.ok(!model.isDisposed());
|
||||
|
||||
// delete event (watcher)
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }]));
|
||||
|
||||
assert.ok(model.isDisposed());
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
test('file change event does NOT dispose model if happening < 2 second after last save', function (done) {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource = toResource('/path/index.txt');
|
||||
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8');
|
||||
manager.add(resource, model);
|
||||
|
||||
assert.ok(!model.isDisposed());
|
||||
|
||||
model.load().done(resolved => {
|
||||
model.textEditorModel.setValue('changed');
|
||||
return model.save().then(() => {
|
||||
|
||||
// change event (watcher)
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }]));
|
||||
|
||||
assert.ok(!model.isDisposed());
|
||||
|
||||
model.dispose();
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
|
||||
manager.dispose();
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('events', function (done) {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user