diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index b204985b86c..8c467a9db3c 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -1452,6 +1452,18 @@ export interface ITokenizedModel extends ITextModel { * @param position The position at which to look for a bracket. */ matchBracket(position:IPosition, inaccurateResultAcceptable?:boolean): IMatchBracketResult; + + /** + * No mode supports allowed on this model because it is simply too large. + * (even tokenization would cause too much memory pressure) + */ + isTooLargeForHavingAMode(): boolean; + + /** + * Only basic mode supports allowed on this model because it is simply too large. + * (tokenization is allowed and other basic supports) + */ + isTooLargeForHavingARichMode(): boolean; } /** diff --git a/src/vs/editor/common/model/mirrorModel.ts b/src/vs/editor/common/model/mirrorModel.ts index 2240bde5959..5f2bd193bf5 100644 --- a/src/vs/editor/common/model/mirrorModel.ts +++ b/src/vs/editor/common/model/mirrorModel.ts @@ -16,6 +16,11 @@ import {IResourceService} from 'vs/editor/common/services/resourceService'; import {URL} from 'vs/base/common/network'; import {disposeAll} from 'vs/base/common/lifecycle'; +export interface IMirrorModelEvents { + contentChanged: EditorCommon.IModelContentChangedEvent[]; + propertiesChanged: EditorCommon.IModelPropertiesChangedEvent; +} + export class AbstractMirrorModel extends TextModelWithTokens implements EditorCommon.IMirrorModel { _lineStarts:PrefixSumComputer; @@ -453,51 +458,43 @@ export class MirrorModel extends AbstractMirrorModel implements EditorCommon.IMi return false; } - public onEvents(events:{ type:string; }[]) : boolean { + public onEvents(events:IMirrorModelEvents) : boolean { if (this._isDisposed) { throw new Error('MirrorModel.onEvents: Model is disposed'); } - var changed = false; - for (var i = 0, len = events.length; i < len; i++) { - var e = events[i]; - switch (e.type) { - case EditorCommon.EventType.ModelContentChanged: - var contentChangedEvent = e; + if (events.propertiesChanged) { + this._extraProperties = events.propertiesChanged.properties; + } - // Force recreating of line starts - this._lineStarts = null; + let changed = false; + for (let i = 0, len = events.contentChanged.length; i < len; i++) { + let contentChangedEvent = events.contentChanged[i]; - this._setVersionId(contentChangedEvent.versionId); - switch (contentChangedEvent.changeType) { - case EditorCommon.EventType.ModelContentChangedFlush: - this._onLinesFlushed(contentChangedEvent); - changed = true; - break; + // Force recreating of line starts + this._lineStarts = null; - case EditorCommon.EventType.ModelContentChangedLinesDeleted: - this._onLinesDeleted(contentChangedEvent); - changed = true; - break; - - case EditorCommon.EventType.ModelContentChangedLinesInserted: - this._onLinesInserted(contentChangedEvent); - changed = true; - break; - - case EditorCommon.EventType.ModelContentChangedLineChanged: - this._onLineChanged(contentChangedEvent); - changed = true; - break; - } + this._setVersionId(contentChangedEvent.versionId); + switch (contentChangedEvent.changeType) { + case EditorCommon.EventType.ModelContentChangedFlush: + this._onLinesFlushed(contentChangedEvent); + changed = true; break; - case EditorCommon.EventType.ModelPropertiesChanged: - this._extraProperties = (e).properties; + case EditorCommon.EventType.ModelContentChangedLinesDeleted: + this._onLinesDeleted(contentChangedEvent); + changed = true; break; - default: - console.warn('Unknown model event: ' + e.type); + case EditorCommon.EventType.ModelContentChangedLinesInserted: + this._onLinesInserted(contentChangedEvent); + changed = true; + break; + + case EditorCommon.EventType.ModelContentChangedLineChanged: + this._onLineChanged(contentChangedEvent); + changed = true; + break; } } diff --git a/src/vs/editor/common/model/textModelWithTokens.ts b/src/vs/editor/common/model/textModelWithTokens.ts index 5fe1014ad81..68bbe2e023f 100644 --- a/src/vs/editor/common/model/textModelWithTokens.ts +++ b/src/vs/editor/common/model/textModelWithTokens.ts @@ -185,7 +185,7 @@ export class TextModelWithTokens extends TextModel implements EditorCommon.IToke private static MODE_TOKENIZATION_FAILED_MSG = nls.localize('mode.tokenizationSupportFailed', "The mode has failed while tokenizing the input."); private static MODEL_SYNC_LIMIT = 5 * 1024 * 1024; // 5 MB - private static MODEL_TOKENIZATION_LIMIT = 50 * 1024 * 1024; // 50 MB + private static MODEL_TOKENIZATION_LIMIT = 20 * 1024 * 1024; // 20 MB private _shouldAutoTokenize:boolean; private _mode: Modes.IMode; @@ -262,12 +262,20 @@ export class TextModelWithTokens extends TextModel implements EditorCommon.IToke super.dispose(); } + public isTooLargeForHavingAMode(): boolean { + return this._shouldDenyMode; + } + + public isTooLargeForHavingARichMode(): boolean { + return this._shouldSimplifyMode; + } + private _massageMode(mode: Modes.IMode): Modes.IMode { - if (this._shouldDenyMode) { + if (this.isTooLargeForHavingAMode()) { return new NullMode(); } - if (this._shouldSimplifyMode) { - return new SimplifiedMode(mode); + if (this.isTooLargeForHavingARichMode()) { + return mode.toSimplifiedMode(); } return mode; } @@ -856,27 +864,3 @@ export class TextModelWithTokens extends TextModel implements EditorCommon.IToke return BracketsHelper.matchBracket(this, this.validatePosition(position), inaccurateResultAcceptable); } } - -class SimplifiedMode implements Modes.IMode { - - tokenizationSupport: Modes.ITokenizationSupport; - electricCharacterSupport: Modes.IElectricCharacterSupport; - commentsSupport: Modes.ICommentsSupport; - characterPairSupport: Modes.ICharacterPairSupport; - tokenTypeClassificationSupport: Modes.ITokenTypeClassificationSupport; - - private _id: string; - - constructor(sourceMode: Modes.IMode) { - this._id = 'vs.editor.modes.simplifiedMode:' + sourceMode.getId(); - this.tokenizationSupport = sourceMode.tokenizationSupport; - this.electricCharacterSupport = sourceMode.electricCharacterSupport; - this.commentsSupport = sourceMode.commentsSupport; - this.characterPairSupport = sourceMode.characterPairSupport; - this.tokenTypeClassificationSupport = sourceMode.tokenTypeClassificationSupport; - } - - public getId(): string { - return this._id; - } -} \ No newline at end of file diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 0b901ead07e..45a94068f00 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -197,6 +197,11 @@ export interface IMode { getId(): string; + /** + * Return a mode "similar" to this one that strips any "smart" supports. + */ + toSimplifiedMode(): IMode; + addSupportChangedListener?(callback: (e: EditorCommon.IModeSupportChangedEvent) => void): IDisposable; /** diff --git a/src/vs/editor/common/modes/abstractMode.ts b/src/vs/editor/common/modes/abstractMode.ts index 211c8460392..6dc61e14b12 100644 --- a/src/vs/editor/common/modes/abstractMode.ts +++ b/src/vs/editor/common/modes/abstractMode.ts @@ -49,6 +49,7 @@ export class AbstractMode implements Modes.IMode { // adapters end private _eventEmitter = new EventEmitter(); + private _simplifiedMode: Modes.IMode; constructor( descriptor:Modes.IModeDescriptor, @@ -73,6 +74,7 @@ export class AbstractMode implements Modes.IMode { this.tokenTypeClassificationSupport = this; this._workerPiecePromise = null; + this._simplifiedMode = null; } public getId(): string { @@ -86,6 +88,13 @@ export class AbstractMode implements Modes.IMode { } } + public toSimplifiedMode(): Modes.IMode { + if (!this._simplifiedMode) { + this._simplifiedMode = new SimplifiedMode(this); + } + return this._simplifiedMode; + } + private _getOrCreateWorker(): TPromise { if (!this._workerPiecePromise) { var workerDescriptor: AsyncDescriptor2 = this._getWorkerDescriptor(); @@ -236,7 +245,84 @@ export class AbstractMode implements Modes.IMode { } } +class SimplifiedMode implements Modes.IMode { + tokenizationSupport: Modes.ITokenizationSupport; + electricCharacterSupport: Modes.IElectricCharacterSupport; + commentsSupport: Modes.ICommentsSupport; + characterPairSupport: Modes.ICharacterPairSupport; + tokenTypeClassificationSupport: Modes.ITokenTypeClassificationSupport; + onEnterSupport: Modes.IOnEnterSupport; + + private _sourceMode: Modes.IMode; + private _eventEmitter: EventEmitter; + private _id: string; + + constructor(sourceMode: Modes.IMode) { + this._sourceMode = sourceMode; + this._eventEmitter = new EventEmitter(); + this._id = 'vs.editor.modes.simplifiedMode:' + sourceMode.getId(); + this._assignSupports(); + + if (this._sourceMode.addSupportChangedListener) { + this._sourceMode.addSupportChangedListener((e) => { + if (e.tokenizationSupport || e.electricCharacterSupport || e.commentsSupport || e.characterPairSupport || e.tokenTypeClassificationSupport || e.onEnterSupport) { + this._assignSupports(); + let newEvent = SimplifiedMode._createModeSupportChangedEvent(e); + this._eventEmitter.emit('modeSupportChanged', newEvent); + } + }) + } + } + + public getId(): string { + return this._id; + } + + public toSimplifiedMode(): Modes.IMode { + return this; + } + + private _assignSupports(): void { + this.tokenizationSupport = this._sourceMode.tokenizationSupport; + this.electricCharacterSupport = this._sourceMode.electricCharacterSupport; + this.commentsSupport = this._sourceMode.commentsSupport; + this.characterPairSupport = this._sourceMode.characterPairSupport; + this.tokenTypeClassificationSupport = this._sourceMode.tokenTypeClassificationSupport; + this.onEnterSupport = this._sourceMode.onEnterSupport; + } + + private static _createModeSupportChangedEvent(originalModeEvent:EditorCommon.IModeSupportChangedEvent): EditorCommon.IModeSupportChangedEvent { + var event = { + codeLensSupport: false, + tokenizationSupport: originalModeEvent.tokenizationSupport, + occurrencesSupport:false, + declarationSupport:false, + typeDeclarationSupport:false, + navigateTypesSupport:false, + referenceSupport:false, + suggestSupport:false, + parameterHintsSupport:false, + extraInfoSupport:false, + outlineSupport:false, + logicalSelectionSupport:false, + formattingSupport:false, + inplaceReplaceSupport:false, + diffSupport:false, + dirtyDiffSupport:false, + emitOutputSupport:false, + linkSupport:false, + configSupport:false, + electricCharacterSupport: originalModeEvent.electricCharacterSupport, + commentsSupport: originalModeEvent.commentsSupport, + characterPairSupport: originalModeEvent.characterPairSupport, + tokenTypeClassificationSupport: originalModeEvent.tokenTypeClassificationSupport, + quickFixSupport:false, + onEnterSupport: originalModeEvent.onEnterSupport + }; + return event; + } +} export var isDigit:(character:string, base:number)=>boolean = (function () { diff --git a/src/vs/editor/common/modes/nullMode.ts b/src/vs/editor/common/modes/nullMode.ts index 4e6c80f0846..36e82233517 100644 --- a/src/vs/editor/common/modes/nullMode.ts +++ b/src/vs/editor/common/modes/nullMode.ts @@ -91,6 +91,10 @@ export class NullMode implements Modes.IMode { return NullMode.ID; } + public toSimplifiedMode(): Modes.IMode { + return this; + } + public getWordDefinition():RegExp { return NullMode.DEFAULT_WORD_REGEXP; } diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 28f98765657..880bc5f1e29 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -21,8 +21,6 @@ export interface IModelService { destroyModel(resource: URL): void; - removeModel(model: EditorCommon.IModel): void; - getModels(): EditorCommon.IModel[]; getModel(resource: URI): EditorCommon.IModel; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 3c985effdb7..ef7b5e1c5cf 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -7,7 +7,7 @@ import {IEmitterEvent} from 'vs/base/common/eventEmitter'; import {IModeService} from 'vs/editor/common/services/modeService'; import {IMarker, IMarkerService} from 'vs/platform/markers/common/markers'; -import {MirrorModel} from 'vs/editor/common/model/mirrorModel'; +import {IMirrorModelEvents, MirrorModel} from 'vs/editor/common/model/mirrorModel'; import {Range} from 'vs/editor/common/core/range'; import EditorCommon = require('vs/editor/common/editorCommon'); import Modes = require('vs/editor/common/modes'); @@ -21,7 +21,7 @@ import URI from 'vs/base/common/uri'; import {URL} from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import {EventProvider} from 'vs/base/common/eventProvider'; -import {IDisposable} from 'vs/base/common/lifecycle'; +import {IDisposable, disposeAll} from 'vs/base/common/lifecycle'; import {TPromise} from 'vs/base/common/winjs.base'; import Errors = require('vs/base/common/errors'); import {anonymize} from 'vs/platform/telemetry/common/telemetry'; @@ -35,53 +35,69 @@ export interface IRawModelData { modeId:string; } -class BoundModel implements IDisposable { +function MODEL_ID(resource:URI): string { + return resource.toString(); +} - model:EditorCommon.IModel; - toUnbind:Function; - private _decorationIds: string[]; +class ModelData implements IDisposable { + model: EditorCommon.IModel; + isSyncedToWorkers: boolean; - constructor(model:EditorCommon.IModel) { + private _markerDecorations: string[]; + private _modelEventsListener: IDisposable; + + constructor(model: EditorCommon.IModel, eventsHandler:(modelData:ModelData, events:IEmitterEvent[])=>void) { this.model = model; - this.toUnbind = null; + this.isSyncedToWorkers = false; + + this._markerDecorations = []; + this._modelEventsListener = model.addBulkListener2((events) => eventsHandler(this, events)); } public dispose(): void { - - this._decorationIds = this.model.deltaDecorations(this._decorationIds, []); + this._markerDecorations = this.model.deltaDecorations(this._markerDecorations, []); + this._modelEventsListener.dispose(); + this._modelEventsListener = null; this.model = null; - - if (this.toUnbind) { - this.toUnbind(); - this.toUnbind = null; - } } - public deltaMarkers(markers:IMarker[]):void { + public getModelId(): string { + return MODEL_ID(this.model.getAssociatedResource()); + } + + public acceptMarkerDecorations(newDecorations:EditorCommon.IModelDeltaDecoration[]): void { + this._markerDecorations = this.model.deltaDecorations(this._markerDecorations, newDecorations); + } +} + +class ModelMarkerHandler { + + public static setMarkers(modelData:ModelData, markers:IMarker[]):void { // Limit to the first 500 errors/warnings markers = markers.slice(0, 500); - var newModelDecorations = markers.map(marker => { - return { - range: this._createDecorationRange(marker), + let newModelDecorations:EditorCommon.IModelDeltaDecoration[] = markers.map((marker) => { + return { + range: this._createDecorationRange(modelData.model, marker), options: this._createDecorationOption(marker) }; }); - this._decorationIds = this.model.deltaDecorations(this._decorationIds, newModelDecorations); + + modelData.acceptMarkerDecorations(newModelDecorations); } - private _createDecorationRange(rawMarker: IMarker): EditorCommon.IRange { - var marker = this.model.validateRange(new Range(rawMarker.startLineNumber, rawMarker.startColumn, rawMarker.endLineNumber, rawMarker.endColumn)); - var ret: EditorCommon.IEditorRange = new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn); + private static _createDecorationRange(model:EditorCommon.IModel, rawMarker: IMarker): EditorCommon.IRange { + let marker = model.validateRange(new Range(rawMarker.startLineNumber, rawMarker.startColumn, rawMarker.endLineNumber, rawMarker.endColumn)); + let ret: EditorCommon.IEditorRange = new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn); if (ret.isEmpty()) { - var word = this.model.getWordAtPosition(ret.getStartPosition()); + let word = model.getWordAtPosition(ret.getStartPosition()); if (word) { ret.startColumn = word.startColumn; ret.endColumn = word.endColumn; } else { - var maxColumn = this.model.getLineLastNonWhitespaceColumn(marker.startLineNumber) || - this.model.getLineMaxColumn(marker.startLineNumber); + let maxColumn = model.getLineLastNonWhitespaceColumn(marker.startLineNumber) || + model.getLineMaxColumn(marker.startLineNumber); if (maxColumn === 1) { // empty line @@ -96,7 +112,7 @@ class BoundModel implements IDisposable { } } } else if (rawMarker.endColumn === Number.MAX_VALUE && rawMarker.startColumn === 1 && ret.startLineNumber === ret.endLineNumber) { - var minColumn = this.model.getLineFirstNonWhitespaceColumn(rawMarker.startLineNumber); + let minColumn = model.getLineFirstNonWhitespaceColumn(rawMarker.startLineNumber); if (minColumn < ret.endColumn) { ret.startColumn = minColumn; rawMarker.startColumn = minColumn; @@ -105,7 +121,7 @@ class BoundModel implements IDisposable { return ret; } - private _createDecorationOption(marker:IMarker): EditorCommon.IModelDecorationOptions { + private static _createDecorationOption(marker:IMarker): EditorCommon.IModelDecorationOptions { let className: string; let color: string; @@ -151,14 +167,9 @@ class BoundModel implements IDisposable { } } -export interface IModelsEvents { - [url:string]: any[]; -} - export class ModelServiceImpl implements IModelService { public serviceId = IModelService; - private _models: {[modelId:string]:BoundModel;}; private _markerService: IMarkerService; private _markerServiceSubscription: IDisposable; private _threadService: IThreadService; @@ -167,9 +178,11 @@ export class ModelServiceImpl implements IModelService { private _onModelAdded: EventSource<(model: EditorCommon.IModel) => void>; private _onModelRemoved: EventSource<(model: EditorCommon.IModel) => void>; private _onModelModeChanged: EventSource<(model: EditorCommon.IModel, oldModeId:string) => void>; - private _accumulatedModelEvents: IModelsEvents; - private _lastSentModelEventsTime: number; - private _sendModelEventsTimerId: number; + + /** + * All the models known in the system. + */ + private _models: {[modelId:string]:ModelData;}; constructor(threadService: IThreadService, markerService: IMarkerService) { this._threadService = threadService; @@ -185,137 +198,75 @@ export class ModelServiceImpl implements IModelService { if(this._markerService) { this._markerServiceSubscription = this._markerService.onMarkerChanged(this._handleMarkerChange, this); } - - this._accumulatedModelEvents = {}; - this._lastSentModelEventsTime = -1; - this._sendModelEventsTimerId = -1; } public dispose(): void { if(this._markerServiceSubscription) { this._markerServiceSubscription.dispose(); } - if (this._sendModelEventsTimerId !== -1) { - clearTimeout(this._sendModelEventsTimerId); - this._sendModelEventsTimerId = -1; - } - } - - private _sendModelEvents(url:URL, events:any[]): void { - let modelId = url.toString(); - this._accumulatedModelEvents[modelId] = this._accumulatedModelEvents[modelId] || []; - this._accumulatedModelEvents[modelId] = this._accumulatedModelEvents[modelId].concat(events); - - this._sendModelEventsNow(); - // this._scheduleSendModelEvents(); - } - - // private _scheduleSendModelEvents(): void { - // if (this._sendModelEventsTimerId !== -1) { - // // sending model events already scheduled - // return; - // } - - // let elapsed = Date.now() - this._lastSentModelEventsTime; - // if (elapsed >= 100) { - // // more than 100ms have passed since last model events have been sent => send events now - // this._sendModelEventsNow(); - // } else { - // this._sendModelEventsTimerId = setTimeout(() => { - // this._sendModelEventsTimerId = -1; - // this._sendModelEventsNow(); - // }, 100 - elapsed); - // } - // } - - private _sendModelEventsNow(): void { - this._lastSentModelEventsTime = Date.now(); - - let sendingEvents = this._accumulatedModelEvents; - this._accumulatedModelEvents = {}; - this._workerHelper.$onModelsEvents(sendingEvents); } private _handleMarkerChange(changedResources: URI[]): void { - - changedResources.forEach(resource => { - var boundModel = this._models[resource.toString()]; - if (!boundModel) { + changedResources.forEach((resource) => { + let modelId = MODEL_ID(resource); + let modelData = this._models[modelId]; + if (!modelData) { return; } - boundModel.deltaMarkers(this._markerService.read({ resource: resource, take: 500 })); + ModelMarkerHandler.setMarkers(modelData, this._markerService.read({ resource: resource, take: 500 })); }); } // --- begin IModelService - public createModel(value:string, modeOrPromise:TPromise|Modes.IMode, resource: URL): EditorCommon.IModel { - var model = new Model(value, modeOrPromise, resource); - this.addModel(model); - return model; - } - - public addModel(model:EditorCommon.IModel): void { - var modelId = model.getAssociatedResource().toString(); + private _createModelData(value:string, modeOrPromise:TPromise|Modes.IMode, resource: URL): ModelData { + // create & save the model + let model = new Model(value, modeOrPromise, resource); + let modelId = MODEL_ID(model.getAssociatedResource()); if (this._models[modelId]) { // There already exists a model with this id => this is a programmer error - throw new Error('BoundModels: Cannot add model ' + anonymize(modelId) + ' because it already exists!'); + throw new Error('ModelService: Cannot add model ' + anonymize(modelId) + ' because it already exists!'); } - var boundModel = new BoundModel(model); + let modelData = new ModelData(model, (modelData, events) => this._onModelEvents(modelData, events)); + this._models[modelId] = modelData; - boundModel.toUnbind = model.addBulkListener((events) => this._onModelEvents(modelId, events)); - if(this._markerService) { - boundModel.deltaMarkers(this._markerService.read({ resource: model.getAssociatedResource() })); - } - this._models[modelId] = boundModel; - - // Create model in workers - this._workerHelper.$createModel(ModelServiceImpl._getBoundModelData(model)); - this._onModelAdded.fire(model); + return modelData; } - public removeModel(model:EditorCommon.IModel): void { - var modelId = model.getAssociatedResource().toString(); - - if (this._accumulatedModelEvents[modelId]) { - delete this._accumulatedModelEvents[modelId]; - } - - if (!this._models[modelId]) { - // There is no model with this id => this is a programmer error - throw new Error('BoundModels: Cannot remove model ' + anonymize(modelId) + ' because it doesn\'t exist!'); - } - - // Dispose model in workers - this._workerHelper.$disposeModel(model.getAssociatedResource()); - // this._modelDispose(model.getAssociatedResource()); - this._models[modelId].dispose(); - - delete this._models[modelId]; + public createModel(value:string, modeOrPromise:TPromise|Modes.IMode, resource: URL): EditorCommon.IModel { + let modelData = this._createModelData(value, modeOrPromise, resource); + let modelId = modelData.getModelId(); + // handle markers (marker service => model) if (this._markerService) { - var markers = this._markerService.read({ resource: model.getAssociatedResource() }), - owners: { [o: string]: any } = Object.create(null); - - markers.forEach(marker => owners[marker.owner] = this); - Object.keys(owners).forEach(owner => this._markerService.changeOne(owner, model.getAssociatedResource(), [])); + ModelMarkerHandler.setMarkers(modelData, this._markerService.read({ resource: modelData.model.getAssociatedResource() })); } - this._onModelRemoved.fire(model); + if (!modelData.model.isTooLargeForHavingARichMode()) { + // send this model to the workers + modelData.isSyncedToWorkers = true; + this._workerHelper.$_acceptNewModel(ModelServiceImpl._getBoundModelData(modelData.model)); + } + + this._onModelAdded.fire(modelData.model); + + return modelData.model; } public destroyModel(resource: URL): void { - let model = this.getModel(resource); - if (model) { - model.destroy(); + // We need to support that not all models get disposed through this service (i.e. model.dispose() should work!) + let modelData = this._models[MODEL_ID(resource)]; + if (!modelData) { + return; } + modelData.model.dispose(); } public getModels(): EditorCommon.IModel[] { - var ret: EditorCommon.IModel[] = []; - for (var modelId in this._models) { + let ret: EditorCommon.IModel[] = []; + for (let modelId in this._models) { if (this._models.hasOwnProperty(modelId)) { ret.push(this._models[modelId].model); } @@ -324,11 +275,12 @@ export class ModelServiceImpl implements IModelService { } public getModel(resource: URL): EditorCommon.IModel { - var boundModel = this._models[resource.toString()]; - if (boundModel) { - return boundModel.model; + let modelId = MODEL_ID(resource); + let modelData = this._models[modelId]; + if (!modelData) { + return null; } - return null; + return modelData.model; } public get onModelAdded(): EventProvider<(model:EditorCommon.IModel)=>void> { @@ -345,6 +297,30 @@ export class ModelServiceImpl implements IModelService { // --- end IModelService + private _onModelDisposing(model:EditorCommon.IModel): void { + let modelId = MODEL_ID(model.getAssociatedResource()); + let modelData = this._models[modelId]; + + // TODO@Joh why are we removing markers here? + if (this._markerService) { + var markers = this._markerService.read({ resource: model.getAssociatedResource() }), + owners: { [o: string]: any } = Object.create(null); + + markers.forEach(marker => owners[marker.owner] = this); + Object.keys(owners).forEach(owner => this._markerService.changeOne(owner, model.getAssociatedResource(), [])); + } + + if (modelData.isSyncedToWorkers) { + // Dispose model in workers + this._workerHelper.$_acceptDidDisposeModel(model.getAssociatedResource()); + } + + delete this._models[modelId]; + modelData.dispose(); + + this._onModelRemoved.fire(model); + } + private static _getBoundModelData(model:EditorCommon.IModel): IRawModelData { return { url: model.getAssociatedResource(), @@ -355,67 +331,47 @@ export class ModelServiceImpl implements IModelService { }; } - private _onModelEvents(modelId:string, events:IEmitterEvent[]): void { + private _onModelEvents(modelData:ModelData, events:IEmitterEvent[]): void { + let eventsForWorkers: IMirrorModelEvents = { contentChanged: [], propertiesChanged: null }; - var resultingEvents:any[] = [], - changed = false, - i:number, - len:number; + for (let i = 0, len = events.length; i < len; i++) { + let e = events[i]; + let data = e.getData(); - for (i = 0, len = events.length; i < len; i++) { - var e = events[i]; - var data = e.getData(); switch (e.getType()) { - case EditorCommon.EventType.ModelDispose: - this.removeModel(this._models[modelId].model); + this._onModelDisposing(modelData.model); + // no more event processing return; case EditorCommon.EventType.ModelContentChanged: - switch (data.changeType) { - case EditorCommon.EventType.ModelContentChangedFlush: - resultingEvents.push(this._mixinProperties({ type: e.getType() }, data, ['changeType', 'detail', 'versionId'])); - break; - - case EditorCommon.EventType.ModelContentChangedLinesDeleted: - resultingEvents.push(this._mixinProperties({ type: e.getType() }, data, ['changeType', 'fromLineNumber', 'toLineNumber', 'versionId'])); - break; - - case EditorCommon.EventType.ModelContentChangedLinesInserted: - resultingEvents.push(this._mixinProperties({ type: e.getType() }, data, ['changeType', 'fromLineNumber', 'toLineNumber', 'detail', 'versionId'])); - break; - - case EditorCommon.EventType.ModelContentChangedLineChanged: - resultingEvents.push(this._mixinProperties({ type: e.getType() }, data, ['changeType', 'lineNumber', 'detail', 'versionId'])); - break; + if (modelData.isSyncedToWorkers) { + eventsForWorkers.contentChanged.push(data); } - changed = true; break; case EditorCommon.EventType.ModelPropertiesChanged: - resultingEvents.push(this._mixinProperties({ type: e.getType() }, data, ['properties'])); + if (modelData.isSyncedToWorkers) { + eventsForWorkers.propertiesChanged = data; + } break; case EditorCommon.EventType.ModelModeChanged: let modeChangedEvent = data; - this._workerHelper.$onModelModeChanged(modelId, modeChangedEvent.oldMode.getId(), modeChangedEvent.newMode.getId()); - this._onModelModeChanged.fire(this._models[modelId].model, modeChangedEvent.oldMode.getId()); + if (modelData.isSyncedToWorkers) { + // Forward mode change to all the workers + this._workerHelper.$_acceptDidChangeModelMode(modelData.getModelId(), modeChangedEvent.oldMode.getId(), modeChangedEvent.newMode.getId()); + } + this._onModelModeChanged.fire(modelData.model, modeChangedEvent.oldMode.getId()); break; } } - if (resultingEvents.length > 0) { + if (eventsForWorkers.contentChanged.length > 0 || eventsForWorkers.propertiesChanged) { // Forward events to all the workers - this._sendModelEvents(this._models[modelId].model.getAssociatedResource(), resultingEvents); + this._workerHelper.$_acceptModelEvents(modelData.getModelId(), eventsForWorkers); } } - - private _mixinProperties(dst:any, src:any, properties:string[]): any { - for (var i = 0; i < properties.length; i++) { - dst[properties[i]] = src[properties[i]]; - } - return dst; - } } @Remotable.WorkerContext('ModelServiceWorkerHelper', ThreadAffinity.All) @@ -432,9 +388,9 @@ export class ModelServiceWorkerHelper { this._modeService = modeService; } - public $createModel(data:IRawModelData): TPromise { + public $_acceptNewModel(data:IRawModelData): TPromise { // Create & insert the mirror model eagerly in the resource service - var mirrorModel = new MirrorModel(this._resourceService, data.versionId, data.value, null, data.url, data.properties); + let mirrorModel = new MirrorModel(this._resourceService, data.versionId, data.value, null, data.url, data.properties); this._resourceService.insert(mirrorModel.getAssociatedResource(), mirrorModel); // Block worker execution until the mode is instantiated @@ -452,8 +408,8 @@ export class ModelServiceWorkerHelper { }); } - public $onModelModeChanged(modelId:string, oldModeId:string, newModeId:string): TPromise { - var mirrorModel = this._resourceService.get(URI.parse(modelId)); + public $_acceptDidChangeModelMode(modelId:string, oldModeId:string, newModeId:string): TPromise { + let mirrorModel = this._resourceService.get(URI.parse(modelId)); // Block worker execution until the mode is instantiated return this._modeService.getOrCreateMode(newModeId).then((mode) => { @@ -470,31 +426,23 @@ export class ModelServiceWorkerHelper { }); } - public $disposeModel(url:URL): void { - var model = this._resourceService.get(url); + public $_acceptDidDisposeModel(url:URL): void { + let model = this._resourceService.get(url); this._resourceService.remove(url); if (model) { model.dispose(); } } - public $onModelsEvents(events:IModelsEvents): void { - let missingModels: string[] = []; - Object.keys(events).forEach((strURL:string) => { - var model = this._resourceService.get(new URL(strURL)); - if (!model) { - missingModels.push(strURL); - return; - } - try { - model.onEvents(events[strURL]); - } catch (err) { - Errors.onUnexpectedError(err); - } - }); - - if (missingModels.length > 0) { - throw new Error('Received model events for missing models ' + missingModels.map(anonymize).join(' AND ')); + public $_acceptModelEvents(modelId: string, events:IMirrorModelEvents): void { + let model = this._resourceService.get(new URL(modelId)); + if (!model) { + throw new Error('Received model events for missing model ' + anonymize(modelId)); + } + try { + model.onEvents(events); + } catch (err) { + Errors.onUnexpectedError(err); } } } diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 75c6088e86f..c1f09732675 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -13,7 +13,7 @@ import {TextModel} from 'vs/editor/common/model/textModel'; import {LineMarker, TextModelWithMarkers} from 'vs/editor/common/model/textModelWithMarkers'; import {ILineMarker} from 'vs/editor/common/model/modelLine'; import {PluginHostDocument} from 'vs/workbench/api/common/pluginHostDocuments'; -import {MirrorModel} from 'vs/editor/common/model/mirrorModel'; +import {MirrorModel, IMirrorModelEvents} from 'vs/editor/common/model/mirrorModel'; suite('EditorModel - EditableTextModel._getInverseEdits', () => { @@ -1194,8 +1194,11 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { console.warn('Model version id did not advance between edits (1)'); } mirrorModel1PrevVersionId = versionId; - (e).type = EditorCommon.EventType.ModelContentChanged; - mirrorModel1.onEvents([e]); + let mirrorModelEvents:IMirrorModelEvents = { + propertiesChanged: null, + contentChanged: [e] + }; + mirrorModel1.onEvents(mirrorModelEvents); }); model.addListener(EditorCommon.EventType.ModelContentChanged2, (e:EditorCommon.IModelContentChangedEvent2) => { diff --git a/src/vs/editor/test/common/model/mirrorModel.test.ts b/src/vs/editor/test/common/model/mirrorModel.test.ts index 60b60229cd2..bbc608fa292 100644 --- a/src/vs/editor/test/common/model/mirrorModel.test.ts +++ b/src/vs/editor/test/common/model/mirrorModel.test.ts @@ -21,6 +21,57 @@ function equalRange(left, right) { assert.ok(true, 'ranges'); }; +function contentChangedFlushEvent(detail: EditorCommon.IRawText): EditorCommon.IModelContentChangedFlushEvent { + return { + changeType: EditorCommon.EventType.ModelContentChangedFlush, + isRedoing: false, + isUndoing: false, + versionId: 0, + detail: detail + }; +} + +function contentChangedLinesDeletedEvent(fromLineNumber: number, toLineNumber: number): EditorCommon.IModelContentChangedLinesDeletedEvent { + return { + changeType: EditorCommon.EventType.ModelContentChangedLinesDeleted, + isRedoing: false, + isUndoing: false, + versionId: 0, + fromLineNumber: fromLineNumber, + toLineNumber: toLineNumber + }; +} + +function contentChangedLinesInsertedEvent(fromLineNumber: number, toLineNumber: number, detail: string): EditorCommon.IModelContentChangedLinesInsertedEvent { + return { + changeType: EditorCommon.EventType.ModelContentChangedLinesInserted, + isRedoing: false, + isUndoing: false, + versionId: 0, + fromLineNumber: fromLineNumber, + toLineNumber: toLineNumber, + detail: detail + }; +} + +function contentChangedLineChanged(lineNumber: number, detail: string): EditorCommon.IModelContentChangedLineChangedEvent { + return { + changeType: EditorCommon.EventType.ModelContentChangedLineChanged, + isRedoing: false, + isUndoing: false, + versionId: 0, + lineNumber: lineNumber, + detail: detail + }; +} + +function mirrorModelEvents(contentChanged:EditorCommon.IModelContentChangedEvent[]): MirrorModel.IMirrorModelEvents { + return { + propertiesChanged: null, + contentChanged: contentChanged + }; +} + suite('Editor Model - MirrorModel', () => { var mirrorModel:MirrorModel.MirrorModel; @@ -46,19 +97,16 @@ suite('Editor Model - MirrorModel', () => { assert.equal(mirrorModel.getLineStart(2), 6); assert.equal(mirrorModel.getLineStart(3), 12); - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedFlush, - detail: { - lines: [ - 'foo', - 'bar' - ], - BOM: '', - EOL: '\n' - } - }; - mirrorModel.onEvents([e]); + mirrorModel.onEvents(mirrorModelEvents([contentChangedFlushEvent({ + length: -1, + lines: [ + 'foo', + 'bar' + ], + BOM: '', + EOL: '\n' + })])); + assert.equal(mirrorModel.getLineStart(1), 0); assert.equal(mirrorModel.getLineStart(2), 4); }); @@ -170,14 +218,9 @@ suite('Editor Model - MirrorModel Eventing', () => { }); test('delete single line', () => { - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedLinesDeleted, - fromLineNumber: 3, - toLineNumber: 3 - }; assert.equal(mirrorModel.getLineContent(3), 'line three'); - mirrorModel.onEvents([e]); + + mirrorModel.onEvents(mirrorModelEvents([contentChangedLinesDeletedEvent(3, 3)])); assert.equal(mirrorModel.getLineContent(3), 'line four'); var words = mirrorModel.getAllWords(); @@ -191,13 +234,8 @@ suite('Editor Model - MirrorModel Eventing', () => { }); test('delete multiple lines', () => { - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedLinesDeleted, - fromLineNumber: 1, - toLineNumber: 2 - }; - mirrorModel.onEvents([e]); + mirrorModel.onEvents(mirrorModelEvents([contentChangedLinesDeletedEvent(1, 2)])); + assert.equal(mirrorModel.getLineContent(1), 'line three'); assert.equal(mirrorModel.getLineContent(2), 'line four'); var words = mirrorModel.getAllWords(); @@ -220,13 +258,8 @@ suite('Editor Model - MirrorModel Eventing', () => { }); test('delete all lines', () => { - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedLinesDeleted, - fromLineNumber: 1, - toLineNumber: 4 - }; - mirrorModel.onEvents([e]); + mirrorModel.onEvents(mirrorModelEvents([contentChangedLinesDeletedEvent(1, 4)])); + var words = mirrorModel.getAllWords(); assert.equal(words.length, 0); var wordsWithRanges = mirrorModel.getAllWordsWithRange(); @@ -234,16 +267,10 @@ suite('Editor Model - MirrorModel Eventing', () => { }); test('add single lines', () => { - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedLinesInserted, - fromLineNumber: 1, - toLineNumber: 1, - detail: 'foo bar\nbar foo' - }; - assert.equal(mirrorModel.getLineContent(1), 'line one'); - mirrorModel.onEvents([e]); + + mirrorModel.onEvents(mirrorModelEvents([contentChangedLinesInsertedEvent(1, 1, 'foo bar\nbar foo')])); + assert.equal(mirrorModel.getLineContent(1), 'foo bar'); assert.equal(mirrorModel.getLineContent(2), 'line one'); @@ -257,16 +284,10 @@ suite('Editor Model - MirrorModel Eventing', () => { test('add multiple lines', () => { - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedLinesInserted, - fromLineNumber: 1, - toLineNumber: 2, - detail: 'foo bar\nbar foo' - }; - assert.equal(mirrorModel.getLineContent(1), 'line one'); - mirrorModel.onEvents([e]); + + mirrorModel.onEvents(mirrorModelEvents([contentChangedLinesInsertedEvent(1, 2, 'foo bar\nbar foo')])); + assert.equal(mirrorModel.getLineContent(1), 'foo bar'); assert.equal(mirrorModel.getLineContent(2), 'bar foo'); assert.equal(mirrorModel.getLineContent(3), 'line one'); @@ -284,16 +305,11 @@ suite('Editor Model - MirrorModel Eventing', () => { }); test('change line', () => { - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedLineChanged, - lineNumber: 1, - detail: 'foobar' - }; assert.equal(mirrorModel.getLineContent(1), 'line one'); var wordsWithRanges = mirrorModel.getAllWordsWithRange(); assert.equal(wordsWithRanges.length, 8); - mirrorModel.onEvents([e]); + + mirrorModel.onEvents(mirrorModelEvents([contentChangedLineChanged(1, 'foobar')])); assert.equal(mirrorModel.getLineContent(1), 'foobar'); wordsWithRanges = mirrorModel.getAllWordsWithRange(); @@ -305,19 +321,17 @@ suite('Editor Model - MirrorModel Eventing', () => { test('flush model', () => { assert.equal(mirrorModel.getLineContent(1), 'line one'); assert.equal(mirrorModel.getLineContent(2), 'line two'); - var e = { - type: EditorCommon.EventType.ModelContentChanged, - changeType: EditorCommon.EventType.ModelContentChangedFlush, - detail: { - lines: [ - 'foo', - 'bar' - ], - BOM: '', - EOL: '\n' - } - }; - mirrorModel.onEvents([e]); + + mirrorModel.onEvents(mirrorModelEvents([contentChangedFlushEvent({ + length: -1, + lines: [ + 'foo', + 'bar' + ], + BOM: '', + EOL: '\n' + })])); + assert.equal(mirrorModel.getLineContent(1), 'foo'); assert.equal(mirrorModel.getLineContent(2), 'bar'); var wordsWithRanges = mirrorModel.getAllWordsWithRange(); diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index 525767021ac..7451b1737cc 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -84,4 +84,8 @@ class Mode implements modes.IMode { public getId(): string { return "testMode"; } + + public toSimplifiedMode(): modes.IMode { + return this; + } } diff --git a/src/vs/editor/test/common/modes/tokenization.test.ts b/src/vs/editor/test/common/modes/tokenization.test.ts index b1416d43323..3854a550cef 100644 --- a/src/vs/editor/test/common/modes/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/tokenization.test.ts @@ -44,6 +44,10 @@ export class Mode implements modes.IMode { public getId(): string { return "testMode"; } + + public toSimplifiedMode(): modes.IMode { + return this; + } } @@ -114,6 +118,10 @@ export class SwitchingMode implements modes.IMode { return this._id; } + public toSimplifiedMode(): modes.IMode { + return this; + } + public addSupportChangedListener(callback: (e: EditorCommon.IModeSupportChangedEvent) => void): IDisposable { return EmptyDisposable; } diff --git a/src/vs/editor/test/common/modesTestUtils.ts b/src/vs/editor/test/common/modesTestUtils.ts index ef974978919..cdb3a85aa35 100644 --- a/src/vs/editor/test/common/modesTestUtils.ts +++ b/src/vs/editor/test/common/modesTestUtils.ts @@ -7,6 +7,25 @@ import modes = require('vs/editor/common/modes'); import {Arrays} from 'vs/editor/common/core/arrays'; +class SimpleTokenTypeClassificationMode implements modes.IMode { + + private _id:string; + public tokenTypeClassificationSupport: modes.ITokenTypeClassificationSupport; + + constructor(id:string, tokenTypeClassificationSupport: modes.ITokenTypeClassificationSupport) { + this._id = id; + this.tokenTypeClassificationSupport = tokenTypeClassificationSupport; + } + + public getId(): string { + return this._id; + } + + public toSimplifiedMode(): modes.IMode { + return this; + } +} + export function createMockMode(id:string, wordRegExp:RegExp = null):modes.IMode { var tokenTypeClassificationSupport: modes.ITokenTypeClassificationSupport; if (wordRegExp) { @@ -14,10 +33,7 @@ export function createMockMode(id:string, wordRegExp:RegExp = null):modes.IMode getWordDefinition: () => wordRegExp }; } - return { - getId: () => id, - tokenTypeClassificationSupport: tokenTypeClassificationSupport - }; + return new SimpleTokenTypeClassificationMode(id, tokenTypeClassificationSupport); } export interface TokenText { diff --git a/src/vs/editor/test/common/modesUtil.ts b/src/vs/editor/test/common/modesUtil.ts index 4726b0ae1b6..5ea5bb37704 100644 --- a/src/vs/editor/test/common/modesUtil.ts +++ b/src/vs/editor/test/common/modesUtil.ts @@ -87,11 +87,28 @@ export interface IOnEnterAsserter { indentsOutdents(oneLineAboveText:string, beforeText:string, afterText:string): void; } +class SimpleMode implements modes.IMode { + + private _id:string; + + constructor(id:string) { + this._id = id; + } + + public getId(): string { + return this._id; + } + + public toSimplifiedMode(): modes.IMode { + return this; + } +} + export function createOnEnterAsserter(modeId:string, onEnterSupport: modes.IOnEnterSupport): IOnEnterAsserter { var assertOne = (oneLineAboveText:string, beforeText:string, afterText:string, expected: modes.IndentAction) => { var model = new Model( [ oneLineAboveText, beforeText + afterText ].join('\n'), - { getId: () => modeId } + new SimpleMode(modeId) ); var actual = onEnterSupport.onEnter(model, { lineNumber: 2, column: beforeText.length + 1 }); if (expected === modes.IndentAction.None) { @@ -130,9 +147,7 @@ export function executeMonarchTokenizationTests(name:string, language:monarchTyp var modeService = servicesUtil.createMockModeService(); - var tokenizationSupport = monarchLexer.createTokenizationSupport(modeService, { - getId: () => 'mock.mode' - }, lexer); + var tokenizationSupport = monarchLexer.createTokenizationSupport(modeService, new SimpleMode('mock.mode'), lexer); executeTests(tokenizationSupport, tests); } diff --git a/src/vs/editor/test/common/testModes.ts b/src/vs/editor/test/common/testModes.ts index af9503e0d68..e9c097da553 100644 --- a/src/vs/editor/test/common/testModes.ts +++ b/src/vs/editor/test/common/testModes.ts @@ -76,6 +76,10 @@ export class TestingMode implements modes.IMode { public getId():string { return 'testing'; } + + public toSimplifiedMode(): modes.IMode { + return this; + } } export class AbstractIndentingMode extends TestingMode { diff --git a/src/vs/languages/typescript.workbench/test/common/projectResolver.test.ts b/src/vs/languages/typescript.workbench/test/common/projectResolver.test.ts index fc695e0f373..1d357b685a5 100644 --- a/src/vs/languages/typescript.workbench/test/common/projectResolver.test.ts +++ b/src/vs/languages/typescript.workbench/test/common/projectResolver.test.ts @@ -62,7 +62,6 @@ function createModelService(): IModelService { onModelAdded: undefined, onModelRemoved: undefined, onModelModeChanged: undefined, - removeModel: undefined, destroyModel: undefined } }