diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index e0874f5d515..4a3056c3d5f 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -21,7 +21,7 @@ import errors = require('vs/base/common/errors'); import {Scope as MementoScope} from 'vs/workbench/common/memento'; import {Scope} from 'vs/workbench/browser/actionBarRegistry'; import {Part} from 'vs/workbench/browser/part'; -import {EventType as WorkbenchEventType, EditorInputEvent, EditorEvent} from 'vs/workbench/common/events'; +import {EventType as WorkbenchEventType, EditorEvent} from 'vs/workbench/common/events'; import {IEditorRegistry, Extensions as EditorExtensions, BaseEditor, EditorDescriptor} from 'vs/workbench/browser/parts/editor/baseEditor'; import {EditorInput, EditorOptions, TextEditorOptions, ConfirmResult} from 'vs/workbench/common/editor'; import {BaseTextEditor} from 'vs/workbench/browser/parts/editor/textEditor'; @@ -122,17 +122,19 @@ export class EditorPart extends Part implements IEditorPart { } private registerListeners(): void { - this.toUnbind.push(this.eventService.addListener2(WorkbenchEventType.EDITOR_INPUT_DIRTY_STATE_CHANGED, (event: EditorInputEvent) => this.onEditorInputDirtyStateChanged(event))); + this.toUnbind.push(this.stacks.onEditorDirty(identifier => this.onEditorDirty(identifier))); this.toUnbind.push(this.stacks.onEditorDisposed(identifier => this.onEditorDisposed(identifier))); } - private onEditorInputDirtyStateChanged(event: EditorInputEvent): void { + private onEditorDirty(identifier: IEditorIdentifier): void { + const position = this.stacks.positionOfGroup(identifier.group); + const group = identifier.group; - // we pin every editor that becomes dirty across all groups - this.stacks.groups.forEach(group => group.contains(event.editorInput) && this.pinEditor(this.stacks.positionOfGroup(group), event.editorInput)); + // we pin every editor that becomes dirty + this.pinEditor(position, identifier.editor, false /* we update the UI right after */); // Update UI - this.sideBySideControl.updateTitleArea(event.editorInput); + this.sideBySideControl.updateTitleArea({ position, preview: group.previewEditor, editorCount: group.count }); } private onEditorDisposed(identifier: IEditorIdentifier): void { @@ -1026,7 +1028,7 @@ export class EditorPart extends Part implements IEditorPart { } } - public pinEditor(position: Position, input: EditorInput): void { + public pinEditor(position: Position, input: EditorInput, updateTitleArea = true): void { const group = this.stacks.groupAt(position); if (group) { if (group.isPinned(input)) { @@ -1037,7 +1039,9 @@ export class EditorPart extends Part implements IEditorPart { group.pin(input); // Update UI - this.sideBySideControl.updateTitleArea({ position, preview: group.previewEditor, editorCount: group.count }); + if (updateTitleArea) { + this.sideBySideControl.updateTitleArea({ position, preview: group.previewEditor, editorCount: group.count }); + } } } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts index 0e8b81231c5..93cfeb992d8 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts @@ -20,7 +20,7 @@ import {Dimension, Builder, $} from 'vs/base/browser/builder'; import {Sash, ISashEvent, IVerticalSashLayoutProvider} from 'vs/base/browser/ui/sash/sash'; import {ProgressBar} from 'vs/base/browser/ui/progressbar/progressbar'; import {BaseEditor, IEditorInputActionContext} from 'vs/workbench/browser/parts/editor/baseEditor'; -import {EditorInput, isInputRelated} from 'vs/workbench/common/editor'; +import {EditorInput} from 'vs/workbench/common/editor'; import {EventType as BaseEventType} from 'vs/base/common/events'; import DOM = require('vs/base/browser/dom'); import {IActionItem, ActionsOrientation, Separator} from 'vs/base/browser/ui/actionbar/actionbar'; @@ -86,7 +86,6 @@ export interface ISideBySideEditorControl { recreateTitleArea(states: ITitleAreaState[]): void; updateTitleArea(state: ITitleAreaState): void; - updateTitleArea(input: EditorInput): void; clearTitleArea(position: Position): void; setTitleLabel(position: Position, input: EditorInput, isPinned: boolean, isActive: boolean): void; @@ -1178,49 +1177,32 @@ export class SideBySideEditorControl implements ISideBySideEditorControl, IVerti return actionItem; } - public updateTitleArea(state: ITitleAreaState): void; - public updateTitleArea(input: EditorInput): void; - public updateTitleArea(arg1: any): void { + public updateTitleArea(state: ITitleAreaState): void { + let editor = this.visibleEditors[state.position]; + let input = editor ? editor.input : null; - // Update all title areas that relate to given input if provided - if (arg1 instanceof EditorInput) { - const input: EditorInput = arg1; + if (input && editor) { - // Update the input title actions in each position according to the new status - POSITIONS.forEach((position) => { - if (this.visibleEditors[position] && isInputRelated(this.visibleEditors[position].input, input)) { - this.closeEditorActions[position].class = input.isDirty() ? 'close-editor-dirty-action' : 'close-editor-action'; - } - }); - } + // Dirty + this.closeEditorActions[state.position].class = input.isDirty() ? 'close-editor-dirty-action' : 'close-editor-action'; - // Otherwise update specific title position - else { - const state: ITitleAreaState = arg1; + // Pinned + const isPinned = !input.matches(state.preview); + if (isPinned) { + this.titleContainer[state.position].addClass('pinned'); + } else { + this.titleContainer[state.position].removeClass('pinned'); + } - let editor = this.visibleEditors[state.position]; - let input = editor ? editor.input : null; - - if (input && editor) { - - // Pinned - const isPinned = !input.matches(state.preview); - if (isPinned) { - this.titleContainer[state.position].addClass('pinned'); - } else { - this.titleContainer[state.position].removeClass('pinned'); - } - - // Overflow - const isOverflowing = state.editorCount > 1; - const showEditorAction = this.showEditorsOfGroup[state.position]; - if (!isOverflowing) { - showEditorAction.class = 'show-group-editors-overflowing-action-hidden'; - showEditorAction.enabled = false; - } else { - showEditorAction.class = 'show-group-editors-action'; - showEditorAction.enabled = true; - } + // Overflow + const isOverflowing = state.editorCount > 1; + const showEditorAction = this.showEditorsOfGroup[state.position]; + if (!isOverflowing) { + showEditorAction.class = 'show-group-editors-overflowing-action-hidden'; + showEditorAction.enabled = false; + } else { + showEditorAction.class = 'show-group-editors-action'; + showEditorAction.enabled = true; } } } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index e9da47698eb..3e5389f08eb 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -6,6 +6,7 @@ import {TPromise} from 'vs/base/common/winjs.base'; import {EventEmitter} from 'vs/base/common/eventEmitter'; +import Event, {Emitter} from 'vs/base/common/event'; import types = require('vs/base/common/types'); import URI from 'vs/base/common/uri'; import {IEditor, IEditorViewState, IRange} from 'vs/editor/common/editorCommon'; @@ -24,14 +25,23 @@ export enum ConfirmResult { * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. */ export abstract class EditorInput extends EventEmitter implements IEditorInput { + protected _onDidChangeDirty: Emitter; private disposed: boolean; constructor() { super(); + this._onDidChangeDirty = new Emitter(); this.disposed = false; } + /** + * Fired when the dirty state of this input changes. + */ + public get onDidChangeDirty(): Event { + return this._onDidChangeDirty.event; + } + /** * Returns the name of this input that can be shown to the user. Examples include showing the name of the input * above the editor area when the input is shown. @@ -565,23 +575,4 @@ export function asFileEditorInput(obj: any, supportDiff?: boolean): IFileEditorI let i = obj; return i instanceof EditorInput && types.areFunctions(i.setResource, i.setMime, i.setEncoding, i.getEncoding, i.getResource, i.getMime) ? i : null; -} - -export function isInputRelated(sourceInput: EditorInput, targetInput: EditorInput): boolean { - if (!sourceInput || !targetInput) { - return false; - } - - if (sourceInput.matches(targetInput)) { - return true; - } - - if (sourceInput instanceof BaseDiffEditorInput) { - let modifiedInput = (sourceInput).getModifiedInput(); - if (modifiedInput && modifiedInput.matches(targetInput)) { - return true; - } - } - - return false; } \ No newline at end of file diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index e02eeff9e63..c3ba06f9578 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -57,6 +57,9 @@ export class DiffEditorInput extends BaseDiffEditorInput { this.dispose(); } })); + + // When the modified model gets dirty, re-emit this to the outside + this._toUnbind.push(this.modifiedInput.onDidChangeDirty(() => this._onDidChangeDirty.fire())); } public get toUnbind() { diff --git a/src/vs/workbench/common/editor/editorStacksModel.ts b/src/vs/workbench/common/editor/editorStacksModel.ts index 17f32ad340f..e0698e265a2 100644 --- a/src/vs/workbench/common/editor/editorStacksModel.ts +++ b/src/vs/workbench/common/editor/editorStacksModel.ts @@ -134,6 +134,7 @@ export class EditorGroup implements IEditorGroup { private _onEditorOpened: Emitter; private _onEditorClosed: Emitter; private _onEditorDisposed: Emitter; + private _onEditorDirty: Emitter; private _onEditorMoved: Emitter; private _onEditorPinned: Emitter; private _onEditorUnpinned: Emitter; @@ -154,12 +155,13 @@ export class EditorGroup implements IEditorGroup { this._onEditorOpened = new Emitter(); this._onEditorClosed = new Emitter(); this._onEditorDisposed = new Emitter(); + this._onEditorDirty = new Emitter(); this._onEditorMoved = new Emitter(); this._onEditorPinned = new Emitter(); this._onEditorUnpinned = new Emitter(); this._onEditorChanged = new Emitter(); - this.toDispose.push(this._onEditorActivated, this._onEditorOpened, this._onEditorClosed, this._onEditorDisposed, this._onEditorMoved, this._onEditorPinned, this._onEditorUnpinned, this._onEditorChanged); + this.toDispose.push(this._onEditorActivated, this._onEditorOpened, this._onEditorClosed, this._onEditorDisposed, this._onEditorDirty, this._onEditorMoved, this._onEditorPinned, this._onEditorUnpinned, this._onEditorChanged); if (typeof arg1 === 'object') { this.deserialize(arg1); @@ -200,6 +202,10 @@ export class EditorGroup implements IEditorGroup { return this._onEditorDisposed.event; } + public get onEditorDirty(): Event { + return this._onEditorDirty.event; + } + public get onEditorMoved(): Event { return this._onEditorMoved.event; } @@ -325,21 +331,26 @@ export class EditorGroup implements IEditorGroup { } private hookEditorListeners(editor: EditorInput): void { + const unbind: IDisposable[] = []; // Re-emit disposal of editor input as our own event - const l1 = editor.addOneTimeDisposableListener('dispose', () => { + unbind.push(editor.addOneTimeDisposableListener('dispose', () => { if (this.indexOf(editor) >= 0) { this._onEditorDisposed.fire(editor); } - }); + })); + + // Re-Emit dirty state changes + unbind.push(editor.onDidChangeDirty(() => { + this._onEditorDirty.fire(editor); + })); // Clean up dispose listeners once the editor gets closed - const l2 = this.onEditorClosed(event => { + unbind.push(this.onEditorClosed(event => { if (event.editor.matches(editor)) { - l1.dispose(); - l2.dispose(); + dispose(unbind); } - }); + })); } public closeEditor(editor: EditorInput, openNext = true): void { @@ -666,6 +677,7 @@ export class EditorStacksModel implements IEditorStacksModel { private _onGroupRenamed: Emitter; private _onModelChanged: Emitter; private _onEditorDisposed: Emitter; + private _onEditorDirty: Emitter; constructor( @IStorageService private storageService: IStorageService, @@ -687,8 +699,9 @@ export class EditorStacksModel implements IEditorStacksModel { this._onGroupRenamed = new Emitter(); this._onModelChanged = new Emitter(); this._onEditorDisposed = new Emitter(); + this._onEditorDirty = new Emitter(); - this.toDispose.push(this._onGroupOpened, this._onGroupClosed, this._onGroupActivated, this._onGroupMoved, this._onGroupRenamed, this._onModelChanged, this._onEditorDisposed); + this.toDispose.push(this._onGroupOpened, this._onGroupClosed, this._onGroupActivated, this._onGroupMoved, this._onGroupRenamed, this._onModelChanged, this._onEditorDisposed, this._onEditorDirty); this.registerListeners(); } @@ -725,6 +738,10 @@ export class EditorStacksModel implements IEditorStacksModel { return this._onEditorDisposed.event; } + public get onEditorDirty(): Event { + return this._onEditorDirty.event; + } + public get groups(): EditorGroup[] { this.ensureLoaded(); @@ -1081,14 +1098,16 @@ export class EditorStacksModel implements IEditorStacksModel { this.groupToIdentifier[group.id] = group; // Funnel editor changes in the group through our event aggregator - const l1 = group.onEditorChanged(e => this._onModelChanged.fire(group)); - const l2 = group.onEditorClosed(e => this.onEditorClosed(e)); - const l3 = group.onEditorDisposed(editor => this._onEditorDisposed.fire({ editor, group })); - const l4 = this.onGroupClosed(g => { + const unbind: IDisposable[] = []; + unbind.push(group.onEditorChanged(e => this._onModelChanged.fire(group))); + unbind.push(group.onEditorClosed(e => this.onEditorClosed(e))); + unbind.push(group.onEditorDisposed(editor => this._onEditorDisposed.fire({ editor, group }))); + unbind.push(group.onEditorDirty(editor => this._onEditorDirty.fire({ editor, group }))); + unbind.push(this.onGroupClosed(g => { if (g === group) { - dispose(l1, l2, l3, l4); + dispose(unbind); } - }); + })); return group; } diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 85a4dba3d98..d7c88681b2e 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -15,6 +15,9 @@ import {IInstantiationService} from 'vs/platform/instantiation/common/instantiat import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {IModeService} from 'vs/editor/common/services/modeService'; +import {IDisposable, dispose} from 'vs/base/common/lifecycle'; +import {IEventService} from 'vs/platform/event/common/event'; +import {EventType as WorkbenchEventType, UntitledEditorEvent} from 'vs/workbench/common/events'; import {ITextFileService} from 'vs/workbench/parts/files/common/files'; // TODO@Ben layer breaker @@ -31,6 +34,8 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { private modeId: string; private cachedModel: UntitledEditorModel; + private toUnbind: IDisposable[]; + constructor( resource: URI, hasAssociatedFilePath: boolean, @@ -39,13 +44,28 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { @ILifecycleService private lifecycleService: ILifecycleService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IModeService private modeService: IModeService, - @ITextFileService private textFileService: ITextFileService + @ITextFileService private textFileService: ITextFileService, + @IEventService private eventService: IEventService ) { super(); this.resource = resource; this.hasAssociatedFilePath = hasAssociatedFilePath; this.modeId = modeId; + this.toUnbind = []; + + this.registerListeners(); + } + + private registerListeners(): void { + this.toUnbind.push(this.eventService.addListener2(WorkbenchEventType.UNTITLED_FILE_DELETED, (e: UntitledEditorEvent) => this.onDirtyStateChange(e))); + this.toUnbind.push(this.eventService.addListener2(WorkbenchEventType.UNTITLED_FILE_DIRTY, (e: UntitledEditorEvent) => this.onDirtyStateChange(e))); + } + + private onDirtyStateChange(e: UntitledEditorEvent): void { + if (e.resource.toString() === this.resource.toString()) { + this._onDidChangeDirty.fire(); + } } public getTypeId(): string { @@ -158,11 +178,16 @@ export class UntitledEditorInput extends AbstractUntitledEditorInput { } public dispose(): void { - super.dispose(); + // Listeners + dispose(this.toUnbind); + + // Model if (this.cachedModel) { this.cachedModel.dispose(); this.cachedModel = null; } + + super.dispose(); } } \ No newline at end of file diff --git a/src/vs/workbench/common/events.ts b/src/vs/workbench/common/events.ts index 77b39dd8cbb..0e7ef0608c2 100644 --- a/src/vs/workbench/common/events.ts +++ b/src/vs/workbench/common/events.ts @@ -52,11 +52,6 @@ export class EventType { */ static EDITOR_INPUT_CHANGED = 'editorInputChanged'; - /** - * Event type for when the editor input dirty state changed. - */ - static EDITOR_INPUT_DIRTY_STATE_CHANGED = 'editorInputDirtyStateChanged'; - /** * Event type for when the editor input failed to be set to the editor. */ diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 9dfe97c57c9..ff116a7ed93 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -53,6 +53,11 @@ export class ElectronWindow { // React to editor input changes (Mac only) if (platform.platform === platform.Platform.Mac) { this.eventService.addListener2(EventType.EDITOR_INPUT_CHANGED, (e: EditorEvent) => { + let activeEditor = this.editorService.getActiveEditor(); + if (activeEditor !== e.editor) { + return; // only care about active editor + } + let fileInput = workbenchEditorCommon.asFileEditorInput(e.editorInput, true); let representedFilename = ''; if (fileInput) { diff --git a/src/vs/workbench/parts/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/browser/editors/fileEditorInput.ts index 629d0a5d632..90b72fc35fa 100644 --- a/src/vs/workbench/parts/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/browser/editors/fileEditorInput.ts @@ -18,10 +18,12 @@ import {IEditorRegistry, Extensions, EditorDescriptor} from 'vs/workbench/browse import {BinaryEditorModel} from 'vs/workbench/common/editor/binaryEditorModel'; import {IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files'; import {FileEditorDescriptor} from 'vs/workbench/parts/files/browser/files'; -import {ITextFileService, BINARY_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID, FileEditorInput as CommonFileEditorInput, AutoSaveMode, ModelState} from 'vs/workbench/parts/files/common/files'; +import {ITextFileService, BINARY_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID, FileEditorInput as CommonFileEditorInput, AutoSaveMode, ModelState, EventType as FileEventType, TextFileChangeEvent} from 'vs/workbench/parts/files/common/files'; import {CACHE, TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; +import {IDisposable, dispose} from 'vs/base/common/lifecycle'; +import {IEventService} from 'vs/platform/event/common/event'; /** * A file editor input is the input type for the file editor of file system resources. @@ -42,6 +44,8 @@ export class FileEditorInput extends CommonFileEditorInput { private description: string; private verboseDescription: string; + private toUnbind: IDisposable[]; + /** * An editor input who's contents are retrieved from file services. */ @@ -49,17 +53,35 @@ export class FileEditorInput extends CommonFileEditorInput { resource: URI, mime: string, preferredEncoding: string, + @IEventService private eventService: IEventService, @IInstantiationService private instantiationService: IInstantiationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @ITextFileService private textFileService: ITextFileService ) { super(); + this.toUnbind = []; + if (resource) { this.setResource(resource); this.setMime(mime || guessMimeTypes(this.resource.fsPath).join(', ')); this.preferredEncoding = preferredEncoding; } + + this.registerListeners(); + } + + 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))); + } + + private onDirtyStateChange(e: TextFileChangeEvent): void { + if (e.resource.toString() === this.resource.toString()) { + this._onDidChangeDirty.fire(); + } } public setResource(resource: URI): void { @@ -254,9 +276,10 @@ export class FileEditorInput extends CommonFileEditorInput { } private indexOfClient(): number { - if (!types.isUndefinedOrNull(FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[this.resource.toString()])) { - for (let i = 0; i < FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[this.resource.toString()].length; i++) { - let client = FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[this.resource.toString()][i]; + const inputs = FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[this.resource.toString()]; + if (inputs) { + for (let i = 0; i < inputs.length; i++) { + let client = inputs[i]; if (client === this) { return i; } @@ -291,6 +314,9 @@ export class FileEditorInput extends CommonFileEditorInput { public dispose(force?: boolean): void { + // Listeners + dispose(this.toUnbind); + // TextFileEditorModel let cachedModel = CACHE.get(this.resource); if (cachedModel) { diff --git a/src/vs/workbench/parts/files/browser/fileTracker.ts b/src/vs/workbench/parts/files/browser/fileTracker.ts index c07522b7c03..a2014ea9ab7 100644 --- a/src/vs/workbench/parts/files/browser/fileTracker.ts +++ b/src/vs/workbench/parts/files/browser/fileTracker.ts @@ -10,7 +10,6 @@ import nls = require('vs/nls'); import {MIME_UNKNOWN} from 'vs/base/common/mime'; import URI from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); -import arrays = require('vs/base/common/arrays'); import {DiffEditorInput} from 'vs/workbench/common/editor/diffEditorInput'; import {EditorInput, EditorOptions} from 'vs/workbench/common/editor'; import {BaseEditor} from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -82,49 +81,32 @@ export class FileTracker implements IWorkbenchContribution { } private onTextFileDirty(e: TextFileChangeEvent): void { - this.emitInputDirtyStateChangeEvent(e.resource, true); - if (this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) { this.updateActivityBadge(); // no indication needed when auto save is enabled for short delay } } private onTextFileSaveError(e: TextFileChangeEvent): void { - this.emitInputDirtyStateChangeEvent(e.resource, true); this.updateActivityBadge(); } private onTextFileSaved(e: TextFileChangeEvent): void { - this.emitInputDirtyStateChangeEvent(e.resource, false); - if (this.lastDirtyCount > 0) { this.updateActivityBadge(); } } private onTextFileReverted(e: TextFileChangeEvent): void { - this.emitInputDirtyStateChangeEvent(e.resource, false); - if (this.lastDirtyCount > 0) { this.updateActivityBadge(); } } private onUntitledEditorDirty(e: UntitledEditorEvent): void { - let input = this.untitledEditorService.get(e.resource); - if (input) { - this.eventService.emit(WorkbenchEventType.EDITOR_INPUT_DIRTY_STATE_CHANGED, new EditorInputEvent(input)); - } - this.updateActivityBadge(); } private onUntitledEditorDeleted(e: UntitledEditorEvent): void { - let input = this.untitledEditorService.get(e.resource); - if (input) { - this.eventService.emit(WorkbenchEventType.EDITOR_INPUT_DIRTY_STATE_CHANGED, new EditorInputEvent(input)); - } - if (this.lastDirtyCount > 0) { this.updateActivityBadge(); } @@ -140,29 +122,6 @@ export class FileTracker implements IWorkbenchContribution { } } - private emitInputDirtyStateChangeEvent(resource: URI, gotDirty: boolean): void { - - // Find all file editor inputs that are open from the given file resource and emit a editor input state change event. - // We could do all of this within the file editor input but having all the file change listeners in - // one place is more elegant and keeps the logic together at once place. - const editors = arrays.flatten(this.stacks.groups.map(g => g.getEditors())); - editors.forEach(input => { - if (this.matchesResource(input, resource)) { - this.eventService.emit(WorkbenchEventType.EDITOR_INPUT_DIRTY_STATE_CHANGED, new EditorInputEvent(input)); - } - }); - } - - private matchesResource(input: EditorInput, resource: URI): boolean { - - // Diff Editor Input - if (input instanceof DiffEditorInput) { - input = (input).getModifiedInput(); - } - - return input instanceof FileEditorInput && input.getResource().toString() === resource.toString(); - } - // Note: there is some duplication with the other file event handler below. Since we cannot always rely on the disk events // carrying all necessary data in all environments, we also use the local file events to make sure operations are handled. // In any case there is no guarantee if the local event is fired first or the disk one. Thus, code must handle the case diff --git a/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts index 7707f3daf1e..e37e6dc03be 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts @@ -115,8 +115,18 @@ suite('Files - FileEditorInput', () => { }); test('Input.matches() - FileEditorInput', function () { - let fileEditorInput = new FileEditorInput(toResource('/foo/bar/updatefile.js'), 'text/javascript', void 0, void 0, void 0, void 0); - let contentEditorInput2 = new FileEditorInput(toResource('/foo/bar/updatefile.js'), 'text/javascript', void 0, void 0, void 0, void 0); + let eventService = new TestEventService(); + let contextService = new TestContextService(); + + let services = new ServiceCollection(); + let instantiationService = new InstantiationService(services); + + services.set(IEventService, eventService); + services.set(IWorkspaceContextService, contextService); + services.set(ITextFileService, instantiationService.createInstance( TextFileService)); + + let fileEditorInput = instantiationService.createInstance(FileEditorInput, toResource('/foo/bar/updatefile.js'), 'text/javascript', void 0); + let contentEditorInput2 = instantiationService.createInstance(FileEditorInput, toResource('/foo/bar/updatefile.js'), 'text/javascript', void 0); assert.strictEqual(fileEditorInput.matches(null), false); assert.strictEqual(fileEditorInput.matches(fileEditorInput), true); diff --git a/src/vs/workbench/parts/files/test/browser/textFileEditor.test.ts b/src/vs/workbench/parts/files/test/browser/textFileEditor.test.ts index a5b20a85c5f..689bf76d7e1 100644 --- a/src/vs/workbench/parts/files/test/browser/textFileEditor.test.ts +++ b/src/vs/workbench/parts/files/test/browser/textFileEditor.test.ts @@ -13,6 +13,13 @@ import {Registry} from 'vs/platform/platform'; import {SyncDescriptor} from 'vs/platform/instantiation/common/descriptors'; import {FileEditorInput} from 'vs/workbench/parts/files/browser/editors/fileEditorInput'; import {Extensions} from 'vs/workbench/browser/parts/editor/baseEditor'; +import {TestEventService, TestContextService} from 'vs/workbench/test/common/servicesTestUtils'; +import {IEventService} from 'vs/platform/event/common/event'; +import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; +import {ServiceCollection} from 'vs/platform/instantiation/common/serviceCollection'; +import {InstantiationService} from 'vs/platform/instantiation/common/instantiationService'; +import {ITextFileService} from 'vs/workbench/parts/files/common/files'; +import {TextFileService} from 'vs/workbench/parts/files/browser/textFileServices'; const ExtensionId = Extensions.Editors; @@ -37,9 +44,19 @@ suite('Files - TextFileEditor', () => { equal(Registry.as(ExtensionId).getEditors().length, oldEditorCnt + 2); equal(Registry.as(ExtensionId).getEditorInputs().length, oldInputCnt + 2); - strictEqual(Registry.as(ExtensionId).getEditor(new FileEditorInput(URI.file(join('C:\\', '/foo/bar/foobar.html')), 'test-text/html', void 0, void 0, void 0, void 0)), d1); - strictEqual(Registry.as(ExtensionId).getEditor(new FileEditorInput(URI.file(join('C:\\', '/foo/bar/foobar.js')), 'test-text/javascript', void 0, void 0, void 0, void 0)), d1); - strictEqual(Registry.as(ExtensionId).getEditor(new FileEditorInput(URI.file(join('C:\\', '/foo/bar/foobar.css')), 'test-text/css', void 0, void 0, void 0, void 0)), d2); + let eventService = new TestEventService(); + let contextService = new TestContextService(); + + let services = new ServiceCollection(); + let instantiationService = new InstantiationService(services); + + services.set(IEventService, eventService); + services.set(IWorkspaceContextService, contextService); + services.set(ITextFileService, instantiationService.createInstance( TextFileService)); + + strictEqual(Registry.as(ExtensionId).getEditor(instantiationService.createInstance(FileEditorInput, URI.file(join('C:\\', '/foo/bar/foobar.html')), 'test-text/html', void 0)), d1); + strictEqual(Registry.as(ExtensionId).getEditor(instantiationService.createInstance(FileEditorInput, URI.file(join('C:\\', '/foo/bar/foobar.js')), 'test-text/javascript', void 0)), d1); + strictEqual(Registry.as(ExtensionId).getEditor(instantiationService.createInstance(FileEditorInput, URI.file(join('C:\\', '/foo/bar/foobar.css')), 'test-text/css', void 0)), d2); Registry.as(ExtensionId).setEditors(oldEditors); }); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index c4ca11c6890..3fa2bd872f5 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -11,7 +11,7 @@ import {EventType} from 'vs/base/common/events'; import {IEditor as IBaseEditor} from 'vs/platform/editor/common/editor'; import {TextEditorOptions, EditorInput} from 'vs/workbench/common/editor'; import {BaseTextEditor} from 'vs/workbench/browser/parts/editor/textEditor'; -import {EditorEvent, TextEditorSelectionEvent, EventType as WorkbenchEventType, EditorInputEvent} from 'vs/workbench/common/events'; +import {EditorEvent, TextEditorSelectionEvent, EventType as WorkbenchEventType} from 'vs/workbench/common/events'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {IHistoryService} from 'vs/workbench/services/history/common/history'; import {Selection} from 'vs/editor/common/core/selection'; @@ -28,7 +28,6 @@ export class EditorState { private static EDITOR_SELECTION_THRESHOLD = 5; // number of lines to move in editor to justify for new state constructor(private _editorInput: IEditorInput, private _selection: Selection) { - // } public get editorInput(): IEditorInput { @@ -68,6 +67,7 @@ interface IInputWithPath { export abstract class BaseHistoryService { protected toUnbind: IDisposable[]; + private activeEditorUnbind: IDisposable; constructor( private eventService: IEventService, @@ -82,25 +82,10 @@ export abstract class BaseHistoryService { // Editor Input Changes this.toUnbind.push(this.eventService.addListener2(WorkbenchEventType.EDITOR_INPUT_CHANGED, (e: EditorEvent) => this.onEditorInputChanged(e))); - // Editor Input State Changes - this.toUnbind.push(this.eventService.addListener2(WorkbenchEventType.EDITOR_INPUT_DIRTY_STATE_CHANGED, (e: EditorInputEvent) => this.onEditorInputDirtyStateChanged(e.editorInput))); - // Text Editor Selection Changes this.toUnbind.push(this.eventService.addListener2(WorkbenchEventType.TEXT_EDITOR_SELECTION_CHANGED, (event: TextEditorSelectionEvent) => this.onTextEditorSelectionChanged(event))); } - private onEditorInputDirtyStateChanged(input: IEditorInput): void { - - // If an active editor is set, but is different from the one from the event, prevent update because the editor is not active. - let activeEditor = this.editorService.getActiveEditor(); - if (activeEditor && !input.matches(activeEditor.input)) { - return; - } - - // Calculate New Window Title - this.updateWindowTitle(input); - } - private onTextEditorSelectionChanged(event: TextEditorSelectionEvent): void { // If an active editor is set, but is different from the one from the event, prevent update because the editor is not active. @@ -115,7 +100,22 @@ export abstract class BaseHistoryService { } private onEditorInputChanged(event: EditorEvent): void { + + // Propagate to history this.onEditorEvent(event.editor); + + // Stop old listener + if (this.activeEditorUnbind) { + this.activeEditorUnbind.dispose(); + } + + // Apply listener for dirty changes + let activeInput = this.editorService.getActiveEditorInput(); + if (activeInput instanceof EditorInput) { + this.activeEditorUnbind = activeInput.onDidChangeDirty(() => { + this.updateWindowTitle(activeInput); // Calculate New Window Title when dirty state changes + }); + } } private onEditorEvent(editor: IBaseEditor): void { diff --git a/src/vs/workbench/test/browser/parts/quickOpen/quickopen.test.ts b/src/vs/workbench/test/browser/parts/quickOpen/quickopen.test.ts index c1599ed5421..474967a29f1 100644 --- a/src/vs/workbench/test/browser/parts/quickOpen/quickopen.test.ts +++ b/src/vs/workbench/test/browser/parts/quickOpen/quickopen.test.ts @@ -16,7 +16,9 @@ import {QuickOpenController} from 'vs/workbench/browser/parts/quickopen/quickOpe import {Mode} from 'vs/base/parts/quickopen/common/quickOpen'; import {StringEditorInput} from 'vs/workbench/common/editor/stringEditorInput'; import {EditorInput} from 'vs/workbench/common/editor'; +import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {isEmptyObject} from 'vs/base/common/types'; +import {IEventService} from 'vs/platform/event/common/event'; import {join} from 'vs/base/common/paths'; import {Extensions, IEditorRegistry} from 'vs/workbench/browser/parts/editor/baseEditor'; import URI from 'vs/base/common/uri'; @@ -96,10 +98,16 @@ suite('Workbench QuickOpen', () => { test('EditorHistoryModel', () => { Registry.as('workbench.contributions.editors').setInstantiationService(new InstantiationService()); + let services = new ServiceCollection(); + + let eventService = new TestEventService(); let editorService = new TestEditorService(); let contextService = new TestContextService(); - let inst = new InstantiationService(new ServiceCollection([IWorkbenchEditorService, editorService])); + services.set(IEventService, eventService); + services.set(IWorkspaceContextService, contextService); + services.set(IWorkbenchEditorService, editorService); + let inst = new InstantiationService(services); let model = new EditorHistoryModel(editorService, inst, contextService); @@ -202,13 +210,18 @@ suite('Workbench QuickOpen', () => { }); test('QuickOpenController adds to history on editor input change and can handle dispose', () => { - let editorService = new TestEditorService(); + let services = new ServiceCollection(); - let eventService = new TestEventService(); let storageService = new TestStorageService(); + let eventService = new TestEventService(); + let editorService = new TestEditorService(); let contextService = new TestContextService(); - let inst = new InstantiationService(new ServiceCollection([IWorkbenchEditorService, editorService])); + services.set(IEventService, eventService); + services.set(IWorkspaceContextService, contextService); + services.set(IWorkbenchEditorService, editorService); + + let inst = new InstantiationService(services); let controller = new QuickOpenController( eventService, @@ -226,7 +239,7 @@ suite('Workbench QuickOpen', () => { assert.equal(0, controller.getEditorHistoryModel().getEntries().length); - let cinput1 = inst.createInstance(fileInputCtor, toResource('Hello World'), 'text/plain', void 0); + let cinput1 = inst.createInstance(fileInputCtor, toResource('Hello World'), 'text/plain', null); let event = new EditorEvent(null, '', cinput1, null, Position.LEFT); eventService.emit(EventType.EDITOR_INPUT_CHANGING, event); diff --git a/src/vs/workbench/test/common/editor/editorStacksModel.test.ts b/src/vs/workbench/test/common/editor/editorStacksModel.test.ts index 41c8b35e0ce..5d69752f8ff 100644 --- a/src/vs/workbench/test/common/editor/editorStacksModel.test.ts +++ b/src/vs/workbench/test/common/editor/editorStacksModel.test.ts @@ -101,6 +101,10 @@ class TestEditorInput extends EditorInput { public matches(other: TestEditorInput): boolean { return other && this.id === other.id && other instanceof TestEditorInput; } + + public setDirty(): void { + this._onDidChangeDirty.fire(); + } } class NonSerializableTestEditorInput extends EditorInput { @@ -1608,4 +1612,42 @@ suite('Editor Stacks Model', () => { assert.equal(input2.isDisposed(), true); assert.equal(input1.isDisposed(), false); }); + + test('Stack - Multiple Editors - Editor Emits Dirty', function () { + const model = create(); + + const group1 = model.openGroup('group1'); + const group2 = model.openGroup('group2'); + + const input1 = input(); + const input2 = input(); + + group1.openEditor(input1, { pinned: true, active: true}); + group2.openEditor(input2, { pinned: true, active: true }); + + let dirtyCounter = 0; + model.onEditorDirty(() => { + dirtyCounter++; + }); + + (input1).setDirty(); + + assert.equal(dirtyCounter, 1); + + (input2).setDirty(); + + assert.equal(dirtyCounter, 2); + + group2.closeAllEditors(); + + (input2).setDirty(); + + assert.equal(dirtyCounter, 2); + + model.closeGroups(); + + (input1).setDirty(); + + assert.equal(dirtyCounter, 2); + }); }); \ No newline at end of file