diff --git a/src/vs/workbench/api/node/extHostDocuments.ts b/src/vs/workbench/api/node/extHostDocuments.ts index ce453bb14ee176d6039be638bfa14702f05f83c5..92f9e7d9bc2a34297b477d44a3eabe21f48accca 100644 --- a/src/vs/workbench/api/node/extHostDocuments.ts +++ b/src/vs/workbench/api/node/extHostDocuments.ts @@ -24,8 +24,8 @@ import * as vscode from 'vscode'; import {WordHelper} from 'vs/editor/common/model/textModelWithTokensHelpers'; import {IFileService} from 'vs/platform/files/common/files'; import {IUntitledEditorService} from 'vs/workbench/services/untitled/common/untitledEditorService'; +import {ResourceEditorInput} from 'vs/workbench/common/editor/resourceEditorInput'; import {asWinJsPromise} from 'vs/base/common/async'; -import {IModeService} from 'vs/editor/common/services/modeService'; import * as weak from 'weak'; export interface IModelAddedData { @@ -134,6 +134,7 @@ export class ExtHostModelService { throw new Error(`scheme '${scheme}' already registered`); } this._documentContentProviders[scheme] = provider; + this._proxy.$registerTextContentProvider(scheme); let subscription: IDisposable; if (typeof provider.onDidChange === 'function') { @@ -146,6 +147,7 @@ export class ExtHostModelService { }); } return new Disposable(() => { + this._proxy.$unregisterTextContentProvider(scheme); delete this._documentContentProviders[scheme]; if (subscription) { subscription.dispose(); @@ -166,8 +168,14 @@ export class ExtHostModelService { }); } - $isDocumentReferenced(uri: URI): TPromise { - return TPromise.as(this.getDocumentData(uri).isDocumentReferenced); + $getUnferencedDocuments(): TPromise { + const result: URI[] = []; + for (let key in this._documentData) { + if (!this._documentData[key].isDocumentReferenced) { + result.push(URI.parse(key)); + } + } + return TPromise.as(result); } public _acceptModelAdd(initData: IModelAddedData): void { @@ -449,7 +457,6 @@ export class ExtHostDocumentData extends MirrorModel2 { @Remotable.MainContext('MainThreadDocuments') export class MainThreadDocuments { private _modelService: IModelService; - private _modeService: IModeService; private _textFileService: ITextFileService; private _editorService: IWorkbenchEditorService; private _fileService: IFileService; @@ -458,11 +465,11 @@ export class MainThreadDocuments { private _modelToDisposeMap: { [modelUrl: string]: IDisposable; }; private _proxy: ExtHostModelService; private _modelIsSynced: { [modelId: string]: boolean; }; + private _resourceContentProvider: { [scheme: string]: IDisposable }; constructor( @IThreadService threadService: IThreadService, @IModelService modelService: IModelService, - @IModeService modeService: IModeService, @IEventService eventService: IEventService, @ITextFileService textFileService: ITextFileService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @@ -470,7 +477,6 @@ export class MainThreadDocuments { @IUntitledEditorService untitledEditorService: IUntitledEditorService ) { this._modelService = modelService; - this._modeService = modeService; this._textFileService = textFileService; this._editorService = editorService; this._fileService = fileService; @@ -493,7 +499,11 @@ export class MainThreadDocuments { this._proxy._acceptModelDirty(e.getAfter().resource); })); + const handle = setInterval(() => this._runDocumentCleanup(), 30 * 1000); + this._toDispose.push({ dispose() { clearInterval(handle) } }); + this._modelToDisposeMap = Object.create(null); + this._resourceContentProvider = Object.create(null); } public dispose(): void { @@ -571,14 +581,12 @@ export class MainThreadDocuments { let promise: TPromise; switch (uri.scheme) { - case 'file': - promise = this._handleFileScheme(uri); - break; case 'untitled': promise = this._handleUnititledScheme(uri); break; + case 'file': default: - promise = this._handleAnyScheme(uri); + promise = this._handleAsResourceInput(uri); break; } @@ -591,7 +599,7 @@ export class MainThreadDocuments { }); } - private _handleFileScheme(uri: URI): TPromise { + private _handleAsResourceInput(uri: URI): TPromise { return this._editorService.resolveEditorModel({ resource: uri }).then(model => { return !!model; }); @@ -617,46 +625,46 @@ export class MainThreadDocuments { // --- virtual document logic - private _handleAnyScheme(uri: URI): TPromise { + $registerTextContentProvider(scheme: string): void { + this._resourceContentProvider[scheme] = ResourceEditorInput.registerResourceContentProvider(scheme, { + provideTextContent: (uri: URI): TPromise => { + return this._proxy.$provideTextDocumentContent(uri); + } + }); + } - if (this._modelService.getModel(uri)) { - return TPromise.as(true); + $unregisterTextContentProvider(scheme: string): void { + const registration = this._resourceContentProvider[scheme]; + if (registration) { + registration.dispose(); } + } - return this._proxy.$provideTextDocumentContent(uri).then(value => { + $onVirtualDocumentChange(uri: URI, value: string): void { + const model = this._modelService.getModel(uri); + if (model) { + model.setValue(value); + } + } - // create document from string - const firstLineText = value.substr(0, 1 + value.search(/\r?\n/)); - const mode = this._modeService.getOrCreateModeByFilenameOrFirstLine(uri.fsPath, firstLineText); - const model = this._modelService.createModel(value, mode, uri); + private _runDocumentCleanup(): void { + this._proxy.$getUnferencedDocuments().then(resources => { - // if neither the extension host nor an editor reference this - // document anymore we destroy the model to reclaim memory - const handle = setInterval(() => { - this._editorService.inputToType({ resource: uri }).then(input => { + const toBeDisposed: URI[] = []; + const promises = resources.map(resource => { + return this._editorService.inputToType({ resource }).then(input => { if (!this._editorService.isVisible(input, true)) { - return this._proxy.$isDocumentReferenced(uri).then(referenced => { - if (!referenced) { - clearInterval(handle); - this._modelService.destroyModel(uri); - } - }); + toBeDisposed.push(resource); } - }, onUnexpectedError); - }, 30 * 1000); - - return model; + }); + }); - }).then(() => { - return true; - }); - } + return TPromise.join(promises).then(() => { + for (let resource of toBeDisposed) { + this._modelService.destroyModel(resource); + } + }); - $onVirtualDocumentChange(uri: URI, value: string): TPromise { - const model = this._modelService.getModel(uri); - if (model) { - model.setValue(value); - return; - } + }, onUnexpectedError); } } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 468f439fccf17ffa0aebfaaf654a2c9489530db3..e72ecd6a1e186d65e4581ae60d9441fd4aba4d9c 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -7,10 +7,21 @@ import {TPromise} from 'vs/base/common/winjs.base'; import {EditorModel, EditorInput} from 'vs/workbench/common/editor'; import {ResourceEditorModel} from 'vs/workbench/common/editor/resourceEditorModel'; +import {IModel} from 'vs/editor/common/editorCommon'; import URI from 'vs/base/common/uri'; import {EventType} from 'vs/base/common/events'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IModelService} from 'vs/editor/common/services/modelService'; +import {IModeService} from 'vs/editor/common/services/modeService'; +import {IDisposable} from 'vs/base/common/lifecycle'; + +/** + * + */ +export interface IResourceEditorContentProvider { + provideTextContent(resource: URI): TPromise; + // onDidChange +} /** * A read-only text editor input whos contents are made of the provided resource that points to an existing @@ -18,6 +29,57 @@ import {IModelService} from 'vs/editor/common/services/modelService'; */ export class ResourceEditorInput extends EditorInput { + // --- registry logic + // todo@joh,ben this should maybe be a service that is in charge of loading/resolving a uri from a scheme + + private static loadingModels: { [uri: string]: TPromise } = Object.create(null); + private static registry: { [scheme: string]: IResourceEditorContentProvider } = Object.create(null); + + public static registerResourceContentProvider(scheme: string, provider: IResourceEditorContentProvider): IDisposable { + ResourceEditorInput.registry[scheme] = provider; + return { dispose() { delete ResourceEditorInput.registry[scheme] } }; + } + + private static getOrCreateModel(modelService: IModelService, modeService: IModeService, resource: URI): TPromise { + const model = modelService.getModel(resource); + if (model) { + return TPromise.as(model); + } + + let loadingModel = ResourceEditorInput.loadingModels[resource.toString()]; + if (!loadingModel) { + + // make sure we have a provider this scheme + // the resource uses + const provider = ResourceEditorInput.registry[resource.scheme]; + if (!provider) { + return TPromise.wrapError(`No model with uri '${resource}' nor a resolver for the scheme '${resource.scheme}'.`); + } + + // load the model-content from the provider and cache + // the loading such that we don't create the same model + // twice + ResourceEditorInput.loadingModels[resource.toString()] = loadingModel = new TPromise((resolve, reject) => { + + provider.provideTextContent(resource).then(value => { + const firstLineText = value.substr(0, 1 + value.search(/\r?\n/)); + const mode = modeService.getOrCreateModeByFilenameOrFirstLine(resource.fsPath, firstLineText); + return modelService.createModel(value, mode, resource); + }).then(resolve, reject); + + }, function() { + // no cancellation when caching promises + }); + + // remove the cached promise 'cos the model is now + // known to the model service (see above) + loadingModel.then(() => delete ResourceEditorInput.loadingModels[resource.toString()], () => delete ResourceEditorInput.loadingModels[resource.toString()]); + } + + return loadingModel; + } + + public static ID: string = 'workbench.editors.resourceEditorInput'; protected cachedModel: ResourceEditorModel; @@ -31,6 +93,7 @@ export class ResourceEditorInput extends EditorInput { description: string, resource: URI, @IModelService protected modelService: IModelService, + @IModeService protected modeService: IModeService, @IInstantiationService protected instantiationService: IInstantiationService ) { super(); @@ -60,18 +123,20 @@ export class ResourceEditorInput extends EditorInput { } // Otherwise Create Model and handle dispose event - let model = this.instantiationService.createInstance(ResourceEditorModel, this.resource); - const unbind = model.addListener(EventType.DISPOSE, () => { - this.cachedModel = null; // make sure we do not dispose model again - unbind(); - this.dispose(); - }); - - // Load it - return model.load().then((resolvedModel: ResourceEditorModel) => { - this.cachedModel = resolvedModel; - - return this.cachedModel; + return ResourceEditorInput.getOrCreateModel(this.modelService, this.modeService, this.resource).then(() => { + let model = this.instantiationService.createInstance(ResourceEditorModel, this.resource); + const unbind = model.addListener(EventType.DISPOSE, () => { + this.cachedModel = null; // make sure we do not dispose model again + unbind(); + this.dispose(); + }); + + // Load it + return model.load().then((resolvedModel: ResourceEditorModel) => { + this.cachedModel = resolvedModel; + + return this.cachedModel; + }); }); } diff --git a/src/vs/workbench/parts/files/browser/saveErrorHandler.ts b/src/vs/workbench/parts/files/browser/saveErrorHandler.ts index ba5b4849fe4d080f349289c09b7715b6f25dce22..2661d26d14913b52f359177aca3a4425c92f42ba 100644 --- a/src/vs/workbench/parts/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/browser/saveErrorHandler.ts @@ -151,14 +151,14 @@ export class FileOnDiskEditorInput extends ResourceEditorInput { name: string, description: string, @IModelService modelService: IModelService, - @IModeService private modeService: IModeService, + @IModeService modeService: IModeService, @IInstantiationService instantiationService: IInstantiationService, @IFileService private fileService: IFileService ) { // We create a new resource URI here that is different from the file resource because we represent the state of // the file as it is on disk and not as it is (potentially cached) in Code. That allows us to have a different // model for the left-hand comparision compared to the conflicting one in Code to the right. - super(name, description, URI.create('disk', null, fileResource.fsPath), modelService, instantiationService); + super(name, description, URI.create('disk', null, fileResource.fsPath), modelService, modeService, instantiationService); this.fileResource = fileResource; this.mime = mime; diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index a1e09ee2fed9a6feb5283f066fb461b07024537e..fc7cb514d9cde1ba690287ddf8f86c206a922768 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -9,6 +9,7 @@ import URI from 'vs/base/common/uri'; import network = require('vs/base/common/network'); import {guessMimeTypes} from 'vs/base/common/mime'; import {Registry} from 'vs/platform/platform'; +import {basename, dirname} from 'vs/base/common/paths'; import types = require('vs/base/common/types'); import {IDiffEditor, ICodeEditor} from 'vs/editor/browser/editorBrowser'; import {ICommonCodeEditor, IModel, EditorType, IEditor as ICommonEditor} from 'vs/editor/common/editorCommon'; @@ -289,8 +290,12 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { return this.createFileInput(resourceInput.resource, resourceInput.mime); } + // Treat an URI as ResourceEditorInput else if (URI.isURI(resourceInput.resource)) { - return TPromise.as(this.instantiationService.createInstance(ResourceEditorInput, resourceInput.resource.fsPath, undefined, resourceInput.resource)); + return TPromise.as(this.instantiationService.createInstance(ResourceEditorInput, + basename(resourceInput.resource.fsPath), + dirname(resourceInput.resource.fsPath), + resourceInput.resource)); } return TPromise.as(null);