From 96afb61cbfff286c89c6634267a9bb7acadf3fb2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 9 Sep 2016 08:06:16 +0200 Subject: [PATCH] adopt more typed file events (for #7176) --- .../workbench/api/node/mainThreadDocuments.ts | 10 +-- .../parts/files/browser/fileActions.ts | 10 +-- .../parts/files/browser/saveErrorHandler.ts | 7 +- .../files/browser/views/openEditorsView.ts | 4 +- .../files/common/editors/fileEditorInput.ts | 12 ++-- .../common/editors/textFileEditorModel.ts | 23 +++++-- .../editors/textFileEditorModelManager.ts | 67 +++++++++++++++++-- src/vs/workbench/parts/files/common/files.ts | 52 ++++++++------ .../electron-browser/dirtyFilesTracker.ts | 18 ++--- .../test/browser/fileEditorModel.test.ts | 39 ++++++----- .../textFileEditorModelManager.test.ts | 51 ++++++++++++++ .../parts/git/browser/gitServices.ts | 11 +-- .../electron-browser/electronGitService.ts | 4 +- 13 files changed, 221 insertions(+), 87 deletions(-) diff --git a/src/vs/workbench/api/node/mainThreadDocuments.ts b/src/vs/workbench/api/node/mainThreadDocuments.ts index 351cffb8297..517cb71db93 100644 --- a/src/vs/workbench/api/node/mainThreadDocuments.ts +++ b/src/vs/workbench/api/node/mainThreadDocuments.ts @@ -14,7 +14,7 @@ import URI from 'vs/base/common/uri'; import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import {IEventService} from 'vs/platform/event/common/event'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; -import {EventType as FileEventType, TextFileChangeEvent, ITextFileService} from 'vs/workbench/parts/files/common/files'; +import {TextFileModelChangeEvent, ITextFileService} from 'vs/workbench/parts/files/common/files'; import {TPromise} from 'vs/base/common/winjs.base'; import {IFileService} from 'vs/platform/files/common/files'; import {IModeService} from 'vs/editor/common/services/modeService'; @@ -61,17 +61,17 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { modelService.onModelRemoved(this._onModelRemoved, this, this._toDispose); modelService.onModelModeChanged(this._onModelModeChanged, this, this._toDispose); - this._toDispose.push(eventService.addListener2(FileEventType.FILE_SAVED, (e: TextFileChangeEvent) => { + this._toDispose.push(textFileService.models.onModelSaved(e => { if (this._shouldHandleFileEvent(e)) { this._proxy.$acceptModelSaved(e.resource.toString()); } })); - this._toDispose.push(eventService.addListener2(FileEventType.FILE_REVERTED, (e: TextFileChangeEvent) => { + this._toDispose.push(textFileService.models.onModelReverted(e => { if (this._shouldHandleFileEvent(e)) { this._proxy.$acceptModelReverted(e.resource.toString()); } })); - this._toDispose.push(eventService.addListener2(FileEventType.FILE_DIRTY, (e: TextFileChangeEvent) => { + this._toDispose.push(textFileService.models.onModelDirty(e => { if (this._shouldHandleFileEvent(e)) { this._proxy.$acceptModelDirty(e.resource.toString()); } @@ -93,7 +93,7 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { this._toDispose = dispose(this._toDispose); } - private _shouldHandleFileEvent(e: TextFileChangeEvent): boolean { + private _shouldHandleFileEvent(e: TextFileModelChangeEvent): boolean { const model = this._modelService.getModel(e.resource); return model && !model.isTooLargeForHavingARichMode(); } diff --git a/src/vs/workbench/parts/files/browser/fileActions.ts b/src/vs/workbench/parts/files/browser/fileActions.ts index fd7a3c9a40c..41e583e1dd7 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.ts @@ -24,7 +24,7 @@ import {Action, IAction} from 'vs/base/common/actions'; import {MessageType, IInputValidator} from 'vs/base/browser/ui/inputbox/inputBox'; import {ITree, IHighlightEvent} from 'vs/base/parts/tree/browser/tree'; import {dispose, IDisposable} from 'vs/base/common/lifecycle'; -import {LocalFileChangeEvent, VIEWLET_ID, ITextFileService, TextFileChangeEvent, EventType as FileEventType} from 'vs/workbench/parts/files/common/files'; +import {LocalFileChangeEvent, VIEWLET_ID, ITextFileService} from 'vs/workbench/parts/files/common/files'; import {IFileService, IFileStat, IImportResult} from 'vs/platform/files/common/files'; import {DiffEditorInput, toDiffLabel} from 'vs/workbench/common/editor/diffEditorInput'; import {asFileEditorInput, getUntitledOrFileResource, UntitledEditorInput, IEditorIdentifier} from 'vs/workbench/common/editor'; @@ -1558,10 +1558,10 @@ export abstract class BaseSaveAllAction extends BaseActionWithErrorReporting { private registerListeners(): void { // listen to files being changed locally - this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_DIRTY, (e: TextFileChangeEvent) => this.updateEnablement(true))); - this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_SAVED, (e: TextFileChangeEvent) => this.updateEnablement(false))); - this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_REVERTED, (e: TextFileChangeEvent) => this.updateEnablement(false))); - this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_SAVE_ERROR, (e: TextFileChangeEvent) => this.updateEnablement(true))); + this.toDispose.push(this.textFileService.models.onModelDirty(e => this.updateEnablement(true))); + this.toDispose.push(this.textFileService.models.onModelSaved(e => this.updateEnablement(false))); + this.toDispose.push(this.textFileService.models.onModelReverted(e => this.updateEnablement(false))); + this.toDispose.push(this.textFileService.models.onModelSaveError(e => this.updateEnablement(true))); if (this.includeUntitled()) { this.toDispose.push(this.untitledEditorService.onDidChangeDirty(resource => this.updateEnablement(this.untitledEditorService.isDirty(resource)))); diff --git a/src/vs/workbench/parts/files/browser/saveErrorHandler.ts b/src/vs/workbench/parts/files/browser/saveErrorHandler.ts index 0a7d7a74d90..013290b537f 100644 --- a/src/vs/workbench/parts/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/browser/saveErrorHandler.ts @@ -23,7 +23,7 @@ import {SaveFileAsAction, RevertFileAction, SaveFileAction} from 'vs/workbench/p import {IFileService, IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {IEventService} from 'vs/platform/event/common/event'; -import {EventType as FileEventType, TextFileChangeEvent, ITextFileService, ISaveErrorHandler, ITextFileEditorModel} from 'vs/workbench/parts/files/common/files'; +import {ITextFileService, ISaveErrorHandler, ITextFileEditorModel} from 'vs/workbench/parts/files/common/files'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IMessageService, IMessageWithAction, Severity, CancelAction} from 'vs/platform/message/common/message'; import {IModeService} from 'vs/editor/common/services/modeService'; @@ -36,6 +36,7 @@ export class SaveErrorHandler implements ISaveErrorHandler { constructor( @IMessageService private messageService: IMessageService, @IEventService private eventService: IEventService, + @ITextFileService private textFileService: ITextFileService, @IInstantiationService private instantiationService: IInstantiationService ) { this.messages = Object.create(null); @@ -44,8 +45,8 @@ export class SaveErrorHandler implements ISaveErrorHandler { } private registerListeners(): void { - this.eventService.addListener2(FileEventType.FILE_SAVED, (e: TextFileChangeEvent) => this.onFileSavedOrReverted(e.resource)); - this.eventService.addListener2(FileEventType.FILE_REVERTED, (e: TextFileChangeEvent) => this.onFileSavedOrReverted(e.resource)); + this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource)); + this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource)); } private onFileSavedOrReverted(resource: URI): void { diff --git a/src/vs/workbench/parts/files/browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/browser/views/openEditorsView.ts index 7499e65a762..92ffcf61779 100644 --- a/src/vs/workbench/parts/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/browser/views/openEditorsView.ts @@ -20,7 +20,7 @@ import {IKeybindingService} from 'vs/platform/keybinding/common/keybinding'; import {IEditorStacksModel, IStacksModelChangeEvent, IEditorGroup} from 'vs/workbench/common/editor'; import {SaveAllAction} from 'vs/workbench/parts/files/browser/fileActions'; import {AdaptiveCollapsibleViewletView} from 'vs/workbench/browser/viewlet'; -import {ITextFileService, IFilesConfiguration, VIEWLET_ID, AutoSaveMode, EventType as FileEventType} from 'vs/workbench/parts/files/common/files'; +import {ITextFileService, IFilesConfiguration, VIEWLET_ID, AutoSaveMode} from 'vs/workbench/parts/files/common/files'; import {IViewletService} from 'vs/workbench/services/viewlet/common/viewletService'; import {Renderer, DataSource, Controller, AccessibilityProvider, ActionProvider, OpenEditor, DragAndDrop} from 'vs/workbench/parts/files/browser/views/openEditorsViewer'; import {IUntitledEditorService} from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -133,7 +133,7 @@ export class OpenEditorsView extends AdaptiveCollapsibleViewletView { this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(e.config))); // Also handle dirty count indicator #10556 - this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_DIRTY, (e) => this.updateDirtyIndicator())); + this.toDispose.push(this.textFileService.models.onModelDirty(e => this.updateDirtyIndicator())); // We are not updating the tree while the viewlet is not visible. Thus refresh when viewlet becomes visible #6702 this.toDispose.push(this.viewletService.onDidViewletOpen(viewlet => { diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index ee197c016ea..8ed1d424ed3 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -16,7 +16,7 @@ import assert = require('vs/base/common/assert'); import {IEditorRegistry, Extensions, EditorModel, EncodingMode, ConfirmResult, IEditorDescriptor} from 'vs/workbench/common/editor'; import {BinaryEditorModel} from 'vs/workbench/common/editor/binaryEditorModel'; import {IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files'; -import {ITextFileService, BINARY_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID, FileEditorInput as CommonFileEditorInput, AutoSaveMode, ModelState, EventType as FileEventType, TextFileChangeEvent, IFileEditorDescriptor} from 'vs/workbench/parts/files/common/files'; +import {ITextFileService, BINARY_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID, FileEditorInput as CommonFileEditorInput, AutoSaveMode, ModelState, TextFileModelChangeEvent, IFileEditorDescriptor} from 'vs/workbench/parts/files/common/files'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IDisposable, dispose} from 'vs/base/common/lifecycle'; @@ -66,13 +66,13 @@ export class FileEditorInput extends CommonFileEditorInput { } private registerListeners(): void { - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_DIRTY, (e: TextFileChangeEvent) => this.onDirtyStateChange(e))); - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_SAVE_ERROR, (e: TextFileChangeEvent) => this.onDirtyStateChange(e))); - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_SAVED, (e: TextFileChangeEvent) => this.onDirtyStateChange(e))); - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_REVERTED, (e: TextFileChangeEvent) => this.onDirtyStateChange(e))); + this.toUnbind.push(this.textFileService.models.onModelDirty(e => this.onDirtyStateChange(e))); + this.toUnbind.push(this.textFileService.models.onModelSaveError(e => this.onDirtyStateChange(e))); + this.toUnbind.push(this.textFileService.models.onModelSaved(e => this.onDirtyStateChange(e))); + this.toUnbind.push(this.textFileService.models.onModelReverted(e => this.onDirtyStateChange(e))); } - private onDirtyStateChange(e: TextFileChangeEvent): void { + private onDirtyStateChange(e: TextFileModelChangeEvent): void { if (e.resource.toString() === this.resource.toString()) { this._onDidChangeDirty.fire(); } diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts index 06ec2314df2..c5ddcb43d0e 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModel.ts @@ -5,6 +5,7 @@ 'use strict'; import nls = require('vs/nls'); +import Event, {Emitter} from 'vs/base/common/event'; import {TPromise} from 'vs/base/common/winjs.base'; import {onUnexpectedError} from 'vs/base/common/errors'; import {toErrorMessage} from 'vs/base/common/errorMessage'; @@ -16,7 +17,7 @@ import types = require('vs/base/common/types'); import {IModelContentChangedEvent} from 'vs/editor/common/editorCommon'; import {IMode} from 'vs/editor/common/modes'; import {EventType as WorkbenchEventType, ResourceEvent} from 'vs/workbench/common/events'; -import {EventType as FileEventType, TextFileChangeEvent, ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveErrorHandler} from 'vs/workbench/parts/files/common/files'; +import {EventType as FileEventType, TextFileChangeEvent, ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveErrorHandler, StateChange} from 'vs/workbench/parts/files/common/files'; import {EncodingMode, EditorModel} from 'vs/workbench/common/editor'; import {BaseTextEditorModel} from 'vs/workbench/common/editor/textEditorModel'; import {IFileService, IFileStat, IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files'; @@ -55,6 +56,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private inErrorMode: boolean; private lastSaveAttemptTime: number; private createTextEditorModelPromise: TPromise; + private _onDidStateChange: Emitter; constructor( resource: URI, @@ -75,6 +77,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil throw new Error('TextFileEditorModel can only handle file:// resources.'); } + this._onDidStateChange = new Emitter(); this.preferredEncoding = preferredEncoding; this.textModelChangeListener = null; this.dirty = false; @@ -101,6 +104,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } + public get onDidStateChange(): Event { + return this._onDidStateChange.event; + } + /** * Set a save error handler to install code that executes when save errors occur. */ @@ -143,12 +150,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.load(true /* force */).then(() => { // Emit file change event - this.emitEvent(FileEventType.FILE_REVERTED, new TextFileChangeEvent(this.resource, this.textEditorModel)); + this._onDidStateChange.fire(StateChange.REVERTED); }, (error) => { // FileNotFound means the file got deleted meanwhile, so emit revert event because thats ok if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { - this.emitEvent(FileEventType.FILE_REVERTED, new TextFileChangeEvent(this.resource, this.textEditorModel)); + this._onDidStateChange.fire(StateChange.REVERTED); } // Set flags back to previous values, we are still dirty if revert failed and we where @@ -296,7 +303,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Emit event if (wasDirty) { - this.emitEvent(FileEventType.FILE_REVERTED, new TextFileChangeEvent(this.resource, this.textEditorModel)); + this._onDidStateChange.fire(StateChange.REVERTED); } return; @@ -325,7 +332,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Emit as Event if we turned dirty if (!wasDirty) { - this.emitEvent(FileEventType.FILE_DIRTY, new TextFileChangeEvent(this.resource, this.textEditorModel)); + this._onDidStateChange.fire(StateChange.DIRTY); } } @@ -454,7 +461,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.updateVersionOnDiskStat(stat); // Emit File Saved Event - this.emitEvent(FileEventType.FILE_SAVED, new TextFileChangeEvent(this.resource, this.textEditorModel)); + this._onDidStateChange.fire(StateChange.SAVED); }, (error) => { diag('doSave(' + versionId + ') - exit - resulted in a save error: ' + error.toString(), this.resource, new Date()); @@ -468,7 +475,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.onSaveError(error); // Emit as event - this.emitEvent(FileEventType.FILE_SAVE_ERROR, new TextFileChangeEvent(this.resource, this.textEditorModel)); + this._onDidStateChange.fire(StateChange.SAVE_ERROR); }); return this.mapPendingSaveToVersionId[versionId]; @@ -676,6 +683,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.inConflictResolutionMode = false; this.inErrorMode = false; + this._onDidStateChange.dispose(); + this.createTextEditorModelPromise = null; if (this.textModelChangeListener) { diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts index 19e6888fb16..a8f7f556d19 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import Event, {Emitter} from 'vs/base/common/event'; import {TPromise} from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import {TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; -import {ITextFileEditorModelManager} from 'vs/workbench/parts/files/common/files'; import {dispose, IDisposable} from 'vs/base/common/lifecycle'; import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService'; -import {ModelState, ITextFileEditorModel, LocalFileChangeEvent} from 'vs/workbench/parts/files/common/files'; +import {ModelState, ITextFileEditorModel, LocalFileChangeEvent, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange} from 'vs/workbench/parts/files/common/files'; import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle'; import {IEventService} from 'vs/platform/event/common/event'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; @@ -24,7 +24,13 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { private toUnbind: IDisposable[]; + private _onModelDirty: Emitter; + private _onModelSaveError: Emitter; + private _onModelSaved: Emitter; + private _onModelReverted: Emitter; + private mapResourceToDisposeListener: { [resource: string]: IDisposable; }; + private mapResourceToStateChangeListener: { [resource: string]: IDisposable; }; private mapResourcePathToModel: { [resource: string]: TextFileEditorModel; }; private mapResourceToPendingModelLoaders: { [resource: string]: TPromise}; @@ -36,8 +42,19 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { ) { this.toUnbind = []; + this._onModelDirty = new Emitter(); + this._onModelSaveError = new Emitter(); + this._onModelSaved = new Emitter(); + this._onModelReverted = new Emitter(); + + this.toUnbind.push(this._onModelDirty); + this.toUnbind.push(this._onModelSaveError); + this.toUnbind.push(this._onModelSaved); + this.toUnbind.push(this._onModelReverted); + this.mapResourcePathToModel = Object.create(null); this.mapResourceToDisposeListener = Object.create(null); + this.mapResourceToStateChangeListener = Object.create(null); this.mapResourceToPendingModelLoaders = Object.create(null); this.registerListeners(); @@ -118,6 +135,22 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return true; } + public get onModelDirty(): Event { + return this._onModelDirty.event; + } + + public get onModelSaveError(): Event { + return this._onModelSaveError.event; + } + + public get onModelSaved(): Event { + return this._onModelSaved.event; + } + + public get onModelReverted(): Event { + return this._onModelReverted.event; + } + public get(resource: URI): TextFileEditorModel { return this.mapResourcePathToModel[resource.toString()]; } @@ -146,6 +179,21 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { else { model = this.instantiationService.createInstance(TextFileEditorModel, resource, encoding); modelPromise = model.load(); + + // Install state change listener + this.mapResourceToStateChangeListener[resource.toString()] = model.onDidStateChange(state => { + const event = new TextFileModelChangeEvent(model, state); + switch(state) { + case StateChange.DIRTY: + this._onModelDirty.fire(event); + case StateChange.SAVE_ERROR: + this._onModelSaveError.fire(event); + case StateChange.SAVED: + this._onModelSaved.fire(event); + case StateChange.REVERTED: + this._onModelReverted.fire(event); + } + }); } // Store pending loads to avoid race conditions @@ -203,6 +251,12 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { dispose(disposeListener); delete this.mapResourceToDisposeListener[resource.toString()]; } + + const stateChangeListener = this.mapResourceToStateChangeListener[resource.toString()]; + if (stateChangeListener) { + dispose(stateChangeListener); + delete this.mapResourceToStateChangeListener[resource.toString()]; + } } public clear(): void { @@ -210,10 +264,15 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { // model cache this.mapResourcePathToModel = Object.create(null); - // dispose listeners - const keys = Object.keys(this.mapResourceToDisposeListener); + // dispose dispose listeners + let keys = Object.keys(this.mapResourceToDisposeListener); dispose(keys.map(k => this.mapResourceToDisposeListener[k])); this.mapResourceToDisposeListener = Object.create(null); + + // dispose state change listeners + keys = Object.keys(this.mapResourceToStateChangeListener); + dispose(keys.map(k => this.mapResourceToStateChangeListener[k])); + this.mapResourceToStateChangeListener = Object.create(null); } private disposeUnusedModels(): void { diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index a18fb4f2045..5ecde8738d0 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -112,31 +112,10 @@ export interface ISaveErrorHandler { */ export const EventType = { - /** - * Indicates that a file content has changed but not yet saved. - */ - FILE_DIRTY: 'files:fileDirty', - /** * Indicates that a file is being saved. */ FILE_SAVING: 'files:fileSaving', - - /** - * Indicates that a file save resulted in an error. - */ - FILE_SAVE_ERROR: 'files:fileSaveError', - - /** - * Indicates that a file content has been saved to the disk. - */ - FILE_SAVED: 'files:fileSaved', - - /** - * Indicates that a file content has been reverted to the state - * on disk. - */ - FILE_REVERTED: 'files:fileReverted' }; /** @@ -236,6 +215,32 @@ export class TextFileChangeEvent extends BaseEvent { } } +export enum StateChange { + DIRTY, + SAVING, + SAVE_ERROR, + SAVED, + REVERTED +} + +export class TextFileModelChangeEvent { + private _resource: URI; + private _kind: StateChange; + + constructor(model: ITextFileEditorModel, kind: StateChange) { + this._resource = model.getResource(); + this._kind = kind; + } + + public get resource(): URI { + return this._resource; + } + + public get kind(): StateChange { + return this._kind; + } +} + export const TEXT_FILE_SERVICE_ID = 'textFileService'; export interface ITextFileOperationResult { @@ -288,6 +293,11 @@ export interface IRawTextContent extends IBaseStat { export interface ITextFileEditorModelManager { + onModelDirty: Event; + onModelSaveError: Event; + onModelSaved: Event; + onModelReverted: Event; + get(resource: URI): ITextFileEditorModel; getAll(resource?: URI): ITextFileEditorModel[]; diff --git a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts index c045895aec0..f5a8f581eae 100644 --- a/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/electron-browser/dirtyFilesTracker.ts @@ -8,7 +8,7 @@ import nls = require('vs/nls'); import errors = require('vs/base/common/errors'); import {IWorkbenchContribution} from 'vs/workbench/common/contributions'; -import {VIEWLET_ID, TextFileChangeEvent, EventType as FileEventType, ITextFileService, AutoSaveMode} from 'vs/workbench/parts/files/common/files'; +import {VIEWLET_ID, TextFileModelChangeEvent, ITextFileService, AutoSaveMode} from 'vs/workbench/parts/files/common/files'; import {platform, Platform} from 'vs/base/common/platform'; import {IWindowService} from 'vs/workbench/services/window/electron-browser/windowService'; import {IEventService} from 'vs/platform/event/common/event'; @@ -57,10 +57,10 @@ export class DirtyFilesTracker implements IWorkbenchContribution { // Local text file changes this.toUnbind.push(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_DIRTY, (e: TextFileChangeEvent) => this.onTextFileDirty(e))); - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_SAVED, (e: TextFileChangeEvent) => this.onTextFileSaved(e))); - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_SAVE_ERROR, (e: TextFileChangeEvent) => this.onTextFileSaveError(e))); - this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_REVERTED, (e: TextFileChangeEvent) => this.onTextFileReverted(e))); + this.toUnbind.push(this.textFileService.models.onModelDirty(e => this.onTextFileDirty(e))); + this.toUnbind.push(this.textFileService.models.onModelSaved(e => this.onTextFileSaved(e))); + this.toUnbind.push(this.textFileService.models.onModelSaveError(e => this.onTextFileSaveError(e))); + this.toUnbind.push(this.textFileService.models.onModelReverted(e => this.onTextFileReverted(e))); // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); @@ -78,7 +78,7 @@ export class DirtyFilesTracker implements IWorkbenchContribution { } } - private onTextFileDirty(e: TextFileChangeEvent): void { + private onTextFileDirty(e: TextFileModelChangeEvent): void { if ((this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) && !this.isDocumentedEdited) { this.updateDocumentEdited(); // no indication needed when auto save is enabled for short delay } @@ -119,7 +119,7 @@ export class DirtyFilesTracker implements IWorkbenchContribution { })).done(null, errors.onUnexpectedError); } - private onTextFileSaved(e: TextFileChangeEvent): void { + private onTextFileSaved(e: TextFileModelChangeEvent): void { if (this.isDocumentedEdited) { this.updateDocumentEdited(); } @@ -129,7 +129,7 @@ export class DirtyFilesTracker implements IWorkbenchContribution { } } - private onTextFileSaveError(e: TextFileChangeEvent): void { + private onTextFileSaveError(e: TextFileModelChangeEvent): void { if (!this.isDocumentedEdited) { this.updateDocumentEdited(); } @@ -137,7 +137,7 @@ export class DirtyFilesTracker implements IWorkbenchContribution { this.updateActivityBadge(); } - private onTextFileReverted(e: TextFileChangeEvent): void { + private onTextFileReverted(e: TextFileModelChangeEvent): void { if (this.isDocumentedEdited) { this.updateDocumentEdited(); } 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 8d092364f63..7074ea86830 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorModel.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorModel.test.ts @@ -14,7 +14,7 @@ import paths = require('vs/base/common/paths'); import {EncodingMode} from 'vs/workbench/common/editor'; import {TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; import {IEventService} from 'vs/platform/event/common/event'; -import {EventType, ITextFileService, ModelState} from 'vs/workbench/parts/files/common/files'; +import {EventType, ITextFileService, ModelState, StateChange} from 'vs/workbench/parts/files/common/files'; import {workbenchInstantiationService, TestTextFileService} from 'vs/test/utils/servicesTestUtils'; import {TextFileEditorModelManager} from 'vs/workbench/parts/files/common/editors/textFileEditorModelManager'; import {FileOperationResult, IFileOperationResult} from 'vs/platform/files/common/files'; @@ -103,12 +103,8 @@ suite('Files - TextFileEditorModel', () => { assert.ok(false); }); - accessor.eventService.addListener2(EventType.FILE_DIRTY, () => { - assert.ok(false); - }); - - accessor.eventService.addListener2(EventType.FILE_SAVED, () => { - assert.ok(false); + model.onDidStateChange(e => { + assert.ok(e !== StateChange.DIRTY && e !== StateChange.SAVED); }); model.load().then(() => { @@ -141,12 +137,15 @@ suite('Files - TextFileEditorModel', () => { test('Revert', function (done) { let eventCounter = 0; - accessor.eventService.addListener2(EventType.FILE_REVERTED, () => { - eventCounter++; - }); const model = instantiationService.createInstance(TextFileEditorModel, toResource('/path/index_async.txt'), 'utf8'); + model.onDidStateChange(e => { + if (e === StateChange.REVERTED) { + eventCounter++; + } + }); + model.load().then(() => { model.textEditorModel.setValue('foo'); @@ -217,12 +216,10 @@ suite('Files - TextFileEditorModel', () => { (model).autoSaveAfterMillies = 10; (model).autoSaveAfterMilliesEnabled = true; - accessor.eventService.addListener2(EventType.FILE_DIRTY, () => { - eventCounter++; - }); - - accessor.eventService.addListener2(EventType.FILE_SAVED, () => { - eventCounter++; + model.onDidStateChange(e => { + if (e === StateChange.DIRTY || e === StateChange.SAVED) { + eventCounter++; + } }); model.load().then(() => { @@ -282,10 +279,12 @@ suite('Files - TextFileEditorModel', () => { let eventCounter = 0; const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/index_async.txt'), 'utf8'); - accessor.eventService.addListener2(EventType.FILE_SAVED, (e) => { - assert.equal(model.getValue(), 'bar'); - assert.ok(!model.isDirty()); - eventCounter++; + model.onDidStateChange(e => { + if (e === StateChange.SAVED) { + assert.equal(model.getValue(), 'bar'); + assert.ok(!model.isDirty()); + eventCounter++; + } }); accessor.eventService.addListener2(EventType.FILE_SAVING, (e) => { diff --git a/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts index 6b30896867c..f80699340d4 100644 --- a/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts @@ -113,6 +113,8 @@ suite('Files - TextFileEditorModelManager', () => { assert.notEqual(model3, model2); assert.equal(manager.get(resource), model3); + model3.dispose(); + done(); }); }); @@ -158,6 +160,7 @@ suite('Files - TextFileEditorModelManager', () => { accessor.editorGroupService.fireChange(); assert.ok(model.isDisposed()); + model.dispose(); manager.dispose(); }); @@ -176,6 +179,8 @@ suite('Files - TextFileEditorModelManager', () => { assert.ok(model.isDisposed()); + model.dispose(); + manager.dispose(); }); @@ -252,9 +257,55 @@ suite('Files - TextFileEditorModelManager', () => { assert.ok(!model.isDisposed()); + model.dispose(); manager.dispose(); done(); }); }); }); + + test('events', function (done) { + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + + const resource1 = toResource('/path/index.txt'); + const resource2 = toResource('/path/other.txt'); + + let dirtyCounter = 0; + let revertedCounter = 0; + let savedCounter = 0; + + manager.onModelDirty(e => { + dirtyCounter++; + assert.equal(e.resource.toString(), resource1.toString()); + }); + + manager.onModelReverted(e => { + revertedCounter++; + assert.equal(e.resource.toString(), resource1.toString()); + }); + + manager.onModelSaved(e => { + savedCounter++; + assert.equal(e.resource.toString(), resource1.toString()); + }); + + manager.loadOrCreate(resource1, 'utf8').then(model1 => { + return manager.loadOrCreate(resource2, 'utf8').then(model2 => { + model1.textEditorModel.setValue('changed'); + + return model1.revert().then(() => { + model1.textEditorModel.setValue('changed again'); + + return model1.save().then(() => { + model1.dispose(); + model2.dispose(); + + return model1.revert().then(() => { // should not trigger another event if disposed + done(); + }); + }); + }); + }); + }); + }); }); \ No newline at end of file diff --git a/src/vs/workbench/parts/git/browser/gitServices.ts b/src/vs/workbench/parts/git/browser/gitServices.ts index e8cd1548c09..fc997319032 100644 --- a/src/vs/workbench/parts/git/browser/gitServices.ts +++ b/src/vs/workbench/parts/git/browser/gitServices.ts @@ -20,7 +20,7 @@ import { IFileStatus, IGitServiceError, GitErrorCodes, Status, StatusType, AutoF import { Model } from 'vs/workbench/parts/git/common/gitModel'; import { NativeGitIndexStringEditorInput, GitIndexDiffEditorInput, GitWorkingTreeDiffEditorInput, GitDiffEditorInput } from 'vs/workbench/parts/git/browser/gitEditorInputs'; import { GitOperation } from 'vs/workbench/parts/git/browser/gitOperations'; -import { EventType as WorkbenchFileEventType, TextFileChangeEvent } from 'vs/workbench/parts/files/common/files'; +import { TextFileModelChangeEvent, ITextFileService } from 'vs/workbench/parts/files/common/files'; import { IFileService, EventType as FileEventType, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer, PeriodThrottledDelayer } from 'vs/base/common/async'; import severity from 'vs/base/common/severity'; @@ -396,6 +396,7 @@ export class GitService extends EventEmitter private eventService: IEventService; private contextService: IWorkspaceContextService; private messageService: IMessageService; + private textFileService: ITextFileService; private instantiationService: IInstantiationService; private editorService: IWorkbenchEditorService; private lifecycleService: ILifecycleService; @@ -433,6 +434,7 @@ export class GitService extends EventEmitter @IMessageService messageService: IMessageService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IOutputService outputService: IOutputService, + @ITextFileService textFileService: ITextFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @ILifecycleService lifecycleService: ILifecycleService, @IStorageService storageService: IStorageService, @@ -444,6 +446,7 @@ export class GitService extends EventEmitter this.eventService = eventService; this.messageService = messageService; this.editorService = editorService; + this.textFileService = textFileService; this.outputService = outputService; this.contextService = contextService; this.lifecycleService = lifecycleService; @@ -500,8 +503,8 @@ export class GitService extends EventEmitter private registerListeners(): void { this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_CHANGES, (e) => this.onFileChanges(e))); - this.toDispose.push(this.eventService.addListener2(WorkbenchFileEventType.FILE_SAVED, (e) => this.onTextFileChange(e))); - this.toDispose.push(this.eventService.addListener2(WorkbenchFileEventType.FILE_REVERTED, (e) => this.onTextFileChange(e))); + this.toDispose.push(this.textFileService.models.onModelSaved((e) => this.onTextFileChange(e))); + this.toDispose.push(this.textFileService.models.onModelReverted((e) => this.onTextFileChange(e))); this.toDispose.push(this.configurationService.onDidUpdateConfiguration(() => { if (this._allowHugeRepositories) { return; @@ -529,7 +532,7 @@ export class GitService extends EventEmitter this.toDispose.push(blurEvent(() => this.isFocused = false)); } - private onTextFileChange(e: TextFileChangeEvent): void { + private onTextFileChange(e: TextFileModelChangeEvent): void { var shouldTriggerStatus = paths.basename(e.resource.fsPath) === '.gitignore'; if (!shouldTriggerStatus) { diff --git a/src/vs/workbench/parts/git/electron-browser/electronGitService.ts b/src/vs/workbench/parts/git/electron-browser/electronGitService.ts index cb677cf7595..59e84d0d967 100644 --- a/src/vs/workbench/parts/git/electron-browser/electronGitService.ts +++ b/src/vs/workbench/parts/git/electron-browser/electronGitService.ts @@ -8,6 +8,7 @@ import { IRawGitService, RawServiceState, IGitConfiguration } from 'vs/workbench import { NoOpGitService } from 'vs/workbench/parts/git/common/noopGitService'; import { GitService } from 'vs/workbench/parts/git/browser/gitServices'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ITextFileService } from 'vs/workbench/parts/files/common/files'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -199,6 +200,7 @@ export class ElectronGitService extends GitService { @IMessageService messageService: IMessageService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IOutputService outputService: IOutputService, + @ITextFileService textFileService: ITextFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @ILifecycleService lifecycleService: ILifecycleService, @IStorageService storageService: IStorageService, @@ -228,6 +230,6 @@ export class ElectronGitService extends GitService { } } - super(raw, instantiationService, eventService, messageService, editorService, outputService, contextService, lifecycleService, storageService, configurationService); + super(raw, instantiationService, eventService, messageService, editorService, outputService, textFileService, contextService, lifecycleService, storageService, configurationService); } } -- GitLab