提交 adf528e1 编写于 作者: J João Moreno 提交者: GitHub

Merge pull request #15797 from joaomoreno/reference-counted-models

Reference counted (workbench) text models
......@@ -369,15 +369,15 @@ export function always<T>(promise: TPromise<T>, f: Function): TPromise<T> {
* Runs the provided list of promise factories in sequential order. The returned
* promise will complete to an array of results from each promise.
*/
export function sequence<T>(promiseFactory: ITask<TPromise<T>>[]): TPromise<T[]> {
export function sequence<T>(promiseFactories: ITask<TPromise<T>>[]): TPromise<T[]> {
const results: T[] = [];
// reverse since we start with last element using pop()
promiseFactory = promiseFactory.reverse();
promiseFactories = promiseFactories.reverse();
function next(): Promise {
if (promiseFactory.length) {
return promiseFactory.pop()();
if (promiseFactories.length) {
return promiseFactories.pop()();
}
return null;
......@@ -399,6 +399,29 @@ export function sequence<T>(promiseFactory: ITask<TPromise<T>>[]): TPromise<T[]>
return TPromise.as(null).then(thenHandler);
}
export function first<T>(promiseFactories: ITask<TPromise<T>>[], shouldStop: (t: T) => boolean = t => !!t): TPromise<T> {
promiseFactories = [...promiseFactories.reverse()];
const loop = () => {
if (promiseFactories.length === 0) {
return TPromise.as(null);
}
const factory = promiseFactories.pop();
const promise = factory();
return promise.then(result => {
if (shouldStop(result)) {
return TPromise.as(result);
}
return loop();
});
};
return loop();
}
export function once<T extends Function>(fn: T): T {
const _this = this;
let didCall = false;
......
......@@ -67,3 +67,42 @@ export class Disposables extends Disposable {
}
}
}
export interface IReference<T> extends IDisposable {
readonly object: T;
}
export abstract class ReferenceCollection<T> {
private references: { [key: string]: { readonly object: T; counter: number; } } = Object.create(null);
constructor() { }
acquire(key: string): IReference<T> {
let reference = this.references[key];
if (!reference) {
reference = this.references[key] = { counter: 0, object: this.createReferencedObject(key) };
}
const { object } = reference;
const dispose = () => {
if (--reference.counter === 0) {
this.destroyReferencedObject(reference.object);
delete this.references[key];
}
};
reference.counter++;
return { object, dispose };
}
protected abstract createReferencedObject(key: string): T;
protected abstract destroyReferencedObject(object: T): void;
}
export class ImmortalReference<T> implements IReference<T> {
constructor(public object: T) { }
dispose(): void { /* noop */ }
}
\ No newline at end of file
......@@ -5,7 +5,7 @@
'use strict';
import * as assert from 'assert';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, ReferenceCollection } from 'vs/base/common/lifecycle';
class Disposable implements IDisposable {
isDisposed = false;
......@@ -49,4 +49,42 @@ suite('Lifecycle', () => {
assert(disposable.isDisposed);
assert(disposable2.isDisposed);
});
});
suite('Reference Collection', () => {
class Collection extends ReferenceCollection<number> {
private _count = 0;
get count() { return this._count; }
protected createReferencedObject(key: string): number { this._count++; return key.length; }
protected destroyReferencedObject(object: number): void { this._count--; }
}
test('simple', () => {
const collection = new Collection();
const ref1 = collection.acquire('test');
assert(ref1);
assert.equal(ref1.object, 4);
assert.equal(collection.count, 1);
ref1.dispose();
assert.equal(collection.count, 0);
const ref2 = collection.acquire('test');
const ref3 = collection.acquire('test');
assert.equal(ref2.object, ref3.object);
assert.equal(collection.count, 1);
const ref4 = collection.acquire('monkey');
assert.equal(ref4.object, 6);
assert.equal(collection.count, 2);
ref2.dispose();
assert.equal(collection.count, 2);
ref3.dispose();
assert.equal(collection.count, 1);
ref4.dispose();
assert.equal(collection.count, 0);
});
});
\ No newline at end of file
......@@ -9,7 +9,7 @@ import Severity from 'vs/base/common/severity';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { IConfigurationService, IConfigurationServiceEvent, IConfigurationValue, getConfigurationValue, IConfigurationKeys } from 'vs/platform/configuration/common/configuration';
import { IEditor, IEditorInput, IEditorOptions, IEditorService, IResourceInput, ITextEditorModel, Position } from 'vs/platform/editor/common/editor';
import { IEditor, IEditorInput, IEditorOptions, IEditorService, IResourceInput, Position } from 'vs/platform/editor/common/editor';
import { AbstractExtensionService, ActivatedExtension } from 'vs/platform/extensions/common/abstractExtensionService';
import { IExtensionDescription, IExtensionService } from 'vs/platform/extensions/common/extensions';
import { ICommandService, ICommand, ICommandHandler } from 'vs/platform/commands/common/commands';
......@@ -26,8 +26,8 @@ import { getDefaultValues as getDefaultConfiguration } from 'vs/platform/configu
import { CommandService } from 'vs/platform/commands/common/commandService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/progress';
import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ITextModelResolverService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IDisposable, IReference, ImmortalReference } from 'vs/base/common/lifecycle';
export class SimpleEditor implements IEditor {
......@@ -173,7 +173,7 @@ export class SimpleEditorModelResolverService implements ITextModelResolverServi
this.editor = new SimpleEditor(editor);
}
public resolve(resource: URI): TPromise<ITextEditorModel> {
public createModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
let model: editorCommon.IModel;
model = this.editor.withTypedEditor(
......@@ -182,10 +182,10 @@ export class SimpleEditorModelResolverService implements ITextModelResolverServi
);
if (!model) {
return TPromise.as(null);
return TPromise.as(new ImmortalReference(null));
}
return TPromise.as(new SimpleModel(model));
return TPromise.as(new ImmortalReference(new SimpleModel(model)));
}
public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
......
......@@ -7,9 +7,10 @@
import * as nls from 'vs/nls';
import { merge } from 'vs/base/common/arrays';
import { IStringDictionary, forEach, values } from 'vs/base/common/collections';
import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITextModelResolverService } from 'vs/editor/common/services/resolverService';
import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IEventService } from 'vs/platform/event/common/event';
import { EventType as FileEventType, FileChangesEvent, IFileChange } from 'vs/platform/files/common/files';
import { EditOperation } from 'vs/editor/common/core/editOperation';
......@@ -64,16 +65,17 @@ class ChangeRecorder {
}
}
class EditTask {
class EditTask implements IDisposable {
private _initialSelections: Selection[];
private _endCursorSelection: Selection;
private _model: IModel;
private get _model(): IModel { return this._modelReference.object.textEditorModel; }
private _modelReference: IReference<ITextEditorModel>;
private _edits: IIdentifiedSingleEditOperation[];
constructor(model: IModel) {
constructor(modelReference: IReference<ITextEditorModel>) {
this._endCursorSelection = null;
this._model = model;
this._modelReference = modelReference;
this._edits = [];
}
......@@ -138,14 +140,21 @@ class EditTask {
private static _editCompare(a: IIdentifiedSingleEditOperation, b: IIdentifiedSingleEditOperation): number {
return Range.compareRangesUsingStarts(a.range, b.range);
}
dispose() {
if (this._model) {
this._modelReference.dispose();
this._modelReference = null;
}
}
}
class SourceModelEditTask extends EditTask {
private _knownInitialSelections: Selection[];
constructor(model: IModel, initialSelections: Selection[]) {
super(model);
constructor(modelReference: IReference<ITextEditorModel>, initialSelections: Selection[]) {
super(modelReference);
this._knownInitialSelections = initialSelections;
}
......@@ -154,7 +163,7 @@ class SourceModelEditTask extends EditTask {
}
}
class BulkEditModel {
class BulkEditModel implements IDisposable {
private _textModelResolverService: ITextModelResolverService;
private _numberOfResourcesToModify: number = 0;
......@@ -208,7 +217,9 @@ class BulkEditModel {
}
forEach(this._edits, entry => {
const promise = this._textModelResolverService.resolve(URI.parse(entry.key)).then(model => {
const promise = this._textModelResolverService.createModelReference(URI.parse(entry.key)).then(ref => {
const model = ref.object;
if (!model || !model.textEditorModel) {
throw new Error(`Cannot load file ${entry.key}`);
}
......@@ -217,10 +228,10 @@ class BulkEditModel {
let task: EditTask;
if (this._sourceModel && textEditorModel.uri.toString() === this._sourceModel.toString()) {
this._sourceModelTask = new SourceModelEditTask(textEditorModel, this._sourceSelections);
this._sourceModelTask = new SourceModelEditTask(ref, this._sourceSelections);
task = this._sourceModelTask;
} else {
task = new EditTask(textEditorModel);
task = new EditTask(ref);
}
entry.value.forEach(edit => task.addEdit(edit));
......@@ -251,6 +262,10 @@ class BulkEditModel {
this.progress.worked(1);
}
}
dispose(): void {
this._tasks = dispose(this._tasks);
}
}
export interface BulkEdit {
......@@ -314,7 +329,7 @@ export function createBulkEdit(eventService: IEventService, textModelResolverSer
selections = editor.getSelections();
}
let model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner);
const model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner);
return model.prepare().then(_ => {
......@@ -324,7 +339,10 @@ export function createBulkEdit(eventService: IEventService, textModelResolverSer
}
recording.stop();
return model.apply();
const result = model.apply();
model.dispose();
return result;
});
}
......
......@@ -8,8 +8,8 @@ import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IModel } from 'vs/editor/common/editorCommon';
import { ITextEditorModel as IBaseTextEditorModel } from 'vs/platform/editor/common/editor';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
export const ITextModelResolverService = createDecorator<ITextModelResolverService>('textModelResolverService');
......@@ -17,14 +17,13 @@ export interface ITextModelResolverService {
_serviceBrand: any;
/**
* Given a resource, tries to resolve a ITextEditorModel out of it. Will support many schemes like file://, untitled://,
* inMemory:// and for anything else fall back to the model content provider registry.
* Provided a resource URI, it will return a model reference
* which should be disposed once not needed anymore.
*/
resolve(resource: URI): TPromise<ITextEditorModel>;
createModelReference(resource: URI): TPromise<IReference<ITextEditorModel>>;
/**
* For unknown resources, allows to register a content provider such as this service is able to resolve arbritrary
* resources to ITextEditorModels.
* Registers a specific `scheme` content provider.
*/
registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable;
}
......@@ -37,7 +36,7 @@ export interface ITextModelContentProvider {
provideTextContent(resource: URI): TPromise<IModel>;
}
export interface ITextEditorModel extends IBaseTextEditorModel {
export interface ITextEditorModel extends IEditorModel {
/**
* Provides access to the underlying IModel.
......
......@@ -112,6 +112,8 @@ export class DefinitionAction extends EditorAction {
this._openReference(editorService, next, this._configuration.openToSide).then(editor => {
if (model.references.length > 1) {
this._openInPeek(editorService, editor, model);
} else {
model.dispose();
}
});
}
......@@ -142,6 +144,8 @@ export class DefinitionAction extends EditorAction {
return this._openReference(editorService, reference, false);
}
});
} else {
model.dispose();
}
}
}
......@@ -319,7 +323,9 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC
// Single result
else {
let result = results[0];
this.textModelResolverService.resolve(result.uri).then(model => {
this.textModelResolverService.createModelReference(result.uri).then(ref => {
const model = ref.object;
let hoverMessage: MarkedString;
if (model && model.textEditorModel) {
const editorModel = model.textEditorModel;
......@@ -369,6 +375,8 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC
};
}
ref.dispose();
this.addDecoration({
startLineNumber: position.lineNumber,
startColumn: word.startColumn,
......
......@@ -147,6 +147,11 @@ export class ReferencesController implements editorCommon.IEditorContribution {
if (requestId !== this._requestIdPool || !this._widget) {
return;
}
if (this._model) {
this._model.dispose();
}
this._model = model;
// measure time it stays open
......@@ -194,7 +199,10 @@ export class ReferencesController implements editorCommon.IEditorContribution {
}
this._referenceSearchVisible.reset();
this._disposables = dispose(this._disposables);
this._model = null;
if (this._model) {
this._model.dispose();
this._model = null;
}
this._editor.focus();
this._requestIdPool += 1; // Cancel pending requests
}
......
......@@ -7,14 +7,15 @@
import { EventEmitter } from 'vs/base/common/eventEmitter';
import Event, { fromEventEmitter } from 'vs/base/common/event';
import { basename, dirname } from 'vs/base/common/paths';
import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
import URI from 'vs/base/common/uri';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { TPromise } from 'vs/base/common/winjs.base';
import { Range } from 'vs/editor/common/core/range';
import { IModel, IPosition, IRange } from 'vs/editor/common/editorCommon';
import { IPosition, IRange } from 'vs/editor/common/editorCommon';
import { Location } from 'vs/editor/common/modes';
import { ITextModelResolverService } from 'vs/editor/common/services/resolverService';
import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
export class OneReference {
......@@ -62,30 +63,39 @@ export class OneReference {
}
}
export class FilePreview {
export class FilePreview implements IDisposable {
constructor(private _value: IModel) {
constructor(private _modelReference: IReference<ITextEditorModel>) {
}
private get _model() { return this._modelReference.object.textEditorModel; }
public preview(range: IRange, n: number = 8): { before: string; inside: string; after: string } {
const {startLineNumber, startColumn, endColumn} = range;
const word = this._value.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n });
const word = this._model.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n });
const beforeRange = new Range(startLineNumber, word.startColumn, startLineNumber, startColumn);
const afterRange = new Range(startLineNumber, endColumn, startLineNumber, Number.MAX_VALUE);
const ret = {
before: this._value.getValueInRange(beforeRange).replace(/^\s+/, strings.empty),
inside: this._value.getValueInRange(range),
after: this._value.getValueInRange(afterRange).replace(/\s+$/, strings.empty)
before: this._model.getValueInRange(beforeRange).replace(/^\s+/, strings.empty),
inside: this._model.getValueInRange(range),
after: this._model.getValueInRange(afterRange).replace(/\s+$/, strings.empty)
};
return ret;
}
dispose(): void {
if (this._modelReference) {
this._modelReference.dispose();
this._modelReference = null;
}
}
}
export class FileReferences {
export class FileReferences implements IDisposable {
private _children: OneReference[];
private _preview: FilePreview;
......@@ -134,11 +144,15 @@ export class FileReferences {
return TPromise.as(this);
}
return textModelResolverService.resolve(this._uri).then(model => {
return textModelResolverService.createModelReference(this._uri).then(modelReference => {
const model = modelReference.object;
if (!model) {
modelReference.dispose();
throw new Error();
}
this._preview = new FilePreview(model.textEditorModel);
this._preview = new FilePreview(modelReference);
this._resolved = true;
return this;
......@@ -150,9 +164,16 @@ export class FileReferences {
return this;
});
}
dispose(): void {
if (this._preview) {
this._preview.dispose();
this._preview = null;
}
}
}
export class ReferencesModel {
export class ReferencesModel implements IDisposable {
private _groups: FileReferences[] = [];
private _references: OneReference[] = [];
......@@ -239,6 +260,10 @@ export class ReferencesModel {
}
}
dispose(): void {
this._groups = dispose(this._groups);
}
private static _compareReferences(a: Location, b: Location): number {
if (a.uri.toString() < b.uri.toString()) {
return -1;
......
......@@ -10,7 +10,7 @@ import * as collections from 'vs/base/common/collections';
import { onUnexpectedError } from 'vs/base/common/errors';
import { getPathLabel } from 'vs/base/common/labels';
import Event, { Emitter } from 'vs/base/common/event';
import { IDisposable, dispose, Disposables } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposables, empty as EmptyDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import * as strings from 'vs/base/common/strings';
import { TPromise } from 'vs/base/common/winjs.base';
......@@ -494,6 +494,7 @@ export class ReferenceWidget extends PeekViewWidget {
private _treeContainer: Builder;
private _sash: VSash;
private _preview: ICodeEditor;
private _previewModelReference: IDisposable = EmptyDisposable;
private _previewNotAvailableMessage: Model;
private _previewContainer: Builder;
private _messageContainer: Builder;
......@@ -715,24 +716,31 @@ export class ReferenceWidget extends PeekViewWidget {
this.setTitle(nls.localize('peekView.alternateTitle', "References"));
}
return TPromise.join([
this._textModelResolverService.resolve(reference.uri),
this._tree.reveal(reference)
]).then(values => {
const promise = this._textModelResolverService.createModelReference(reference.uri);
return TPromise.join([promise, this._tree.reveal(reference)]).then(values => {
const ref = values[0];
if (!this._model) {
ref.dispose();
// disposed
return;
}
this._previewModelReference.dispose();
this._previewModelReference = EmptyDisposable;
// show in editor
let [model] = values;
const model = ref.object;
if (model) {
this._previewModelReference = ref;
this._preview.setModel(model.textEditorModel);
var sel = Range.lift(reference.range).collapseToStart();
this._preview.setSelection(sel);
this._preview.revealRangeInCenter(sel);
} else {
this._preview.setModel(this._previewNotAvailableMessage);
ref.dispose();
}
// show in tree
......
......@@ -37,10 +37,6 @@ export interface IEditorModel {
dispose(): void;
}
export interface ITextEditorModel extends IEditorModel {
textEditorModel: any;
}
export interface IResourceInput {
/**
......
......@@ -187,8 +187,11 @@ export class MainThreadDocuments extends MainThreadDocumentsShape {
}
private _handleAsResourceInput(uri: URI): TPromise<boolean> {
return this._textModelResolverService.resolve(uri).then(model => {
return !!model;
return this._textModelResolverService.createModelReference(uri).then(ref => {
const result = !!ref.object;
ref.dispose();
return result;
});
}
......
......@@ -107,7 +107,7 @@ export class StringEditor extends BaseTextEditor {
// Set Editor Model
const textEditor = this.getControl();
const textEditorModel = (<BaseTextEditorModel>resolvedModel).textEditorModel;
const textEditorModel = resolvedModel.textEditorModel;
textEditor.setModel(textEditorModel);
// Apply Options from TextOptions
......
......@@ -12,8 +12,8 @@ import { EditorModel } from 'vs/workbench/common/editor';
* and the modified version.
*/
export class DiffEditorModel extends EditorModel {
private _originalModel: EditorModel;
private _modifiedModel: EditorModel;
protected _originalModel: EditorModel;
protected _modifiedModel: EditorModel;
constructor(originalModel: EditorModel, modifiedModel: EditorModel) {
super();
......
......@@ -7,6 +7,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { EditorInput, ITextEditorModel } from 'vs/workbench/common/editor';
import URI from 'vs/base/common/uri';
import { IReference } from 'vs/base/common/lifecycle';
import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetry';
import { ITextModelResolverService } from 'vs/editor/common/services/resolverService';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
......@@ -17,9 +18,9 @@ import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorMo
*/
export class ResourceEditorInput extends EditorInput {
public static ID: string = 'workbench.editors.resourceEditorInput';
static ID: string = 'workbench.editors.resourceEditorInput';
protected cachedModel: ResourceEditorModel;
protected promise: TPromise<IReference<ResourceEditorModel>>;
protected resource: URI;
private name: string;
......@@ -38,64 +39,60 @@ export class ResourceEditorInput extends EditorInput {
this.resource = resource;
}
public getTypeId(): string {
getTypeId(): string {
return ResourceEditorInput.ID;
}
public getName(): string {
getName(): string {
return this.name;
}
public setName(name: string): void {
setName(name: string): void {
if (this.name !== name) {
this.name = name;
this._onDidChangeLabel.fire();
}
}
public getDescription(): string {
getDescription(): string {
return this.description;
}
public setDescription(description: string): void {
setDescription(description: string): void {
if (this.description !== description) {
this.description = description;
this._onDidChangeLabel.fire();
}
}
public getTelemetryDescriptor(): { [key: string]: any; } {
getTelemetryDescriptor(): { [key: string]: any; } {
const descriptor = super.getTelemetryDescriptor();
descriptor['resource'] = telemetryURIDescriptor(this.resource);
return descriptor;
}
public resolve(refresh?: boolean): TPromise<ITextEditorModel> {
// Use Cached Model
if (this.cachedModel) {
return TPromise.as<ITextEditorModel>(this.cachedModel);
resolve(refresh?: boolean): TPromise<ITextEditorModel> {
if (!this.promise) {
this.promise = this.textModelResolverService.createModelReference(this.resource);
}
// Otherwise Create Model and handle dispose event
return this.textModelResolverService.resolve(this.resource).then(model => {
return this.promise.then(ref => {
const model = ref.object;
if (!(model instanceof ResourceEditorModel)) {
ref.dispose();
this.promise = 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()
}
this.cachedModel = model;
const unbind = model.onDispose(() => {
this.cachedModel = null; // make sure we do not dispose model again
unbind.dispose();
this.dispose();
});
// TODO@Joao this should never happen
model.onDispose(() => this.dispose());
return this.cachedModel;
return model;
});
}
public matches(otherInput: any): boolean {
matches(otherInput: any): boolean {
if (super.matches(otherInput) === true) {
return true;
}
......@@ -110,10 +107,10 @@ export class ResourceEditorInput extends EditorInput {
return false;
}
public dispose(): void {
if (this.cachedModel) {
this.cachedModel.dispose();
this.cachedModel = null;
dispose(): void {
if (this.promise) {
this.promise.done(ref => ref.dispose());
this.promise = null;
}
super.dispose();
......
......@@ -23,6 +23,14 @@ export class TextDiffEditorModel extends DiffEditorModel {
this.updateTextDiffEditorModel();
}
get originalModel(): BaseTextEditorModel {
return this._originalModel as BaseTextEditorModel;
}
get modifiedModel(): BaseTextEditorModel {
return this._modifiedModel as BaseTextEditorModel;
}
public load(): TPromise<EditorModel> {
return super.load().then(() => {
this.updateTextDiffEditorModel();
......@@ -37,15 +45,15 @@ export class TextDiffEditorModel extends DiffEditorModel {
// Create new
if (!this._textDiffEditorModel) {
this._textDiffEditorModel = {
original: (<BaseTextEditorModel>this.originalModel).textEditorModel,
modified: (<BaseTextEditorModel>this.modifiedModel).textEditorModel
original: this.originalModel.textEditorModel,
modified: this.modifiedModel.textEditorModel
};
}
// Update existing
else {
this._textDiffEditorModel.original = (<BaseTextEditorModel>this.originalModel).textEditorModel;
this._textDiffEditorModel.modified = (<BaseTextEditorModel>this.modifiedModel).textEditorModel;
this._textDiffEditorModel.original = this.originalModel.textEditorModel;
this._textDiffEditorModel.modified = this.modifiedModel.textEditorModel;
}
}
}
......
......@@ -9,7 +9,7 @@ import { EndOfLinePreference, IModel, IRawText } from 'vs/editor/common/editorCo
import { IMode } from 'vs/editor/common/modes';
import { EditorModel } from 'vs/workbench/common/editor';
import URI from 'vs/base/common/uri';
import { ITextEditorModel } from 'vs/platform/editor/common/editor';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { RawText } from 'vs/editor/common/model/textModel';
......
......@@ -60,8 +60,13 @@ CommandsRegistry.registerCommand('_workbench.htmlZone', function (accessor: Serv
return;
}
return accessor.get(ITextModelResolverService).resolve(params.resource).then(model => {
const textModelResolverService = accessor.get(ITextModelResolverService);
return textModelResolverService.createModelReference(params.resource).then(ref => {
const model = ref.object;
const contents = model.textEditorModel.getValue();
ref.dispose();
HtmlZoneController.getInstance(codeEditor).addZone(params.lineNumber, contents);
});
......
......@@ -10,7 +10,7 @@ import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { IModel } from 'vs/editor/common/editorCommon';
import { Dimension, Builder } from 'vs/base/browser/builder';
import { empty as EmptyDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { empty as EmptyDisposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle';
import { EditorOptions, EditorInput } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { Position } from 'vs/platform/editor/common/editor';
......@@ -20,7 +20,7 @@ import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'
import { HtmlInput } from 'vs/workbench/parts/html/common/htmlInput';
import { IThemeService } from 'vs/workbench/services/themes/common/themeService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITextModelResolverService } from 'vs/editor/common/services/resolverService';
import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import Webview from './webview';
/**
......@@ -39,7 +39,8 @@ export class HtmlPreviewPart extends BaseEditor {
private _baseUrl: URI;
private _model: IModel;
private _modelRef: IReference<ITextEditorModel>;
private get _model(): IModel { return this._modelRef.object.textEditorModel; }
private _modelChangeSubscription = EmptyDisposable;
private _themeChangeSubscription = EmptyDisposable;
......@@ -65,7 +66,9 @@ export class HtmlPreviewPart extends BaseEditor {
// unhook listeners
this._themeChangeSubscription.dispose();
this._modelChangeSubscription.dispose();
this._model = undefined;
if (this._modelRef) {
this._modelRef.dispose();
}
super.dispose();
}
......@@ -121,7 +124,7 @@ export class HtmlPreviewPart extends BaseEditor {
}
private _hasValidModel(): boolean {
return this._model && !this._model.isDisposed();
return this._modelRef && this._model && !this._model.isDisposed();
}
public layout(dimension: Dimension): void {
......@@ -141,7 +144,9 @@ export class HtmlPreviewPart extends BaseEditor {
return TPromise.as(undefined);
}
this._model = undefined;
if (this._modelRef) {
this._modelRef.dispose();
}
this._modelChangeSubscription.dispose();
if (!(input instanceof HtmlInput)) {
......@@ -149,14 +154,18 @@ export class HtmlPreviewPart extends BaseEditor {
}
return super.setInput(input, options).then(() => {
let resourceUri = (<HtmlInput>input).getResource();
return this._textModelResolverService.resolve(resourceUri).then(model => {
const resourceUri = (<HtmlInput>input).getResource();
return this._textModelResolverService.createModelReference(resourceUri).then(ref => {
const model = ref.object;
if (model instanceof BaseTextEditorModel) {
this._model = model.textEditorModel;
this._modelRef = ref;
}
if (!this._model) {
return TPromise.wrapError<void>(localize('html.voidInput', "Invalid editor input."));
}
this._modelChangeSubscription = this._model.onDidChangeContent(() => this.webview.contents = this._model.getLinesContent());
this.webview.baseUrl = resourceUri.toString(true);
this.webview.contents = this._model.getLinesContent();
......
......@@ -57,11 +57,13 @@ class EditorInputCache {
if (editorInputPromise) {
editorInputPromise.done(() => {
if (reloadFromSource) {
this.textModelResolverService.resolve(fileMatch.resource()).then(editorModel => {
if (editorModel.textEditorModel) {
this.textModelResolverService.createModelReference(fileMatch.resource()).done(ref => {
const model = ref.object;
if (model.textEditorModel) {
let replaceResource = this.getReplaceResource(fileMatch.resource());
this.modelService.getModel(replaceResource).setValue(editorModel.textEditorModel.getValue());
this.modelService.getModel(replaceResource).setValue(model.textEditorModel.getValue());
this.replaceService.replace(fileMatch, null, replaceResource);
ref.dispose();
}
});
} else {
......@@ -107,13 +109,14 @@ class EditorInputCache {
}
private createRightInput(element: FileMatch): TPromise<IEditorInput> {
return new TPromise((c, e, p) => {
this.textModelResolverService.resolve(element.resource()).then(value => {
let model = value.textEditorModel;
let replaceResource = this.getReplaceResource(element.resource());
this.modelService.createModel(model.getValue(), model.getMode(), replaceResource);
c(this.editorService.createInput({ resource: replaceResource }));
});
return this.textModelResolverService.createModelReference(element.resource()).then(ref => {
const model = ref.object;
let textEditorModel = model.textEditorModel;
let replaceResource = this.getReplaceResource(element.resource());
this.modelService.createModel(textEditorModel.getValue(), textEditorModel.getMode(), replaceResource);
ref.dispose();
return this.editorService.createInput({ resource: replaceResource });
});
}
......
......@@ -9,9 +9,6 @@ import URI from 'vs/base/common/uri';
import network = require('vs/base/common/network');
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';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions, IFileEditorInput, TextEditorOptions, IEditorRegistry, Extensions } from 'vs/workbench/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
......@@ -206,46 +203,9 @@ export class WorkbenchEditorService implements IWorkbenchEditorService {
// Base Text Editor Support for inmemory resources
const resourceInput = <IResourceInput>input;
if (resourceInput.resource instanceof URI && resourceInput.resource.scheme === network.Schemas.inMemory) {
// For in-memory resources we only support to resolve the input from the current active editor
// because the workbench does not track editor models by in memory URL. This concept is only
// being used in the code editor.
const activeEditor = this.getActiveEditor();
if (activeEditor) {
const control = <ICommonEditor>activeEditor.getControl();
if (types.isFunction(control.getEditorType)) {
// Single Editor: If code editor model matches, return input from editor
if (control.getEditorType() === EditorType.ICodeEditor) {
const codeEditor = <ICodeEditor>control;
const model = this.findModel(codeEditor, input);
if (model) {
return TPromise.as(activeEditor.input);
}
}
// Diff Editor: If left or right code editor model matches, return associated input
else if (control.getEditorType() === EditorType.IDiffEditor) {
const diffInput = <DiffEditorInput>activeEditor.input;
const diffCodeEditor = <IDiffEditor>control;
const originalModel = this.findModel(diffCodeEditor.getOriginalEditor(), input);
if (originalModel) {
return TPromise.as(diffInput.originalInput);
}
const modifiedModel = this.findModel(diffCodeEditor.getModifiedEditor(), input);
if (modifiedModel) {
return TPromise.as(diffInput.modifiedInput);
}
}
}
}
}
// Untitled file support
else if (resourceInput.resource instanceof URI && (resourceInput.resource.scheme === UntitledEditorInput.SCHEMA)) {
if (resourceInput.resource instanceof URI && (resourceInput.resource.scheme === UntitledEditorInput.SCHEMA)) {
return TPromise.as<EditorInput>(this.untitledEditorService.createOrGet(resourceInput.resource));
}
......@@ -273,15 +233,6 @@ export class WorkbenchEditorService implements IWorkbenchEditorService {
return typedFileInput;
});
}
private findModel(editor: ICommonCodeEditor, input: IResourceInput): IModel {
const model = editor.getModel();
if (!model) {
return null;
}
return model.uri.toString() === input.resource.toString() ? model : null;
}
}
export interface IDelegatingWorkbenchEditorServiceHandler {
......
......@@ -12,7 +12,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { IEncodingSupport, ConfirmResult } from 'vs/workbench/common/editor';
import { IFileStat, IBaseStat, IResolveContentOptions } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ITextEditorModel } from 'vs/platform/editor/common/editor';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { Event as BaseEvent, PropertyChangeEvent } from 'vs/base/common/events';
......
......@@ -6,142 +6,137 @@
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { first, always } from 'vs/base/common/async';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModel } from 'vs/editor/common/editorCommon';
import { ITextEditorModel } from 'vs/platform/editor/common/editor';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable, IReference, ReferenceCollection, ImmortalReference } from 'vs/base/common/lifecycle';
import { IModelService } from 'vs/editor/common/services/modelService';
import { sequence } from 'vs/base/common/async';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import network = require('vs/base/common/network');
import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
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 { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { EditorInput } from 'vs/workbench/common/editor';
export class TextModelResolverService implements ITextModelResolverService {
public _serviceBrand: any;
class ResourceModelCollection extends ReferenceCollection<TPromise<ITextEditorModel>> {
private loadingTextModels: { [uri: string]: TPromise<IModel> } = Object.create(null);
private contentProviderRegistry: { [scheme: string]: ITextModelContentProvider[] } = Object.create(null);
private providers: { [scheme: string]: ITextModelContentProvider[] } = Object.create(null);
constructor(
@ITextFileService private textFileService: ITextFileService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IInstantiationService private instantiationService: IInstantiationService,
@IModelService private modelService: IModelService
) {
super();
}
public resolve(resource: URI): TPromise<ITextEditorModel> {
createReferencedObject(key: string): TPromise<ITextEditorModel> {
const resource = URI.parse(key);
// File Schema: use text file service
if (resource.scheme === network.Schemas.file) {
return this.textFileService.models.loadOrCreate(resource);
}
return this.resolveTextModelContent(this.modelService, key)
.then(() => this.instantiationService.createInstance(ResourceEditorModel, resource));
}
// Untitled Schema: go through cached input
if (resource.scheme === UntitledEditorInput.SCHEMA) {
return this.untitledEditorService.createOrGet(resource).resolve();
}
destroyReferencedObject(modelPromise: TPromise<ITextEditorModel>): void {
modelPromise.done(model => model.dispose());
}
// In Memory: only works on the active editor
if (resource.scheme === network.Schemas.inMemory) {
return this.editorService.createInput({ resource }).then(input => {
if (input instanceof EditorInput) {
return input.resolve();
}
registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
const registry = this.providers;
const providers = registry[scheme] || (registry[scheme] = []);
return null;
});
}
providers.unshift(provider);
// Any other resource: use content provider registry
return this.resolveTextModelContent(this.modelService, resource).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource));
}
return toDisposable(() => {
const array = registry[scheme];
public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
let array = this.contentProviderRegistry[scheme];
if (!array) {
array = [provider];
this.contentProviderRegistry[scheme] = array;
} else {
array.unshift(provider);
}
if (!array) {
return;
}
const index = array.indexOf(provider);
if (index === -1) {
return;
}
array.splice(index, 1);
const registry = this.contentProviderRegistry;
return {
dispose() {
const array = registry[scheme];
const idx = array.indexOf(provider);
if (idx >= 0) {
array.splice(idx, 1);
if (array.length === 0) {
delete registry[scheme];
}
}
if (array.length === 0) {
delete registry[scheme];
}
};
});
}
private resolveTextModelContent(modelService: IModelService, resource: URI): TPromise<IModel> {
private resolveTextModelContent(modelService: IModelService, key: string): TPromise<IModel> {
const resource = URI.parse(key);
const model = modelService.getModel(resource);
if (model) {
// TODO@Joao this should never happen
return TPromise.as(model);
}
let loadingTextModel = this.loadingTextModels[resource.toString()];
if (!loadingTextModel) {
const providers = this.providers[resource.scheme] || [];
const factories = providers.map(p => () => p.provideTextContent(resource));
// make sure we have a provider this scheme
// the resource uses
const contentProviders = this.contentProviderRegistry[resource.scheme];
if (!contentProviders) {
return TPromise.wrapError(`No model with uri '${resource}' nor a resolver for the scheme '${resource.scheme}'.`);
return first(factories).then(uri => {
if (!uri) {
return TPromise.wrapError(`Could not resolve any model with uri '${resource}'.`);
}
// load the model-content from the provider and cache
// the loading such that we don't create the same model
// twice
this.loadingTextModels[resource.toString()] = loadingTextModel = new TPromise<IModel>((resolve, reject) => {
let result: IModel;
let lastError: any;
sequence(contentProviders.map(provider => {
return () => {
if (!result) {
const contentPromise = provider.provideTextContent(resource);
if (!contentPromise) {
return TPromise.wrapError<any>(`No resolver for the scheme '${resource.scheme}' found.`);
}
return contentPromise.then(value => {
result = value;
}, err => {
lastError = err;
});
}
};
})).then(() => {
if (!result && lastError) {
reject(lastError);
} else {
resolve(result);
}
}, reject);
}, function () {
// no cancellation when caching promises
});
// remove the cached promise 'cos the model is now known to the model service (see above)
loadingTextModel.then(() => delete this.loadingTextModels[resource.toString()], () => delete this.loadingTextModels[resource.toString()]);
return uri;
});
}
}
export class TextModelResolverService implements ITextModelResolverService {
_serviceBrand: any;
private promiseCache: { [uri: string]: TPromise<IReference<ITextEditorModel>> } = Object.create(null);
private resourceModelCollection: ResourceModelCollection;
constructor(
@ITextFileService private textFileService: ITextFileService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IInstantiationService private instantiationService: IInstantiationService
) {
this.resourceModelCollection = instantiationService.createInstance(ResourceModelCollection);
}
createModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
const uri = resource.toString();
let promise = this.promiseCache[uri];
if (promise) {
return promise;
}
return loadingTextModel;
promise = this.promiseCache[uri] = this._createModelReference(resource);
return always(promise, () => delete this.promiseCache[uri]);
}
private _createModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
// 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
if (resource.scheme === UntitledEditorInput.SCHEMA) {
return this.untitledEditorService.createOrGet(resource).resolve()
.then(model => new ImmortalReference(model));
}
const ref = this.resourceModelCollection.acquire(resource.toString());
return ref.object.then(model => ({ object: model, dispose: () => ref.dispose() }));
}
registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {
return this.resourceModelCollection.registerTextModelContentProvider(scheme, provider);
}
}
\ No newline at end of file
......@@ -78,35 +78,38 @@ suite('Workbench - TextModelResolverService', () => {
});
});
test('resolve file', function (done) {
test('resolve file', function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
model.load().then(() => {
accessor.textModelResolverServie.resolve(model.getResource()).then(model => {
const editorModel = model.textEditorModel as IModel;
return model.load().then(() => {
return accessor.textModelResolverServie.createModelReference(model.getResource()).then(ref => {
const model = ref.object;
const editorModel = model.textEditorModel;
assert.ok(editorModel);
assert.equal(editorModel.getValue(), 'Hello Html');
done();
ref.dispose();
});
});
});
test('resolve untitled', function (done) {
test('resolve untitled', function () {
const service = accessor.untitledEditorService;
const input = service.createOrGet();
input.resolve().then(() => {
accessor.textModelResolverServie.resolve(input.getResource()).then(model => {
const editorModel = model.textEditorModel as IModel;
return input.resolve().then(() => {
return accessor.textModelResolverServie.createModelReference(input.getResource()).then(ref => {
const model = ref.object;
const editorModel = model.textEditorModel;
assert.ok(editorModel);
ref.dispose();
input.dispose();
done();
});
});
});
// TODO: add reference tests!
});
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册