diff --git a/extensions/vscode-api-tests/src/editor.test.ts b/extensions/vscode-api-tests/src/editor.test.ts index 0631d2b4b03..e916ca282eb 100644 --- a/extensions/vscode-api-tests/src/editor.test.ts +++ b/extensions/vscode-api-tests/src/editor.test.ts @@ -128,12 +128,18 @@ suite('editor tests', () => { }); return Promise.all([ - commands.executeCommand('workbench.action.closeAllEditors'), + delay(800).then(() => commands.executeCommand('workbench.action.closeAllEditors')), // TODO@Ben TODO@Joh this delay is a hack p ]).then(() => undefined); }); }); + function delay(time) { + return new Promise(function (fulfill) { + setTimeout(fulfill, time); + }); + } + test('issue #20867: vscode.window.visibleTextEditors returns closed document 2/2', () => { const file10Path = join(workspace.rootPath || '', './10linefile.ts'); @@ -165,8 +171,11 @@ suite('editor tests', () => { // hide doesn't what it means because it triggers a close event and because it // detached the editor. For this test that's what we want. - editors[0].hide(); - return p; + delay(800).then(() => { // TODO@Ben TODO@Joh this delay is a hack + editors[0].hide(); + + return p; + }); }); }); diff --git a/extensions/vscode-api-tests/src/workspace.test.ts b/extensions/vscode-api-tests/src/workspace.test.ts index d45c6bd5610..f54c5b525b6 100644 --- a/extensions/vscode-api-tests/src/workspace.test.ts +++ b/extensions/vscode-api-tests/src/workspace.test.ts @@ -60,7 +60,7 @@ suite('workspace-namespace', () => { test('openTextDocument', () => { let len = workspace.textDocuments.length; - return workspace.openTextDocument(join(workspace.rootPath || '', './far.js')).then(doc => { + return workspace.openTextDocument(join(workspace.rootPath || '', './simple.txt')).then(doc => { assert.ok(doc); assert.equal(workspace.textDocuments.length, len + 1); }); diff --git a/extensions/vscode-api-tests/testWorkspace/simple.txt b/extensions/vscode-api-tests/testWorkspace/simple.txt new file mode 100644 index 00000000000..b5462829966 --- /dev/null +++ b/extensions/vscode-api-tests/testWorkspace/simple.txt @@ -0,0 +1 @@ +Just a simple file... \ No newline at end of file diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 237ef662803..b6b52b4e303 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -69,6 +69,29 @@ export interface IResourceInput extends IBaseResourceInput { encoding?: string; } +export interface IUntitledResourceInput extends IBaseResourceInput { + + /** + * Optional resource. If the resource is not provided a new untitled file is created. + */ + resource?: URI; + + /** + * Optional file path. Using the file resource will associate the file to the untitled resource. + */ + filePath?: string; + + /** + * Optional language of the untitled resource. + */ + language?: string; + + /** + * Optional contents of the untitled resource. + */ + contents?: string; +} + export interface IResourceDiffInput extends IBaseResourceInput { /** diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 48ef5e08c2d..1aff5f9a493 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -11,7 +11,7 @@ import types = require('vs/base/common/types'); import { Builder } from 'vs/base/browser/builder'; import { Registry } from 'vs/platform/platform'; import { Panel } from 'vs/workbench/browser/panel'; -import { EditorInput, IFileEditorInput, EditorOptions, IEditorDescriptor, IEditorInputFactory, IEditorRegistry, Extensions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorDescriptor, IEditorInputFactory, IEditorRegistry, Extensions, IFileInputFactory } from 'vs/workbench/common/editor'; import { IEditor, Position, POSITIONS } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation'; import { SyncDescriptor, AsyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -160,7 +160,7 @@ const INPUT_DESCRIPTORS_PROPERTY = '__$inputDescriptors'; class EditorRegistry implements IEditorRegistry { private editors: EditorDescriptor[]; private instantiationService: IInstantiationService; - private defaultFileInputDescriptor: AsyncDescriptor; + private fileInputFactory: IFileInputFactory; private editorInputFactoryConstructors: { [editorInputId: string]: IConstructorSignature0 } = Object.create(null); private editorInputFactoryInstances: { [editorInputId: string]: IEditorInputFactory } = Object.create(null); @@ -283,12 +283,12 @@ class EditorRegistry implements IEditorRegistry { return inputClasses; } - public registerDefaultFileInput(editorInputDescriptor: AsyncDescriptor): void { - this.defaultFileInputDescriptor = editorInputDescriptor; + public registerFileInputFactory(factory: IFileInputFactory): void { + this.fileInputFactory = factory; } - public getDefaultFileInput(): AsyncDescriptor { - return this.defaultFileInputDescriptor; + public getFileInputFactory(): IFileInputFactory { + return this.fileInputFactory; } public registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0): void { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index ed6a6f78088..a7d2f169347 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -8,7 +8,6 @@ import { Registry } from 'vs/platform/platform'; import nls = require('vs/nls'); import URI from 'vs/base/common/uri'; import { Action, IAction } from 'vs/base/common/actions'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEditorQuickOpenEntry, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { StatusbarItemDescriptor, StatusbarAlignment, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -37,6 +36,7 @@ import { NAVIGATE_IN_GROUP_TWO_PREFIX, ShowEditorsInGroupThreeAction, NAVIGATE_IN_GROUP_THREE_PREFIX, FocusLastEditorInStackAction, OpenNextRecentlyUsedEditorInGroupAction, MoveEditorToPreviousGroupAction, MoveEditorToNextGroupAction, MoveEditorLeftInGroupAction, ClearRecentFilesAction } from 'vs/workbench/browser/parts/editor/editorActions'; import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands'; +import { IWorkbenchEditorService } from "vs/workbench/services/editor/common/editorService"; // Register String Editor Registry.as(EditorExtensions.Editors).registerEditor( @@ -101,7 +101,6 @@ interface ISerializedUntitledEditorInput { class UntitledEditorInputFactory implements IEditorInputFactory { constructor( - @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @ITextFileService private textFileService: ITextFileService ) { } @@ -127,10 +126,15 @@ class UntitledEditorInputFactory implements IEditorInputFactory { return JSON.stringify(serialized); } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput); + public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledEditorInput { + return instantiationService.invokeFunction(accessor => { + const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput); + const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource); + const filePath = resource.scheme === 'file' ? resource.fsPath : void 0; + const language = deserialized.modeId; - return this.untitledEditorService.createOrGet(!!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource), deserialized.modeId); + return accessor.get(IWorkbenchEditorService).createInput({ resource, filePath, language }) as UntitledEditorInput; + }); } } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 159f2508d0a..10af5c4320c 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -14,7 +14,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { MIME_BINARY } from 'vs/base/common/mime'; import { shorten } from 'vs/base/common/labels'; import { ActionRunner, IAction } from 'vs/base/common/actions'; -import { Position, IEditorInput, Verbosity } from 'vs/platform/editor/common/editor'; +import { Position, IEditorInput, Verbosity, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; import { IEditorGroup, toResource } from 'vs/workbench/common/editor'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -23,7 +23,6 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IMessageService } from 'vs/platform/message/common/message'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -66,7 +65,6 @@ export class TabsTitleControl extends TitleControl { @IInstantiationService instantiationService: IInstantiationService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IEditorGroupService editorGroupService: IEditorGroupService, - @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, @@ -143,7 +141,7 @@ export class TabsTitleControl extends TitleControl { const group = this.context; if (group) { - this.editorService.openEditor(this.untitledEditorService.createOrGet(), { pinned: true, index: group.count /* always at the end */ }).done(null, errors.onUnexpectedError); // untitled are always pinned + this.editorService.openEditor({ options: { pinned: true, index: group.count /* always at the end */ } } as IUntitledResourceInput).done(null, errors.onUnexpectedError); // untitled are always pinned } } })); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index e7ee6d0cf00..ea82395bac6 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -14,7 +14,7 @@ import { IEditor, ICommonCodeEditor, IEditorViewState, IEditorOptions as ICodeEd import { IEditorInput, IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, Position, Verbosity } from 'vs/platform/editor/common/editor'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { SyncDescriptor, AsyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -49,6 +49,10 @@ export const TEXT_DIFF_EDITOR_ID = 'workbench.editors.textDiffEditor'; */ export const BINARY_DIFF_EDITOR_ID = 'workbench.editors.binaryResourceDiffEditor'; +export interface IFileInputFactory { + createFileInput(resource: URI, encoding: string, instantiationService: IInstantiationService): IFileEditorInput; +} + export interface IEditorRegistry { /** @@ -79,20 +83,14 @@ export interface IEditorRegistry { getEditors(): IEditorDescriptor[]; /** - * Registers the default input to be used for files in the workbench. - * - * @param editorInputDescriptor a descriptor that resolves to an instance of EditorInput that - * should be used to handle file inputs. + * Registers the file input factory to use for file inputs. */ - registerDefaultFileInput(editorInputDescriptor: AsyncDescriptor): void; + registerFileInputFactory(factory: IFileInputFactory): void; /** - * Returns a descriptor of the default input to be used for files in the workbench. - * - * @return a descriptor that resolves to an instance of EditorInput that should be used to handle - * file inputs. + * Returns the file input factory to use for file inputs. */ - getDefaultFileInput(): AsyncDescriptor; + getFileInputFactory(): IFileInputFactory; /** * Registers a editor input factory for the given editor input to the registry. An editor input factory @@ -329,11 +327,6 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport { */ getResource(): URI; - /** - * Sets the absolute file resource URI this input is about. - */ - setResource(resource: URI): void; - /** * Sets the preferred encodingt to use for this input. */ @@ -851,7 +844,6 @@ export interface IEditorStacksModel { next(jumpGroups: boolean, cycleAtEnd?: boolean): IEditorIdentifier; previous(jumpGroups: boolean, cycleAtStart?: boolean): IEditorIdentifier; - isOpen(editor: IEditorInput): boolean; isOpen(resource: URI): boolean; toString(): string; @@ -869,8 +861,7 @@ export interface IEditorGroup { getEditor(resource: URI): IEditorInput; indexOf(editor: IEditorInput): number; - contains(editor: IEditorInput): boolean; - contains(resource: URI): boolean; + contains(editorOrResource: IEditorInput | URI): boolean; getEditors(mru?: boolean): IEditorInput[]; isActive(editor: IEditorInput): boolean; diff --git a/src/vs/workbench/common/editor/editorStacksModel.ts b/src/vs/workbench/common/editor/editorStacksModel.ts index 4af83be2a24..00e7d3722c2 100644 --- a/src/vs/workbench/common/editor/editorStacksModel.ts +++ b/src/vs/workbench/common/editor/editorStacksModel.ts @@ -587,14 +587,12 @@ export class EditorGroup implements IEditorGroup { return -1; } - public contains(candidate: EditorInput): boolean; - public contains(resource: URI): boolean; - public contains(arg1: any): boolean { - if (arg1 instanceof EditorInput) { - return this.indexOf(arg1) >= 0; + public contains(editorOrResource: EditorInput | URI): boolean { + if (editorOrResource instanceof EditorInput) { + return this.indexOf(editorOrResource) >= 0; } - const counter = this.mapResourceToEditorCount.get(arg1); + const counter = this.mapResourceToEditorCount.get(editorOrResource); return typeof counter === 'number' && counter > 0; } @@ -1174,32 +1172,28 @@ export class EditorStacksModel implements IEditorStacksModel { private handleOnEditorClosed(event: GroupEvent): void { const editor = event.editor; + const editorsToClose = [editor]; - // Close the editor when it is no longer open in any group - if (!this.isOpen(editor)) { - editor.close(); - - // Also take care of side by side editor inputs that wrap around 2 editors - if (editor instanceof SideBySideEditorInput) { - [editor.master, editor.details].forEach(editor => { - if (!this.isOpen(editor)) { - editor.close(); - } - }); - } + // Include both sides of side by side editors when being closed and not opened multiple times + if (editor instanceof SideBySideEditorInput && !this.isOpen(editor)) { + editorsToClose.push(editor.master, editor.details); } + + // Close the editor when it is no longer open in any group including diff editors + editorsToClose.forEach(editorToClose => { + const resource = toResource(editorToClose); // prefer resource to not close right-hand side editors of a diff editor + if (!this.isOpen(resource || editorToClose)) { + editorToClose.close(); + } + }); } - public isOpen(resource: URI): boolean; - public isOpen(editor: EditorInput): boolean; - public isOpen(arg1: any): boolean { - return this._groups.some(group => group.contains(arg1)); + public isOpen(editorOrResource: URI | EditorInput): boolean { + return this._groups.some(group => group.contains(editorOrResource)); } - public count(resource: URI): number; - public count(editor: EditorInput): number; - public count(arg1: any): number { - return this._groups.filter(group => group.contains(arg1)).length; + public count(editor: EditorInput): number { + return this._groups.filter(group => group.contains(editor)).length; } private onShutdown(): void { diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index d4f9ba53279..a4eede8ca42 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -20,9 +20,8 @@ export class ResourceEditorInput extends EditorInput { static ID: string = 'workbench.editors.resourceEditorInput'; - protected promise: TPromise>; - protected resource: URI; - + private modelReference: TPromise>; + private resource: URI; private name: string; private description: string; @@ -39,54 +38,54 @@ export class ResourceEditorInput extends EditorInput { this.resource = resource; } - getResource(): URI { + public getResource(): URI { return this.resource; } - getTypeId(): string { + public getTypeId(): string { return ResourceEditorInput.ID; } - getName(): string { + public getName(): string { return this.name; } - setName(name: string): void { + public setName(name: string): void { if (this.name !== name) { this.name = name; this._onDidChangeLabel.fire(); } } - getDescription(): string { + public getDescription(): string { return this.description; } - setDescription(description: string): void { + public setDescription(description: string): void { if (this.description !== description) { this.description = description; this._onDidChangeLabel.fire(); } } - getTelemetryDescriptor(): { [key: string]: any; } { + public getTelemetryDescriptor(): { [key: string]: any; } { const descriptor = super.getTelemetryDescriptor(); descriptor['resource'] = telemetryURIDescriptor(this.resource); return descriptor; } - resolve(refresh?: boolean): TPromise { - if (!this.promise) { - this.promise = this.textModelResolverService.createModelReference(this.resource); + public resolve(refresh?: boolean): TPromise { + if (!this.modelReference) { + this.modelReference = this.textModelResolverService.createModelReference(this.resource); } - return this.promise.then(ref => { + return this.modelReference.then(ref => { const model = ref.object; if (!(model instanceof ResourceEditorModel)) { ref.dispose(); - this.promise = null; + this.modelReference = null; return TPromise.wrapError(`Unexpected model for ResourceInput: ${this.resource}`); // TODO@Ben eventually also files should be supported, but we guard due to the dangerous dispose of the model in dispose() } @@ -94,7 +93,7 @@ export class ResourceEditorInput extends EditorInput { }); } - matches(otherInput: any): boolean { + public matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } @@ -109,10 +108,10 @@ export class ResourceEditorInput extends EditorInput { return false; } - dispose(): void { - if (this.promise) { - this.promise.done(ref => ref.dispose()); - this.promise = null; + public dispose(): void { + if (this.modelReference) { + this.modelReference.done(ref => ref.dispose()); + this.modelReference = null; } super.dispose(); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index fb554e91285..8b46d36050c 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -22,14 +22,13 @@ import { Builder, $ } from 'vs/base/browser/builder'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { toResource } from 'vs/workbench/common/editor'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkbenchEditorService, IResourceInputType } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IMessageService } from 'vs/platform/message/common/message'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IWindowsService, IWindowService, IWindowSettings } from 'vs/platform/windows/common/windows'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IWindowIPCService } from 'vs/workbench/services/window/electron-browser/windowService'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IPath, IOpenFileRequest, IWindowConfiguration } from 'vs/workbench/electron-browser/common'; @@ -44,7 +43,7 @@ import { ReloadWindowAction, ToggleDevToolsAction, ShowStartupPerformance, OpenR import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { Position, IResourceInput } from 'vs/platform/editor/common/editor'; +import { Position, IResourceInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; import { Themable, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; @@ -91,8 +90,7 @@ export class ElectronWindow extends Themable { @IViewletService private viewletService: IViewletService, @IContextMenuService private contextMenuService: IContextMenuService, @IKeybindingService private keybindingService: IKeybindingService, - @IEnvironmentService private environmentService: IEnvironmentService, - @IUntitledEditorService private untitledEditorService: IUntitledEditorService, + @IEnvironmentService private environmentService: IEnvironmentService ) { super(themeService); @@ -384,7 +382,7 @@ export class ElectronWindow extends Themable { } private onOpenFiles(request: IOpenFileRequest): void { - let inputs: IResourceInput[] = []; + let inputs: IResourceInputType[] = []; let diffMode = (request.filesToDiff.length === 2); if (!diffMode && request.filesToOpen) { @@ -404,7 +402,7 @@ export class ElectronWindow extends Themable { } } - private openResources(resources: IResourceInput[], diffMode: boolean): TPromise { + private openResources(resources: (IResourceInput | IUntitledResourceInput)[], diffMode: boolean): TPromise { return this.partService.joinCreation().then(() => { // In diffMode we open 2 resources as diff @@ -428,14 +426,15 @@ export class ElectronWindow extends Themable { }); } - private toInputs(paths: IPath[], isNew: boolean): IResourceInput[] { + private toInputs(paths: IPath[], isNew: boolean): IResourceInputType[] { return paths.map(p => { - let input = { - resource: isNew ? this.untitledEditorService.createOrGet(URI.file(p.filePath)).getResource() : URI.file(p.filePath), - options: { - pinned: true - } - }; + const resource = URI.file(p.filePath); + let input: IResourceInput | IUntitledResourceInput; + if (isNew) { + input = { filePath: resource.fsPath, options: { pinned: true } } as IUntitledResourceInput; + } else { + input = { resource, options: { pinned: true } } as IResourceInput; + } if (!isNew && p.lineNumber) { input.options.selection = { diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 67378af381a..35909f67a08 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -23,7 +23,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Registry } from 'vs/platform/platform'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { IOptions } from 'vs/workbench/common/options'; -import { Position as EditorPosition, IResourceInput, IResourceDiffInput } from 'vs/platform/editor/common/editor'; +import { Position as EditorPosition, IResourceDiffInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; @@ -39,7 +39,6 @@ import { IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs/workbe import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickOpenController'; import { getServices } from 'vs/platform/instantiation/common/extensions'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { WorkbenchEditorService } from 'vs/workbench/services/editor/browser/editorService'; import { Position, Parts, IPartService, ILayoutOptions } from 'vs/workbench/services/part/common/partService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -200,7 +199,6 @@ export class Workbench implements IPartService { options: IOptions, serviceCollection: ServiceCollection, @IInstantiationService private instantiationService: IInstantiationService, - @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IStorageService private storageService: IStorageService, @ILifecycleService private lifecycleService: ILifecycleService, @@ -382,14 +380,14 @@ export class Workbench implements IPartService { // Otherwise: Open/Create files else { - const filesToCreateInputs: IResourceInput[] = filesToCreate.map(resourceInput => { - return { - resource: this.untitledEditorService.createOrGet(resourceInput.resource).getResource(), + const filesToCreateInputs: IUntitledResourceInput[] = filesToCreate.map(resourceInput => { + return { + filePath: resourceInput.resource.fsPath, options: { pinned: true } }; }); - return TPromise.as(filesToOpen.concat(filesToCreateInputs)); + return TPromise.as([].concat(filesToOpen).concat(filesToCreateInputs)); } } @@ -400,7 +398,7 @@ export class Workbench implements IPartService { return TPromise.as([]); // do not open any empty untitled file if we have backups to restore } - return TPromise.as([{ resource: this.untitledEditorService.createOrGet().getResource() }]); + return TPromise.as([{}]); }); } diff --git a/src/vs/workbench/parts/backup/common/backupRestorer.ts b/src/vs/workbench/parts/backup/common/backupRestorer.ts index d1d430f4ec6..d32647ee465 100644 --- a/src/vs/workbench/parts/backup/common/backupRestorer.ts +++ b/src/vs/workbench/parts/backup/common/backupRestorer.ts @@ -14,9 +14,9 @@ import { IPartService } from 'vs/workbench/services/part/common/partService'; import errors = require('vs/base/common/errors'); import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Position, IResourceInput } from 'vs/platform/editor/common/editor'; +import { Position, IResourceInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; +import { ITextFileService } from "vs/workbench/services/textfile/common/textfiles"; export class BackupRestorer implements IWorkbenchContribution { @@ -28,7 +28,7 @@ export class BackupRestorer implements IWorkbenchContribution { @IPartService private partService: IPartService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IBackupFileService private backupFileService: IBackupFileService, - @ITextModelResolverService private textModelResolverService: ITextModelResolverService, + @ITextFileService private textFileService: ITextFileService, @IEditorGroupService private groupService: IEditorGroupService ) { this.restoreBackups(); @@ -68,7 +68,7 @@ export class BackupRestorer implements IWorkbenchContribution { backups.forEach(backup => { if (stacks.isOpen(backup)) { if (backup.scheme === 'file') { - restorePromises.push(this.textModelResolverService.createModelReference(backup).then(null, () => unresolved.push(backup))); + restorePromises.push(this.textFileService.models.loadOrCreate(backup).then(null, () => unresolved.push(backup))); } else if (backup.scheme === 'untitled') { restorePromises.push(this.untitledEditorService.get(backup).resolve().then(null, () => unresolved.push(backup))); } @@ -80,30 +80,27 @@ export class BackupRestorer implements IWorkbenchContribution { return TPromise.join(restorePromises).then(() => unresolved, () => unresolved); } - private doOpenEditors(inputs: URI[]): TPromise { + private doOpenEditors(resources: URI[]): TPromise { const stacks = this.groupService.getStacksModel(); const hasOpenedEditors = stacks.groups.length > 0; - return TPromise.join(inputs.map(resource => this.resolveInput(resource))).then(inputs => { - const openEditorsArgs = inputs.map((input, index) => { - return { input, options: { pinned: true, preserveFocus: true, inactive: index > 0 || hasOpenedEditors }, position: Position.ONE }; - }); - - // Open all remaining backups as editors and resolve them to load their backups - return this.editorService.openEditors(openEditorsArgs).then(() => void 0); + const inputs = resources.map(resource => this.resolveInput(resource)); + const openEditorsArgs = inputs.map((input, index) => { + return { input, options: { pinned: true, preserveFocus: true, inactive: index > 0 || hasOpenedEditors }, position: Position.ONE }; }); + + // Open all remaining backups as editors and resolve them to load their backups + return this.editorService.openEditors(openEditorsArgs).then(() => void 0); } - private resolveInput(resource: URI): TPromise { + private resolveInput(resource: URI): IResourceInput | IUntitledResourceInput { if (resource.scheme === 'untitled' && !BackupRestorer.UNTITLED_REGEX.test(resource.fsPath)) { // TODO@Ben debt: instead of guessing if an untitled file has an associated file path or not // this information should be provided by the backup service and stored as meta data within - return TPromise.as({ - resource: this.untitledEditorService.createOrGet(URI.file(resource.fsPath)).getResource() - }); + return { filePath: resource.fsPath }; } - return TPromise.as({ resource }); + return { resource }; } public getId(): string { diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/inspectKeybindings.ts b/src/vs/workbench/parts/codeEditor/electron-browser/inspectKeybindings.ts index 30140bb06bf..716ded4af5a 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/inspectKeybindings.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/inspectKeybindings.ts @@ -10,7 +10,7 @@ import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/e import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchKeybindingService } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +import { IUntitledResourceInput } from "vs/platform/editor/common/editor"; @editorAction class InspectKeyMap extends EditorAction { @@ -27,11 +27,9 @@ class InspectKeyMap extends EditorAction { public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void { const keybindingService = accessor.get(IKeybindingService); const editorService = accessor.get(IWorkbenchEditorService); - const untitledEditorService = accessor.get(IUntitledEditorService); if (keybindingService instanceof WorkbenchKeybindingService) { - const input = untitledEditorService.createOrGet(undefined, null, keybindingService.dumpDebugInfo()); - editorService.openEditor(input, { pinned: true }); + editorService.openEditor({ contents: keybindingService.dumpDebugInfo(), options: { pinned: true } } as IUntitledResourceInput); } } } diff --git a/src/vs/workbench/parts/files/browser/fileActions.ts b/src/vs/workbench/parts/files/browser/fileActions.ts index 6dd3e7a2e56..815d2be4570 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.ts @@ -37,14 +37,13 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer import { IQuickOpenService, IFilePickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { Position, IResourceInput, IEditorInput } from 'vs/platform/editor/common/editor'; +import { Position, IResourceInput, IEditorInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature2, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IMessageService, IMessageWithAction, IConfirmation, Severity, CancelAction } from 'vs/platform/message/common/message'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { getCodeEditor } from 'vs/editor/common/services/codeEditorService'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; import { withFocussedFilesExplorer, revealInOSCommand, revealInExplorerCommand, copyPathCommand } from 'vs/workbench/parts/files/browser/fileCommands'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; @@ -277,7 +276,6 @@ class RenameFileAction extends BaseRenameAction { @IFileService fileService: IFileService, @IMessageService messageService: IMessageService, @ITextFileService textFileService: ITextFileService, - @ITextModelResolverService private textModelResolverService: ITextModelResolverService, @IBackupFileService private backupFileService: IBackupFileService ) { super(RenameFileAction.ID, nls.localize('rename', "Rename"), element, fileService, messageService, textFileService); @@ -323,7 +321,7 @@ class RenameFileAction extends BaseRenameAction { // 4.) resolve those that were dirty to load their previous dirty contents from disk .then(() => { - return TPromise.join(dirtyRenamed.map(t => this.textModelResolverService.createModelReference(t))); + return TPromise.join(dirtyRenamed.map(t => this.textFileService.models.loadOrCreate(t))); }); } } @@ -517,16 +515,13 @@ export class GlobalNewUntitledFileAction extends Action { constructor( id: string, label: string, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IUntitledEditorService private untitledEditorService: IUntitledEditorService + @IWorkbenchEditorService private editorService: IWorkbenchEditorService ) { super(id, label); } public run(): TPromise { - const input = this.untitledEditorService.createOrGet(); - - return this.editorService.openEditor(input, { pinned: true }); // untitled are always pinned + return this.editorService.openEditor({ options: { pinned: true } } as IUntitledResourceInput); // untitled are always pinned } } diff --git a/src/vs/workbench/parts/files/browser/files.contribution.ts b/src/vs/workbench/parts/files/browser/files.contribution.ts index e819da41c0e..a383c9702d0 100644 --- a/src/vs/workbench/parts/files/browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/browser/files.contribution.ts @@ -23,7 +23,7 @@ import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEdi import { TextFileEditor } from 'vs/workbench/parts/files/browser/editors/textFileEditor'; import { BinaryFileEditor } from 'vs/workbench/parts/files/browser/editors/binaryFileEditor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { SyncDescriptor, AsyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -96,12 +96,12 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); -// Register default file input handler -// Note: because of service injection, the descriptor needs to have the exact count -// of arguments as the FileEditorInput constructor. Otherwise when creating an -// instance through the instantiation service he will inject the services wrong! -const descriptor = new AsyncDescriptor('vs/workbench/parts/files/common/editors/fileEditorInput', 'FileEditorInput', /* DO NOT REMOVE */ void 0, /* DO NOT REMOVE */ void 0); -Registry.as(EditorExtensions.Editors).registerDefaultFileInput(descriptor); +// Register default file input factory +Registry.as(EditorExtensions.Editors).registerFileInputFactory({ + createFileInput: (resource, encoding, instantiationService): IFileEditorInput => { + return instantiationService.createInstance(FileEditorInput, resource, encoding); + } +}); interface ISerializedFileInput { resource: string; @@ -145,10 +145,14 @@ class FileEditorInputFactory implements IEditorInputFactory { return JSON.stringify(fileInput); } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - const fileInput: ISerializedFileInput = JSON.parse(serializedEditorInput); + public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput { + return instantiationService.invokeFunction(accessor => { + const fileInput: ISerializedFileInput = JSON.parse(serializedEditorInput); + const resource = !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource); + const encoding = fileInput.encoding; - return instantiationService.createInstance(FileEditorInput, !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource), fileInput.encoding); + return accessor.get(IWorkbenchEditorService).createInput({ resource, encoding }) as FileEditorInput; + }); } } diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index 252cd27084d..3d24249410c 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -49,7 +49,6 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -605,7 +604,6 @@ export class FileDragAndDrop implements IDragAndDrop { @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private instantiationService: IInstantiationService, @ITextFileService private textFileService: ITextFileService, - @ITextModelResolverService private textModelResolverService: ITextModelResolverService, @IBackupFileService private backupFileService: IBackupFileService ) { this.toDispose = []; @@ -761,7 +759,7 @@ export class FileDragAndDrop implements IDragAndDrop { // Success: load all files that are dirty again to restore their dirty contents // Error: discard any backups created during the process - const onSuccess = () => TPromise.join(dirtyMoved.map(t => this.textModelResolverService.createModelReference(t))); + const onSuccess = () => TPromise.join(dirtyMoved.map(t => this.textFileService.models.loadOrCreate(t))); const onError = (error?: Error, showError?: boolean) => { if (showError) { this.messageService.show(Severity.Error, error); diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index a78d222aafc..7c0df2e2875 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -17,10 +17,11 @@ import { BINARY_FILE_EDITOR_ID, TEXT_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID } from import { ITextFileService, AutoSaveMode, ModelState, TextFileModelChangeEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils'; import { Verbosity } from 'vs/platform/editor/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ITextModelResolverService } from "vs/editor/common/services/resolverService"; /** * A file editor input is the input type for the file editor of file system resources. @@ -30,6 +31,8 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private preferredEncoding: string; private forceOpenAsBinary: boolean; + private textModelReference: TPromise>; + private name: string; private description: string; @@ -48,16 +51,15 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { @IInstantiationService private instantiationService: IInstantiationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @ITextFileService private textFileService: ITextFileService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @ITextModelResolverService private textModelResolverService: ITextModelResolverService ) { super(); this.toUnbind = []; - if (resource) { - this.setResource(resource); - this.preferredEncoding = preferredEncoding; - } + this.resource = resource; + this.preferredEncoding = preferredEncoding; this.registerListeners(); } @@ -84,17 +86,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } } - public setResource(resource: URI): void { - this.resource = resource; - - // Reset resource dependent properties - this.name = null; - this.description = null; - this.shortTitle = null; - this.mediumTitle = null; - this.longTitle = null; - } - public getResource(): URI { return this.resource; } @@ -216,7 +207,18 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } // Resolve as text - return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh).then(null, error => { + return this.textFileService.models.loadOrCreate(this.resource, { encoding: this.preferredEncoding, reload: refresh }).then(model => { + + // TODO@Ben this is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary + // or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into + // loadOrCreate ensures we are not creating model references for these kind of resources. + // In addition we have a bit of payload to take into account (encoding, reload) that the text resolver does not handle yet. + if (!this.textModelReference) { + this.textModelReference = this.textModelResolverService.createModelReference(this.resource); + } + + return this.textModelReference.then(ref => ref.object); + }, error => { // In case of an error that indicates that the file is binary or too large, just return with the binary editor model if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { @@ -245,6 +247,12 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { public dispose(): void { + // Model reference + if (this.textModelReference) { + this.textModelReference.done(ref => ref.dispose()); + this.textModelReference = null; + } + // Listeners this.toUnbind = dispose(this.toUnbind); 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 702bf8d10e0..3a2d8f66340 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorInput.test.ts @@ -9,13 +9,15 @@ import URI from 'vs/base/common/uri'; import { join } from 'vs/base/common/paths'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestTextFileService, TestEditorGroupService, createFileInput } from 'vs/workbench/test/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EncodingMode } from 'vs/workbench/common/editor'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationResult, IFileOperationResult } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Verbosity } from 'vs/platform/editor/common/editor'; +import { IEditorGroupService } from "vs/workbench/services/group/common/groupService"; +import { IModelService } from "vs/editor/common/services/modelService"; function toResource(path) { return URI.file(join('C:\\', new Buffer(this.test.fullTitle()).toString('base64'), path)); @@ -24,7 +26,9 @@ function toResource(path) { class ServiceAccessor { constructor( @IWorkbenchEditorService public editorService: IWorkbenchEditorService, - @ITextFileService public textFileService: TestTextFileService + @ITextFileService public textFileService: TestTextFileService, + @IModelService public modelService: IModelService, + @IEditorGroupService public editorGroupService: TestEditorGroupService ) { } } @@ -182,4 +186,29 @@ suite('Files - FileEditorInput', () => { done(); }); }); + + test('disposes model when not open anymore', function (done) { + const resource = toResource.call(this, '/path/index.txt'); + + const input = createFileInput(instantiationService, resource); + + input.resolve().then((model: TextFileEditorModel) => { + const stacks = accessor.editorGroupService.getStacksModel(); + const group = stacks.openGroup('group', true); + group.openEditor(input); + + accessor.editorGroupService.fireChange(); + + assert.ok(!model.isDisposed()); + + group.closeEditor(input); + accessor.editorGroupService.fireChange(); + assert.ok(model.isDisposed()); + + model.dispose(); + assert.ok(!accessor.modelService.getModel(model.getResource())); + + 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 c63b7f74b56..bb064394ed2 100644 --- a/src/vs/workbench/parts/git/browser/gitServices.ts +++ b/src/vs/workbench/parts/git/browser/gitServices.ts @@ -193,10 +193,10 @@ class EditorInputCache { resource = URI.file(paths.join(model.getRepositoryRoot(), indexStatus.getRename())); } - return this.editorService.createInput({ resource }); + return TPromise.as(this.editorService.createInput({ resource })); case Status.BOTH_MODIFIED: - return this.editorService.createInput({ resource }); + return TPromise.as(this.editorService.createInput({ resource })); default: return TPromise.as(null); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index c22fa02b839..eaa21dc8f29 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -38,7 +38,6 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -461,7 +460,6 @@ export class DefaultPreferencesEditor extends BaseTextEditor { @IStorageService storageService: IStorageService, @IConfigurationService configurationService: IConfigurationService, @IWorkbenchThemeService themeService: IWorkbenchThemeService, - @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IPreferencesService private preferencesService: IPreferencesService, @IModelService private modelService: IModelService, @IModeService modeService: IModeService, diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 90af0fdce69..56face1caf8 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -10,18 +10,19 @@ import network = require('vs/base/common/network'); import { Registry } from 'vs/platform/platform'; import { basename, dirname } from 'vs/base/common/paths'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorOptions, IFileEditorInput, TextEditorOptions, IEditorRegistry, Extensions, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, TextEditorOptions, IEditorRegistry, Extensions, SideBySideEditorInput, IFileEditorInput, IFileInputFactory } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IWorkbenchEditorService, IResourceInputType } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorInput, IEditorOptions, ITextEditorOptions, Position, Direction, IEditor, IResourceInput, IResourceDiffInput, IResourceSideBySideInput } from 'vs/platform/editor/common/editor'; +import { IEditorInput, IEditorOptions, ITextEditorOptions, Position, Direction, IEditor, IResourceInput, IResourceDiffInput, IResourceSideBySideInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { AsyncDescriptor0 } from 'vs/platform/instantiation/common/descriptors'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import nls = require('vs/nls'); import { getPathLabel, IWorkspaceProvider } from 'vs/base/common/labels'; +import { ResourceMap } from "vs/base/common/map"; +import { once } from "vs/base/common/event"; import { IEnvironmentService } from "vs/platform/environment/common/environment"; export interface IEditorPart { @@ -37,12 +38,16 @@ export interface IEditorPart { getActiveEditorInput(): IEditorInput; } +type ICachedEditorInput = ResourceEditorInput | IFileEditorInput; + export class WorkbenchEditorService implements IWorkbenchEditorService { public _serviceBrand: any; + private static CACHE: ResourceMap = new ResourceMap(); + private editorPart: IEditorPart | IWorkbenchEditorService; - private fileInputDescriptor: AsyncDescriptor0; + private fileInputFactory: IFileInputFactory; constructor( editorPart: IEditorPart | IWorkbenchEditorService, @@ -52,7 +57,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { @IEnvironmentService private environmentService: IEnvironmentService ) { this.editorPart = editorPart; - this.fileInputDescriptor = Registry.as(Extensions.Editors).getDefaultFileInput(); + this.fileInputFactory = Registry.as(Extensions.Editors).getFileInputFactory(); } public getActiveEditor(): IEditor { @@ -117,13 +122,12 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { // Untyped Text Editor Support (required for code that uses this service below workbench level) const textInput = input; - return this.createInput(textInput).then(typedInput => { - if (typedInput) { - return this.doOpenEditor(typedInput, TextEditorOptions.from(textInput), arg2); - } + const typedInput = this.createInput(textInput); + if (typedInput) { + return this.doOpenEditor(typedInput, TextEditorOptions.from(textInput), arg2); + } - return TPromise.as(null); - }); + return TPromise.as(null); } private toOptions(arg1?: any): EditorOptions { @@ -151,39 +155,36 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { public openEditors(editors: { input: IResourceInputType, position: Position }[]): TPromise; public openEditors(editors: { input: IEditorInput, position: Position, options?: IEditorOptions }[]): TPromise; public openEditors(editors: any[]): TPromise { - return TPromise.join(editors.map(editor => this.createInput(editor.input))).then(inputs => { - const typedInputs: { input: IEditorInput, position: Position, options?: EditorOptions }[] = inputs.map((input, index) => { - const options = editors[index].input instanceof EditorInput ? this.toOptions(editors[index].options) : TextEditorOptions.from(editors[index].input); + const inputs = editors.map(editor => this.createInput(editor.input)); + const typedInputs: { input: IEditorInput, position: Position, options?: EditorOptions }[] = inputs.map((input, index) => { + const options = editors[index].input instanceof EditorInput ? this.toOptions(editors[index].options) : TextEditorOptions.from(editors[index].input); - return { - input, - options, - position: editors[index].position - }; - }); - - return this.editorPart.openEditors(typedInputs); + return { + input, + options, + position: editors[index].position + }; }); + + return this.editorPart.openEditors(typedInputs); } public replaceEditors(editors: { toReplace: IResourceInputType, replaceWith: IResourceInputType }[], position?: Position): TPromise; public replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions }[], position?: Position): TPromise; public replaceEditors(editors: any[], position?: Position): TPromise { - return TPromise.join(editors.map(editor => this.createInput(editor.toReplace))).then(toReplaceInputs => { - return TPromise.join(editors.map(editor => this.createInput(editor.replaceWith))).then(replaceWithInputs => { - const typedReplacements: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: EditorOptions }[] = editors.map((editor, index) => { - const options = editor.toReplace instanceof EditorInput ? this.toOptions(editor.options) : TextEditorOptions.from(editor.replaceWith); + const toReplaceInputs = editors.map(editor => this.createInput(editor.toReplace)); + const replaceWithInputs = editors.map(editor => this.createInput(editor.replaceWith)); + const typedReplacements: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: EditorOptions }[] = editors.map((editor, index) => { + const options = editor.toReplace instanceof EditorInput ? this.toOptions(editor.options) : TextEditorOptions.from(editor.replaceWith); - return { - toReplace: toReplaceInputs[index], - replaceWith: replaceWithInputs[index], - options - }; - }); - - return this.editorPart.replaceEditors(typedReplacements, position); - }); + return { + toReplace: toReplaceInputs[index], + replaceWith: replaceWithInputs[index], + options + }; }); + + return this.editorPart.replaceEditors(typedReplacements, position); } public closeEditor(position: Position, input: IEditorInput): TPromise { @@ -202,51 +203,48 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { return this.editorPart.closeAllEditors(except); } - public createInput(input: IEditorInput): TPromise; - public createInput(input: IResourceInputType): TPromise; - public createInput(input: any): TPromise { + public createInput(input: IEditorInput): EditorInput; + public createInput(input: IResourceInputType): EditorInput; + public createInput(input: any): IEditorInput { // Workbench Input Support if (input instanceof EditorInput) { - return TPromise.as(input); + return input; } // Side by Side Support const resourceSideBySideInput = input; if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) { - return this.createInput({ resource: resourceSideBySideInput.masterResource }).then(masterInput => { - return this.createInput({ resource: resourceSideBySideInput.detailResource }).then(detailInput => { - return new SideBySideEditorInput(resourceSideBySideInput.label || masterInput.getName(), typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(), detailInput, masterInput); - }); - }); + const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource }); + const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource }); + + return new SideBySideEditorInput(resourceSideBySideInput.label || masterInput.getName(), typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(), detailInput, masterInput); } // Diff Editor Support const resourceDiffInput = input; if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) { - return this.createInput({ resource: resourceDiffInput.leftResource }).then(leftInput => { - return this.createInput({ resource: resourceDiffInput.rightResource }).then(rightInput => { - const label = resourceDiffInput.label || this.toDiffLabel(resourceDiffInput.leftResource, resourceDiffInput.rightResource, this.workspaceContextService, this.environmentService); + const leftInput = this.createInput({ resource: resourceDiffInput.leftResource }); + const rightInput = this.createInput({ resource: resourceDiffInput.rightResource }); + const label = resourceDiffInput.label || this.toDiffLabel(resourceDiffInput.leftResource, resourceDiffInput.rightResource, this.workspaceContextService, this.environmentService); - return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput); - }); - }); + return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput); } - // Base Text Editor Support for inmemory resources - const resourceInput = input; - // Untitled file support - if (resourceInput.resource instanceof URI && (resourceInput.resource.scheme === UntitledEditorInput.SCHEMA)) { - return TPromise.as(this.untitledEditorService.createOrGet(resourceInput.resource)); + const untitledInput = input; + if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === UntitledEditorInput.SCHEMA)) { + return this.untitledEditorService.createOrGet(untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource, untitledInput.language, untitledInput.contents); } - // Base Text Editor Support for file resources - else if (this.fileInputDescriptor && resourceInput.resource instanceof URI && resourceInput.resource.scheme === network.Schemas.file) { - return this.createFileInput(resourceInput.resource, resourceInput.encoding); + const resourceInput = input; + + // Files support + if (resourceInput.resource instanceof URI && resourceInput.resource.scheme === network.Schemas.file) { + return this.createOrGet(resourceInput.resource, this.instantiationService, resourceInput.label, resourceInput.description, resourceInput.encoding); } - // Treat an URI as ResourceEditorInput + // Any other resource else if (resourceInput.resource instanceof URI) { const label = resourceInput.label || basename(resourceInput.resource.fsPath); let description: string; @@ -256,19 +254,38 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { description = dirname(resourceInput.resource.fsPath); } - return TPromise.as(this.instantiationService.createInstance(ResourceEditorInput, label, description, resourceInput.resource)); + return this.createOrGet(resourceInput.resource, this.instantiationService, label, description); } - return TPromise.as(null); + return null; } - private createFileInput(resource: URI, encoding?: string): TPromise { - return this.instantiationService.createInstance(this.fileInputDescriptor).then(typedFileInput => { - typedFileInput.setResource(resource); - typedFileInput.setPreferredEncoding(encoding); + private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string): ICachedEditorInput { + if (WorkbenchEditorService.CACHE.has(resource)) { + const input = WorkbenchEditorService.CACHE.get(resource); + if (input instanceof ResourceEditorInput) { + input.setName(label); + input.setDescription(description); + } else { + input.setPreferredEncoding(encoding); + } - return typedFileInput; + return input; + } + + let input: ICachedEditorInput; + if (resource.scheme === network.Schemas.file) { + input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService); + } else { + input = instantiationService.createInstance(ResourceEditorInput, label, description, resource); + } + + WorkbenchEditorService.CACHE.set(resource, input); + once(input.onDispose)(() => { + WorkbenchEditorService.CACHE.delete(resource); }); + + return input; } private toDiffLabel(res1: URI, res2: URI, context: IWorkspaceProvider, environment: IEnvironmentService): string { diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index b2359a210f6..abedce481a0 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -7,11 +7,11 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorService, IEditor, IEditorInput, IEditorOptions, ITextEditorOptions, Position, Direction, IResourceInput, IResourceDiffInput, IResourceSideBySideInput } from 'vs/platform/editor/common/editor'; +import { IEditorService, IEditor, IEditorInput, IEditorOptions, ITextEditorOptions, Position, Direction, IResourceInput, IResourceDiffInput, IResourceSideBySideInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; export const IWorkbenchEditorService = createDecorator('editorService'); -export type IResourceInputType = IResourceInput | IResourceDiffInput | IResourceSideBySideInput; +export type IResourceInputType = IResourceInput | IUntitledResourceInput | IResourceDiffInput | IResourceSideBySideInput; /** * The editor service allows to open editors and work on the active @@ -90,5 +90,5 @@ export interface IWorkbenchEditorService extends IEditorService { /** * Allows to resolve an untyped input to a workbench typed instanceof editor input */ - createInput(input: IResourceInputType): TPromise; + createInput(input: IResourceInputType): IEditorInput; } \ No newline at end of file diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts new file mode 100644 index 00000000000..f9f53191d70 --- /dev/null +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -0,0 +1,296 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { Promise, TPromise } from 'vs/base/common/winjs.base'; +import paths = require('vs/base/common/paths'); +import { Position, Direction, IEditor } from 'vs/platform/editor/common/editor'; +import URI from 'vs/base/common/uri'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor'; +import { StringEditorInput } from 'vs/workbench/common/editor/stringEditorInput'; +import { StringEditorModel } from 'vs/workbench/common/editor/stringEditorModel'; +import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; +import { workbenchInstantiationService, TestThemeService } from 'vs/workbench/test/workbenchTestServices'; +import { DelegatingWorkbenchEditorService, WorkbenchEditorService, IEditorPart } from 'vs/workbench/services/editor/browser/editorService'; +import { UntitledEditorInput } from "vs/workbench/common/editor/untitledEditorInput"; + +let activeEditor: BaseEditor = { + getSelection: function () { + return 'test.selection'; + } +}; + +let openedEditorInput; +let openedEditorOptions; +let openedEditorPosition; + +function toResource(path) { + return URI.from({ scheme: 'custom', path }); +} + +function toFileResource(path) { + return URI.file(paths.join('C:\\', new Buffer(this.test.fullTitle()).toString('base64'), path)); +} + +class TestEditorPart implements IEditorPart { + private activeInput; + + public getId(): string { + return null; + } + + public openEditors(args: any[]): Promise { + return TPromise.as([]); + } + + public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: any }[]): TPromise { + return TPromise.as([]); + } + + public closeEditors(position: Position, except?: EditorInput, direction?: Direction): TPromise { + return TPromise.as(null); + } + + public closeAllEditors(except?: Position): TPromise { + return TPromise.as(null); + } + + public closeEditor(position: Position, input: EditorInput): TPromise { + return TPromise.as(null); + } + + public openEditor(input?: EditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise; + public openEditor(input?: EditorInput, options?: EditorOptions, position?: Position): TPromise; + public openEditor(input?: EditorInput, options?: EditorOptions, arg?: any): TPromise { + openedEditorInput = input; + openedEditorOptions = options; + openedEditorPosition = arg; + + return TPromise.as(activeEditor); + } + + public getActiveEditor(): BaseEditor { + return activeEditor; + } + + public setActiveEditorInput(input: EditorInput) { + this.activeInput = input; + } + + public getActiveEditorInput(): EditorInput { + return this.activeInput; + } + + public getVisibleEditors(): IEditor[] { + return [activeEditor]; + } +} + +suite('WorkbenchEditorService', () => { + + test('basics', function () { + let instantiationService = workbenchInstantiationService(); + + let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource.call(this, '/something.js'), void 0); + + let testEditorPart = new TestEditorPart(); + testEditorPart.setActiveEditorInput(activeInput); + let service: WorkbenchEditorService = instantiationService.createInstance(WorkbenchEditorService, testEditorPart); + + assert.strictEqual(service.getActiveEditor(), activeEditor); + assert.strictEqual(service.getActiveEditorInput(), activeInput); + + // Open EditorInput + service.openEditor(activeInput, null).then((editor) => { + assert.strictEqual(openedEditorInput, activeInput); + assert.strictEqual(openedEditorOptions, null); + assert.strictEqual(editor, activeEditor); + assert.strictEqual(service.getVisibleEditors().length, 1); + assert(service.getVisibleEditors()[0] === editor); + }); + + service.openEditor(activeInput, null, Position.ONE).then((editor) => { + assert.strictEqual(openedEditorInput, activeInput); + assert.strictEqual(openedEditorOptions, null); + assert.strictEqual(editor, activeEditor); + assert.strictEqual(service.getVisibleEditors().length, 1); + assert(service.getVisibleEditors()[0] === editor); + }); + + // Open Untyped Input (file) + service.openEditor({ resource: toFileResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => { + assert.strictEqual(editor, activeEditor); + + assert(openedEditorInput instanceof FileEditorInput); + let contentInput = openedEditorInput; + assert.strictEqual(contentInput.getResource().fsPath, toFileResource.call(this, '/index.html').fsPath); + + assert(openedEditorOptions instanceof TextEditorOptions); + let textEditorOptions = openedEditorOptions; + assert(textEditorOptions.hasOptionsDefined()); + }); + + // Open Untyped Input (file, encoding) + service.openEditor({ resource: toFileResource.call(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => { + assert.strictEqual(editor, activeEditor); + + assert(openedEditorInput instanceof FileEditorInput); + let contentInput = openedEditorInput; + assert.equal(contentInput.getPreferredEncoding(), 'utf16le'); + }); + + // Open Untyped Input (untitled) + service.openEditor({ options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => { + assert.strictEqual(editor, activeEditor); + + assert(openedEditorInput instanceof UntitledEditorInput); + + assert(openedEditorOptions instanceof TextEditorOptions); + let textEditorOptions = openedEditorOptions; + assert(textEditorOptions.hasOptionsDefined()); + }); + + // Open Untyped Input (untitled with contents) + service.openEditor({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => { + assert.strictEqual(editor, activeEditor); + + assert(openedEditorInput instanceof UntitledEditorInput); + + const untitledInput = openedEditorInput as UntitledEditorInput; + untitledInput.resolve().then(model => { + assert.equal(model.getValue(), 'Hello Untitled'); + }); + }); + + // Open Untyped Input (untitled with file path) + service.openEditor({ filePath: '/some/path.txt', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => { + assert.strictEqual(editor, activeEditor); + + assert(openedEditorInput instanceof UntitledEditorInput); + + const untitledInput = openedEditorInput as UntitledEditorInput; + assert.ok(untitledInput.hasAssociatedFilePath); + }); + + // Resolve Editor Model (Typed EditorInput) + let input = instantiationService.createInstance(StringEditorInput, 'name', 'description', 'hello world', 'text/plain', false); + input.resolve(true).then((model: StringEditorModel) => { + assert(model instanceof StringEditorModel); + + assert(model.isResolved()); + + input.resolve().then((otherModel) => { + assert(model === otherModel); + + input.dispose(); + }); + }); + }); + + test('caching', function () { + let instantiationService = workbenchInstantiationService(); + + let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource.call(this, '/something.js'), void 0); + + let testEditorPart = new TestEditorPart(); + testEditorPart.setActiveEditorInput(activeInput); + let service: WorkbenchEditorService = instantiationService.createInstance(WorkbenchEditorService, testEditorPart); + + // Cached Input (Files) + const fileResource1 = toFileResource.call(this, '/foo/bar/cache1.js'); + const fileInput1 = service.createInput({ resource: fileResource1 }); + assert.ok(fileInput1); + + const fileResource2 = toFileResource.call(this, '/foo/bar/cache2.js'); + const fileInput2 = service.createInput({ resource: fileResource2 }); + assert.ok(fileInput2); + + assert.notEqual(fileInput1, fileInput2); + + const fileInput1Again = service.createInput({ resource: fileResource1 }); + assert.equal(fileInput1Again, fileInput1); + + fileInput1Again.dispose(); + + assert.ok(fileInput1.isDisposed()); + + const fileInput1AgainAndAgain = service.createInput({ resource: fileResource1 }); + assert.notEqual(fileInput1AgainAndAgain, fileInput1); + assert.ok(!fileInput1AgainAndAgain.isDisposed()); + + // Cached Input (Resource) + const resource1 = toResource.call(this, '/foo/bar/cache1.js'); + const input1 = service.createInput({ resource: resource1 }); + assert.ok(input1); + + const resource2 = toResource.call(this, '/foo/bar/cache2.js'); + const input2 = service.createInput({ resource: resource2 }); + assert.ok(input2); + + assert.notEqual(input1, input2); + + const input1Again = service.createInput({ resource: resource1 }); + assert.equal(input1Again, input1); + + input1Again.dispose(); + + assert.ok(input1.isDisposed()); + + const input1AgainAndAgain = service.createInput({ resource: resource1 }); + assert.notEqual(input1AgainAndAgain, input1); + assert.ok(!input1AgainAndAgain.isDisposed()); + }); + + test('delegate', function (done) { + let instantiationService = workbenchInstantiationService(); + let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource.call(this, '/something.js'), void 0); + + let testEditorPart = new TestEditorPart(); + testEditorPart.setActiveEditorInput(activeInput); + + instantiationService.createInstance(WorkbenchEditorService, testEditorPart); + class MyEditor extends BaseEditor { + + constructor(id: string) { + super(id, null, new TestThemeService()); + } + + getId(): string { + return 'myEditor'; + } + + public layout(): void { + + } + + public createEditor(): any { + + } + } + let ed = instantiationService.createInstance(MyEditor, 'my.editor'); + + let inp = instantiationService.createInstance(StringEditorInput, 'name', 'description', 'hello world', 'text/plain', false); + let delegate = instantiationService.createInstance(DelegatingWorkbenchEditorService); + delegate.setEditorOpenHandler((input, options?) => { + assert.strictEqual(input, inp); + + return TPromise.as(ed); + }); + + delegate.setEditorCloseHandler((position, input) => { + assert.strictEqual(input, inp); + + done(); + + return TPromise.as(void 0); + }); + + delegate.openEditor(inp); + delegate.closeEditor(0, inp); + }); +}); diff --git a/src/vs/workbench/test/browser/services.test.ts b/src/vs/workbench/services/progress/test/progressService.test.ts similarity index 51% rename from src/vs/workbench/test/browser/services.test.ts rename to src/vs/workbench/services/progress/test/progressService.test.ts index 26da8aef32f..d604681b2a7 100644 --- a/src/vs/workbench/test/browser/services.test.ts +++ b/src/vs/workbench/services/progress/test/progressService.test.ts @@ -8,92 +8,16 @@ import * as assert from 'assert'; import { IAction, IActionItem } from 'vs/base/common/actions'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; -import paths = require('vs/base/common/paths'); -import { IEditorControl, Position, Direction, IEditor } from 'vs/platform/editor/common/editor'; -import URI from 'vs/base/common/uri'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor'; -import { StringEditorInput } from 'vs/workbench/common/editor/stringEditorInput'; -import { StringEditorModel } from 'vs/workbench/common/editor/stringEditorModel'; -import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; -import { workbenchInstantiationService, TestThemeService } from 'vs/workbench/test/workbenchTestServices'; +import { IEditorControl } from 'vs/platform/editor/common/editor'; import { Viewlet, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { IPanel } from 'vs/workbench/common/panel'; import { WorkbenchProgressService, ScopedService } from 'vs/workbench/services/progress/browser/progressService'; -import { DelegatingWorkbenchEditorService, WorkbenchEditorService, IEditorPart } from 'vs/workbench/services/editor/browser/editorService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { Emitter } from 'vs/base/common/event'; let activeViewlet: Viewlet = {}; -let activeEditor: BaseEditor = { - getSelection: function () { - return 'test.selection'; - } -}; - -let openedEditorInput; -let openedEditorOptions; -let openedEditorPosition; - -function toResource(path) { - return URI.file(paths.join('C:\\', new Buffer(this.test.fullTitle()).toString('base64'), path)); -} - -class TestEditorPart implements IEditorPart { - private activeInput; - - public getId(): string { - return null; - } - - public openEditors(args: any[]): Promise { - return TPromise.as([]); - } - - public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: any }[]): TPromise { - return TPromise.as([]); - } - - public closeEditors(position: Position, except?: EditorInput, direction?: Direction): TPromise { - return TPromise.as(null); - } - - public closeAllEditors(except?: Position): TPromise { - return TPromise.as(null); - } - - public closeEditor(position: Position, input: EditorInput): TPromise { - return TPromise.as(null); - } - - public openEditor(input?: EditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise; - public openEditor(input?: EditorInput, options?: EditorOptions, position?: Position): TPromise; - public openEditor(input?: EditorInput, options?: EditorOptions, arg?: any): TPromise { - openedEditorInput = input; - openedEditorOptions = options; - openedEditorPosition = arg; - - return TPromise.as(activeEditor); - } - - public getActiveEditor(): BaseEditor { - return activeEditor; - } - - public setActiveEditorInput(input: EditorInput) { - this.activeInput = input; - } - - public getActiveEditorInput(): EditorInput { - return this.activeInput; - } - - public getVisibleEditors(): IEditor[] { - return [activeEditor]; - } -} class TestViewletService implements IViewletService { public _serviceBrand: any; @@ -284,112 +208,7 @@ class TestProgressBar { } } -suite('Workbench UI Services', () => { - - test('WorkbenchEditorService', function () { - let instantiationService = workbenchInstantiationService(); - - let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/something.js'), void 0); - - let testEditorPart = new TestEditorPart(); - testEditorPart.setActiveEditorInput(activeInput); - let service: WorkbenchEditorService = instantiationService.createInstance(WorkbenchEditorService, testEditorPart); - - assert.strictEqual(service.getActiveEditor(), activeEditor); - assert.strictEqual(service.getActiveEditorInput(), activeInput); - - // Open EditorInput - service.openEditor(activeInput, null).then((editor) => { - assert.strictEqual(openedEditorInput, activeInput); - assert.strictEqual(openedEditorOptions, null); - assert.strictEqual(editor, activeEditor); - assert.strictEqual(service.getVisibleEditors().length, 1); - assert(service.getVisibleEditors()[0] === editor); - }); - - service.openEditor(activeInput, null, Position.ONE).then((editor) => { - assert.strictEqual(openedEditorInput, activeInput); - assert.strictEqual(openedEditorOptions, null); - assert.strictEqual(editor, activeEditor); - assert.strictEqual(service.getVisibleEditors().length, 1); - assert(service.getVisibleEditors()[0] === editor); - }); - - // Open Untyped Input - service.openEditor({ resource: toResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => { - assert.strictEqual(editor, activeEditor); - - assert(openedEditorInput instanceof FileEditorInput); - let contentInput = openedEditorInput; - assert.strictEqual(contentInput.getResource().fsPath, toResource.call(this, '/index.html').fsPath); - - assert(openedEditorOptions instanceof TextEditorOptions); - let textEditorOptions = openedEditorOptions; - assert(textEditorOptions.hasOptionsDefined()); - }); - - // Resolve Editor Model (Typed EditorInput) - let input = instantiationService.createInstance(StringEditorInput, 'name', 'description', 'hello world', 'text/plain', false); - input.resolve(true).then((model: StringEditorModel) => { - assert(model instanceof StringEditorModel); - - assert(model.isResolved()); - - input.resolve().then((otherModel) => { - assert(model === otherModel); - - input.dispose(); - }); - }); - }); - - test('DelegatingWorkbenchEditorService', function (done) { - let instantiationService = workbenchInstantiationService(); - let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/something.js'), void 0); - - let testEditorPart = new TestEditorPart(); - testEditorPart.setActiveEditorInput(activeInput); - - instantiationService.createInstance(WorkbenchEditorService, testEditorPart); - class MyEditor extends BaseEditor { - - constructor(id: string) { - super(id, null, new TestThemeService()); - } - - getId(): string { - return 'myEditor'; - } - - public layout(): void { - - } - - public createEditor(): any { - - } - } - let ed = instantiationService.createInstance(MyEditor, 'my.editor'); - - let inp = instantiationService.createInstance(StringEditorInput, 'name', 'description', 'hello world', 'text/plain', false); - let delegate = instantiationService.createInstance(DelegatingWorkbenchEditorService); - delegate.setEditorOpenHandler((input, options?) => { - assert.strictEqual(input, inp); - - return TPromise.as(ed); - }); - - delegate.setEditorCloseHandler((position, input) => { - assert.strictEqual(input, inp); - - done(); - - return TPromise.as(void 0); - }); - - delegate.openEditor(inp); - delegate.closeEditor(0, inp); - }); +suite('Progress Service', () => { test('ScopedService', () => { let viewletService = new TestViewletService(); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index f2e1891aa4e..ad5f6432811 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -9,8 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; @@ -40,8 +39,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { constructor( @ILifecycleService private lifecycleService: ILifecycleService, - @IInstantiationService private instantiationService: IInstantiationService, - @IEditorGroupService private editorGroupService: IEditorGroupService + @IInstantiationService private instantiationService: IInstantiationService ) { this.toUnbind = []; @@ -74,62 +72,10 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { private registerListeners(): void { - // Editors changing/closing - this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); - this.toUnbind.push(this.editorGroupService.getStacksModel().onEditorClosed(() => this.onEditorClosed())); - // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); } - private onEditorsChanged(): void { - this.disposeUnusedModels(); - } - - private onEditorClosed(): void { - this.disposeUnusedModels(); - } - - 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 canDispose(model: ITextFileEditorModel): boolean { - if (!model) { - return false; // we need data! - } - - if (model.isDisposed()) { - return false; // already disposed - } - - if (this.mapResourceToPendingModelLoaders.has(model.getResource())) { - return false; // not yet loaded - } - - if (model.isDirty()) { - return false; // not saved - } - - 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; - } - public get onModelDisposed(): Event { return this._onModelDisposed.event; } @@ -213,7 +159,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return this.mapResourceToModel.get(resource); } - public loadOrCreate(resource: URI, encoding?: string, refresh?: boolean): TPromise { + public loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise { // Return early if model is currently being loaded const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource); @@ -226,7 +172,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { // Model exists let model = this.get(resource); if (model) { - if (!refresh) { + if (!options || !options.reload) { modelPromise = TPromise.as(model); } else { modelPromise = model.load(); @@ -235,7 +181,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { // Model does not exist else { - model = this.instantiationService.createInstance(TextFileEditorModel, resource, encoding); + model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : void 0); modelPromise = model.load(); // Install state change listener @@ -376,6 +322,26 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { this.mapResourceToModelContentChangeListener.clear(); } + public disposeModel(model: TextFileEditorModel): void { + if (!model) { + return; // we need data! + } + + if (model.isDisposed()) { + return; // already disposed + } + + if (this.mapResourceToPendingModelLoaders.has(model.getResource())) { + return; // not yet loaded + } + + if (model.isDirty()) { + return; // not saved + } + + model.dispose(); + } + public dispose(): void { this.toUnbind = dispose(this.toUnbind); } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index a7bfbd8dad8..77023fd5aa4 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -141,6 +141,11 @@ export interface IRawTextContent extends IBaseStat { encoding: string; } +export interface IModelLoadOrCreateOptions { + encoding?: string; + reload?: boolean; +} + export interface ITextFileEditorModelManager { onModelDisposed: Event; @@ -162,7 +167,9 @@ export interface ITextFileEditorModelManager { getAll(resource?: URI): ITextFileEditorModel[]; - loadOrCreate(resource: URI, preferredEncoding?: string, refresh?: boolean): TPromise; + loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise; + + disposeModel(model: ITextFileEditorModel): void; } export interface IModelSaveOptions { diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts index 68dbda18efc..48c32c6a912 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts @@ -11,7 +11,7 @@ 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 } from 'vs/base/common/paths'; -import { workbenchInstantiationService, TestEditorGroupService, createFileInput, TestFileService } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestEditorGroupService, 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'; @@ -108,17 +108,17 @@ suite('Files - TextFileEditorModelManager', () => { const resource = URI.file('/test.html'); const encoding = 'utf8'; - manager.loadOrCreate(resource, encoding, true).done(model => { + manager.loadOrCreate(resource, { encoding, reload: true }).done(model => { assert.ok(model); assert.equal(model.getEncoding(), encoding); assert.equal(manager.get(resource), model); - return manager.loadOrCreate(resource, encoding).then(model2 => { + return manager.loadOrCreate(resource, { encoding }).then(model2 => { assert.equal(model2, model); model.dispose(); - return manager.loadOrCreate(resource, encoding).then(model3 => { + return manager.loadOrCreate(resource, { encoding }).then(model3 => { assert.notEqual(model3, model2); assert.equal(manager.get(resource), model3); @@ -150,34 +150,6 @@ suite('Files - TextFileEditorModelManager', () => { model3.dispose(); }); - test('disposes model when not open anymore', 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); - - const input = createFileInput(instantiationService, resource); - - const stacks = accessor.editorGroupService.getStacksModel(); - const group = stacks.openGroup('group', true); - group.openEditor(input); - - accessor.editorGroupService.fireChange(); - - assert.ok(!model.isDisposed()); - - group.closeEditor(input); - accessor.editorGroupService.fireChange(); - assert.ok(model.isDisposed()); - - model.dispose(); - assert.ok(!accessor.modelService.getModel(model.getResource())); - - manager.dispose(); - }); - test('events', function (done) { TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0; TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 0; @@ -235,11 +207,11 @@ suite('Files - TextFileEditorModelManager', () => { disposeCounter++; }); - manager.loadOrCreate(resource1, 'utf8').done(model1 => { + manager.loadOrCreate(resource1, { encoding: 'utf8' }).done(model1 => { accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }])); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }])); - return manager.loadOrCreate(resource2, 'utf8').then(model2 => { + return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => { model1.textEditorModel.setValue('changed'); model1.updatePreferredEncoding('utf16'); @@ -304,8 +276,8 @@ suite('Files - TextFileEditorModelManager', () => { assert.equal(e[0].resource.toString(), resource1.toString()); }); - manager.loadOrCreate(resource1, 'utf8').done(model1 => { - return manager.loadOrCreate(resource2, 'utf8').then(model2 => { + manager.loadOrCreate(resource1, { encoding: 'utf8' }).done(model1 => { + return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => { model1.textEditorModel.setValue('changed'); model1.updatePreferredEncoding('utf16'); @@ -342,7 +314,7 @@ suite('Files - TextFileEditorModelManager', () => { const resource = toResource('/path/index_something.txt'); - manager.loadOrCreate(resource, 'utf8').done(model => { + manager.loadOrCreate(resource, { encoding: 'utf8' }).done(model => { model.dispose(); assert.ok(!manager.get(resource)); @@ -352,4 +324,25 @@ suite('Files - TextFileEditorModelManager', () => { done(); }, error => onError(error, done)); }); + + test('dispose prevents dirty model from getting disposed', function (done) { + const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + + const resource = toResource('/path/index_something.txt'); + + manager.loadOrCreate(resource, { encoding: 'utf8' }).done((model: TextFileEditorModel) => { + model.textEditorModel.setValue('make dirty'); + + manager.disposeModel(model); + assert.ok(!model.isDisposed()); + + model.revert(true); + + manager.disposeModel(model); + assert.ok(model.isDisposed()); + + manager.dispose(); + done(); + }, error => onError(error, done)); + }); }); \ No newline at end of file diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 873af5c4acf..a4dc91343d5 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -17,29 +17,40 @@ import network = require('vs/base/common/network'); import { ITextModelResolverService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; +import { TextFileEditorModel } from "vs/workbench/services/textfile/common/textFileEditorModel"; class ResourceModelCollection extends ReferenceCollection> { private providers: { [scheme: string]: ITextModelContentProvider[] } = Object.create(null); constructor( - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @ITextFileService private textFileService: ITextFileService ) { super(); } - createReferencedObject(key: string): TPromise { + public createReferencedObject(key: string): TPromise { const resource = URI.parse(key); - return this.resolveTextModelContent(key) - .then(() => this.instantiationService.createInstance(ResourceEditorModel, resource)); + if (resource.scheme === network.Schemas.file) { + return this.textFileService.models.loadOrCreate(resource); + } + + return this.resolveTextModelContent(key).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource)); } - destroyReferencedObject(modelPromise: TPromise): void { - modelPromise.done(model => model.dispose()); + public destroyReferencedObject(modelPromise: TPromise): void { + modelPromise.done(model => { + if (model instanceof TextFileEditorModel) { + this.textFileService.models.disposeModel(model); + } else { + model.dispose(); + } + }); } - registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { + public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { const registry = this.providers; const providers = registry[scheme] || (registry[scheme] = []); @@ -98,7 +109,7 @@ export class TextModelResolverService implements ITextModelResolverService { this.resourceModelCollection = instantiationService.createInstance(ResourceModelCollection); } - createModelReference(resource: URI): TPromise> { + public createModelReference(resource: URI): TPromise> { const uri = resource.toString(); let promise = this.promiseCache[uri]; @@ -112,12 +123,6 @@ export class TextModelResolverService implements ITextModelResolverService { } private _createModelReference(resource: URI): TPromise> { - // File Schema: use text file service - // TODO ImmortalReference is a hack - if (resource.scheme === network.Schemas.file) { - return this.textFileService.models.loadOrCreate(resource) - .then(model => new ImmortalReference(model)); - } // Untitled Schema: go through cached input // TODO ImmortalReference is a hack @@ -144,12 +149,13 @@ export class TextModelResolverService implements ITextModelResolverService { model => ({ object: model, dispose: () => ref.dispose() }), err => { ref.dispose(); + return TPromise.wrapError(err); } ); } - registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { + public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { return this.resourceModelCollection.registerTextModelContentProvider(scheme, provider); } } \ No newline at end of file diff --git a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts index 347ccc2d9e0..44a56bf3580 100644 --- a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts @@ -21,6 +21,7 @@ import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textF import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { once } from "vs/base/common/event"; class ServiceAccessor { constructor( @@ -74,6 +75,14 @@ suite('Workbench - TextModelResolverService', () => { assert.ok(model); assert.equal(model.getValue(), 'Hello Test'); + let disposed = false; + once(model.onDispose)(() => { + disposed = true; + }); + + input.dispose(); + assert.equal(disposed, true); + dispose.dispose(); done(); }); @@ -90,7 +99,14 @@ suite('Workbench - TextModelResolverService', () => { assert.ok(editorModel); assert.equal(editorModel.getValue(), 'Hello Html'); + + let disposed = false; + once(model.onDispose)(() => { + disposed = true; + }); + ref.dispose(); + assert.equal(disposed, true); }); }); }); diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index 6bba56cb275..ca9b0a35175 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -11,6 +11,7 @@ import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorIn import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Event, { Emitter, once } from 'vs/base/common/event'; +import { ResourceMap } from 'vs/base/common/map'; export const IUntitledEditorService = createDecorator('untitledEditorService'); @@ -82,8 +83,8 @@ export class UntitledEditorService implements IUntitledEditorService { public _serviceBrand: any; - private static CACHE: { [resource: string]: UntitledEditorInput } = Object.create(null); - private static KNOWN_ASSOCIATED_FILE_PATHS: { [resource: string]: boolean } = Object.create(null); + private static CACHE: ResourceMap = new ResourceMap(); + private static KNOWN_ASSOCIATED_FILE_PATHS: ResourceMap = new ResourceMap(); private _onDidChangeContent: Emitter; private _onDidChangeDirty: Emitter; @@ -117,15 +118,15 @@ export class UntitledEditorService implements IUntitledEditorService { } public get(resource: URI): UntitledEditorInput { - return UntitledEditorService.CACHE[resource.toString()]; + return UntitledEditorService.CACHE.get(resource); } public getAll(resources?: URI[]): UntitledEditorInput[] { if (resources) { - return arrays.coalesce(resources.map((r) => this.get(r))); + return arrays.coalesce(resources.map(r => this.get(r))); } - return Object.keys(UntitledEditorService.CACHE).map((key) => UntitledEditorService.CACHE[key]); + return UntitledEditorService.CACHE.values(); } public revertAll(resources?: URI[], force?: boolean): URI[] { @@ -151,10 +152,9 @@ export class UntitledEditorService implements IUntitledEditorService { } public getDirty(): URI[] { - return Object.keys(UntitledEditorService.CACHE) - .map((key) => UntitledEditorService.CACHE[key]) - .filter((i) => i.isDirty()) - .map((i) => i.getResource()); + return UntitledEditorService.CACHE.values() + .filter(i => i.isDirty()) + .map(i => i.getResource()); } public createOrGet(resource?: URI, modeId?: string, initialValue?: string): UntitledEditorInput { @@ -164,13 +164,13 @@ export class UntitledEditorService implements IUntitledEditorService { resource = this.resourceToUntitled(resource); // ensure we have the right scheme if (hasAssociatedFilePath) { - UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[resource.toString()] = true; // remember for future lookups + UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS.set(resource, true); // remember for future lookups } } // Return existing instance if asked for it - if (resource && UntitledEditorService.CACHE[resource.toString()]) { - return UntitledEditorService.CACHE[resource.toString()]; + if (resource && UntitledEditorService.CACHE.has(resource)) { + return UntitledEditorService.CACHE.get(resource); } // Create new otherwise @@ -181,11 +181,11 @@ export class UntitledEditorService implements IUntitledEditorService { if (!resource) { // Create new taking a resource URI that is not already taken - let counter = Object.keys(UntitledEditorService.CACHE).length + 1; + let counter = UntitledEditorService.CACHE.size + 1; do { resource = URI.from({ scheme: UntitledEditorInput.SCHEMA, path: `Untitled-${counter}` }); counter++; - } while (Object.keys(UntitledEditorService.CACHE).indexOf(resource.toString()) >= 0); + } while (UntitledEditorService.CACHE.has(resource)); } // Look up default language from settings if any @@ -217,8 +217,8 @@ export class UntitledEditorService implements IUntitledEditorService { // Remove from cache on dispose const onceDispose = once(input.onDispose); onceDispose(() => { - delete UntitledEditorService.CACHE[input.getResource().toString()]; - delete UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[input.getResource().toString()]; + UntitledEditorService.CACHE.delete(input.getResource()); + UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS.delete(input.getResource()); contentListener.dispose(); dirtyListener.dispose(); encodingListener.dispose(); @@ -226,7 +226,7 @@ export class UntitledEditorService implements IUntitledEditorService { }); // Add to cache - UntitledEditorService.CACHE[resource.toString()] = input; + UntitledEditorService.CACHE.set(resource, input); return input; } @@ -240,7 +240,7 @@ export class UntitledEditorService implements IUntitledEditorService { } public hasAssociatedFilePath(resource: URI): boolean { - return !!UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[resource.toString()]; + return UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS.has(resource); } public dispose(): void { diff --git a/src/vs/workbench/test/browser/editorStacksModel.test.ts b/src/vs/workbench/test/browser/editorStacksModel.test.ts index 9bf4a3552d0..9cc08d0d963 100644 --- a/src/vs/workbench/test/browser/editorStacksModel.test.ts +++ b/src/vs/workbench/test/browser/editorStacksModel.test.ts @@ -145,8 +145,6 @@ class TestFileEditorInput extends EditorInput implements IFileEditorInput { return other && this.id === other.id && other instanceof TestFileEditorInput; } - public setResource(r: URI): void { - } public setEncoding(encoding: string) { } @@ -1568,11 +1566,10 @@ suite('Editor Stacks Model', () => { assert.ok(model.isOpen(input1Resource)); assert.ok(group1.contains(input1Resource)); - assert.equal(model.count(input1Resource), 1); + assert.equal(model.count(input1), 1); assert.equal(group1.getEditor(input1Resource), input1); assert.ok(!group1.getEditor(input1ResourceUpper)); - assert.equal(model.count(input1ResourceUpper), 0); assert.ok(!model.isOpen(input1ResourceUpper)); assert.ok(!group1.contains(input1ResourceUpper)); @@ -1585,7 +1582,7 @@ suite('Editor Stacks Model', () => { assert.ok(!group1.getEditor(input1ResourceUpper)); assert.ok(group2.contains(input1Resource)); assert.equal(group2.getEditor(input1Resource), input1); - assert.equal(model.count(input1Resource), 1); + assert.equal(model.count(input1), 1); const input1ResourceClone = URI.file('/hello/world.txt'); const input1Clone = input(void 0, false, input1ResourceClone); @@ -1733,6 +1730,72 @@ suite('Editor Stacks Model', () => { assert.equal(input1.isDisposed(), false); }); + test('Stack - Multiple Editors - Editor Disposed on Close (same input, files)', function () { + const model = create(); + + const group1 = model.openGroup('group1'); + const group2 = model.openGroup('group2'); + + const input1 = input(void 0, void 0, URI.file('/hello/world.txt')); + + group1.openEditor(input1, { pinned: true, active: true }); + group2.openEditor(input1, { pinned: true, active: true }); + + group2.closeEditor(input1); + assert.equal(input1.isDisposed(), false); + + group1.closeEditor(input1); + assert.equal(input1.isDisposed(), true); + }); + + test('Stack - Multiple Editors - Editor Disposed on Close (same input, files, diff)', function () { + const model = create(); + + const group1 = model.openGroup('group1'); + const group2 = model.openGroup('group2'); + + const input1 = input(void 0, void 0, URI.file('/hello/world.txt')); + const input2 = input(void 0, void 0, URI.file('/hello/world_other.txt')); + + const diffInput = new DiffEditorInput('name', 'description', input2, input1); + + group1.openEditor(input1, { pinned: true, active: true }); + group2.openEditor(diffInput, { pinned: true, active: true }); + + group1.closeEditor(input1); + assert.equal(input1.isDisposed(), false); + assert.equal(input2.isDisposed(), false); + assert.equal(diffInput.isDisposed(), false); + + group2.closeEditor(diffInput); + assert.equal(input1.isDisposed(), true); + assert.equal(input2.isDisposed(), true); + assert.equal(diffInput.isDisposed(), true); + }); + + test('Stack - Multiple Editors - Editor Disposed on Close (same input, files, diff, close diff)', function () { + const model = create(); + + const group1 = model.openGroup('group1'); + const group2 = model.openGroup('group2'); + + const input1 = input(void 0, void 0, URI.file('/hello/world.txt')); + const input2 = input(void 0, void 0, URI.file('/hello/world_other.txt')); + + const diffInput = new DiffEditorInput('name', 'description', input2, input1); + + group1.openEditor(input1, { pinned: true, active: true }); + group2.openEditor(diffInput, { pinned: true, active: true }); + + group2.closeEditor(diffInput); + assert.equal(input1.isDisposed(), false); + assert.equal(input2.isDisposed(), true); + assert.equal(diffInput.isDisposed(), true); + + group1.closeEditor(input1); + assert.equal(input1.isDisposed(), true); + }); + test('Stack - Multiple Editors - Editor Emits Dirty and Label Changed', function () { const model = create(); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 8db0081e650..c262a437011 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -568,8 +568,8 @@ export class TestEditorService implements IWorkbenchEditorService { return TPromise.as(null); } - public createInput(input: IResourceInput): TPromise { - return TPromise.as(null); + public createInput(input: IResourceInput): IEditorInput { + return null; } }