diff --git a/src/vs/editor/common/model/mirrorModel2.ts b/src/vs/editor/common/model/mirrorModel2.ts new file mode 100644 index 00000000000..ae70ac947ab --- /dev/null +++ b/src/vs/editor/common/model/mirrorModel2.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import URI from 'vs/base/common/uri'; +import {IModelContentChangedEvent2, IRange, IPosition} from 'vs/editor/common/editorCommon'; +import {PrefixSumComputer} from 'vs/editor/common/viewModel/prefixSumComputer'; + +export class MirrorModel2 { + + protected _uri: URI; + protected _lines: string[]; + protected _eol: string; + protected _versionId: number; + protected _lineStarts: PrefixSumComputer; + + constructor(uri: URI, lines: string[], eol: string, versionId: number) { + this._uri = uri; + this._lines = lines; + this._eol = eol; + this._versionId = versionId; + } + + dispose(): void { + this._lines.length = 0; + } + + get version(): number { + return this._versionId; + } + + getText(): string { + return this._lines.join(this._eol); + } + + onEvents(events: IModelContentChangedEvent2[]): void { + // Update my lines + let lastVersionId = -1; + for (let i = 0, len = events.length; i < len; i++) { + let e = events[i]; + + this._acceptDeleteRange(e.range); + this._acceptInsertText({ + lineNumber: e.range.startLineNumber, + column: e.range.startColumn + }, e.text); + lastVersionId = Math.max(lastVersionId, e.versionId); + } + if (lastVersionId !== -1) { + this._versionId = lastVersionId; + } + } + + protected _ensureLineStarts(): void { + if (!this._lineStarts) { + const lineStartValues:number[] = []; + const eolLength = this._eol.length; + for (let i = 0, len = this._lines.length; i < len; i++) { + lineStartValues.push(this._lines[i].length + eolLength); + } + this._lineStarts = new PrefixSumComputer(lineStartValues); + } + } + + /** + * All changes to a line's text go through this method + */ + private _setLineText(lineIndex:number, newValue:string): void { + this._lines[lineIndex] = newValue; + if (this._lineStarts) { + // update prefix sum + this._lineStarts.changeValue(lineIndex, this._lines[lineIndex].length + this._eol.length); + } + } + + private _acceptDeleteRange(range: IRange): void { + + if (range.startLineNumber === range.endLineNumber) { + if (range.startColumn === range.endColumn) { + // Nothing to delete + return; + } + // Delete text on the affected line + this._setLineText(range.startLineNumber - 1, + this._lines[range.startLineNumber - 1].substring(0, range.startColumn - 1) + + this._lines[range.startLineNumber - 1].substring(range.endColumn - 1) + ); + return; + } + + // Take remaining text on last line and append it to remaining text on first line + this._setLineText(range.startLineNumber - 1, + this._lines[range.startLineNumber - 1].substring(0, range.startColumn - 1) + + this._lines[range.endLineNumber - 1].substring(range.endColumn - 1) + ); + + // Delete middle lines + this._lines.splice(range.startLineNumber, range.endLineNumber - range.startLineNumber); + if (this._lineStarts) { + // update prefix sum + this._lineStarts.removeValues(range.startLineNumber, range.endLineNumber - range.startLineNumber); + } + } + + private _acceptInsertText(position: IPosition, insertText:string): void { + if (insertText.length === 0) { + // Nothing to insert + return; + } + let insertLines = insertText.split(/\r\n|\r|\n/); + if (insertLines.length === 1) { + // Inserting text on one line + this._setLineText(position.lineNumber - 1, + this._lines[position.lineNumber - 1].substring(0, position.column - 1) + + insertLines[0] + + this._lines[position.lineNumber - 1].substring(position.column - 1) + ); + return; + } + + // Append overflowing text from first line to the end of text to insert + insertLines[insertLines.length - 1] += this._lines[position.lineNumber - 1].substring(position.column - 1); + + // Delete overflowing text from first line and insert text on first line + this._setLineText(position.lineNumber - 1, + this._lines[position.lineNumber - 1].substring(0, position.column - 1) + + insertLines[0] + ); + + // Insert new lines & store lengths + let newLengths:number[] = new Array(insertLines.length - 1); + for (let i = 1; i < insertLines.length; i++) { + this._lines.splice(position.lineNumber + i - 1, 0, insertLines[i]); + newLengths[i - 1] = insertLines[i].length + this._eol.length; + } + + if (this._lineStarts) { + // update prefix sum + this._lineStarts.insertValues(position.lineNumber, newLengths); + } + } +} diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 7563198ff4e..ad3c7bd69d1 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -12,7 +12,7 @@ import {EditableTextModel, IValidatedEditOperation} from 'vs/editor/common/model 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 {ExtHostDocument} from 'vs/workbench/api/common/extHostDocuments'; +import {MirrorModel2} from 'vs/editor/common/model/mirrorModel2'; import {MirrorModel, IMirrorModelEvents} from 'vs/editor/common/model/mirrorModel'; suite('EditorModel - EditableTextModel._getInverseEdits', () => { @@ -1185,7 +1185,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { var mirrorModel1 = new MirrorModel(null, model.getVersionId(), model.toRawText(), null); var mirrorModel1PrevVersionId = model.getVersionId(); - var mirrorModel2 = new ExtHostDocument(null, null, model.toRawText().lines, model.toRawText().EOL, null, model.getVersionId(), false); + var mirrorModel2 = new MirrorModel2(null, model.toRawText().lines, model.toRawText().EOL, model.getVersionId()); var mirrorModel2PrevVersionId = model.getVersionId(); model.addListener(EditorCommon.EventType.ModelContentChanged, (e:EditorCommon.IModelContentChangedEvent) => { @@ -1207,7 +1207,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { console.warn('Model version id did not advance between edits (2)'); } mirrorModel2PrevVersionId = versionId; - mirrorModel2._acceptEvents([e]); + mirrorModel2.onEvents([e]); }); var assertMirrorModels = () => { diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index b6b9be12ac3..5cc7830b172 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -7,8 +7,9 @@ import {toErrorMessage} from 'vs/base/common/errors'; import {IEmitterEvent} from 'vs/base/common/eventEmitter'; import {IModelService} from 'vs/editor/common/services/modelService'; -import {PrefixSumComputer, IPrefixSumIndexOfResult} from 'vs/editor/common/viewModel/prefixSumComputer'; import * as EditorCommon from 'vs/editor/common/editorCommon'; +import {IPrefixSumIndexOfResult} from 'vs/editor/common/viewModel/prefixSumComputer'; +import {MirrorModel2} from 'vs/editor/common/model/mirrorModel2'; import {Remotable, IThreadService} from 'vs/platform/thread/common/thread'; import Event, {Emitter} from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; @@ -53,20 +54,20 @@ export function getWordDefinitionFor(modeId:string):RegExp { @Remotable.PluginHostContext('ExtHostModelService') export class ExtHostModelService { - private _onDidAddDocumentEventEmitter: Emitter; - public onDidAddDocument: Event; + private _onDidAddDocumentEventEmitter: Emitter; + public onDidAddDocument: Event; - private _onDidRemoveDocumentEventEmitter: Emitter; - public onDidRemoveDocument: Event; + private _onDidRemoveDocumentEventEmitter: Emitter; + public onDidRemoveDocument: Event; private _onDidChangeDocumentEventEmitter: Emitter; public onDidChangeDocument: Event; - private _onDidSaveDocumentEventEmitter: Emitter; - public onDidSaveDocument: Event; + private _onDidSaveDocumentEventEmitter: Emitter; + public onDidSaveDocument: Event; - private _documents: { [modelUri: string]: ExtHostDocument; }; - private _loadingDocuments: { [modelUri: string]: TPromise }; + private _documents: { [modelUri: string]: ExtHostDocumentData; }; + private _loadingDocuments: { [modelUri: string]: TPromise }; private _documentContentProviders: { [scheme: string]: vscode.TextDocumentContentProvider }; private _proxy: MainThreadDocuments; @@ -74,16 +75,16 @@ export class ExtHostModelService { constructor(@IThreadService threadService: IThreadService) { this._proxy = threadService.getRemotable(MainThreadDocuments); - this._onDidAddDocumentEventEmitter = new Emitter(); + this._onDidAddDocumentEventEmitter = new Emitter(); this.onDidAddDocument = this._onDidAddDocumentEventEmitter.event; - this._onDidRemoveDocumentEventEmitter = new Emitter(); + this._onDidRemoveDocumentEventEmitter = new Emitter(); this.onDidRemoveDocument = this._onDidRemoveDocumentEventEmitter.event; this._onDidChangeDocumentEventEmitter = new Emitter(); this.onDidChangeDocument = this._onDidChangeDocumentEventEmitter.event; - this._onDidSaveDocumentEventEmitter = new Emitter(); + this._onDidSaveDocumentEventEmitter = new Emitter(); this.onDidSaveDocument = this._onDidSaveDocumentEventEmitter.event; this._documents = Object.create(null); @@ -91,15 +92,15 @@ export class ExtHostModelService { this._documentContentProviders = Object.create(null); } - public getDocuments(): BaseTextDocument[] { - let r: BaseTextDocument[] = []; + public getDocuments(): vscode.TextDocument[] { + let r: vscode.TextDocument[] = []; for (let key in this._documents) { r.push(this._documents[key]); } return r; } - public getDocument(resource: vscode.Uri): BaseTextDocument { + public getDocument(resource: vscode.Uri): vscode.TextDocument { if (!resource) { return null; } @@ -158,7 +159,7 @@ export class ExtHostModelService { } public _acceptModelAdd(data:IModelAddedData): void { - let document = new ExtHostDocument(this._proxy, data.url, data.value.lines, data.value.EOL, data.modeId, data.versionId, data.isDirty); + let document = new ExtHostDocumentData(this._proxy, data.url, data.value.lines, data.value.EOL, data.modeId, data.versionId, data.isDirty); let key = document.uri.toString(); if (this._documents[key]) { throw new Error('Document `' + key + '` already exists.'); @@ -206,7 +207,7 @@ export class ExtHostModelService { public _acceptModelChanged(url: URI, events: EditorCommon.IModelContentChangedEvent2[]): void { let document = this._documents[url.toString()]; - document._acceptEvents(events); + document.onEvents(events); this._onDidChangeDocumentEventEmitter.fire({ document: document, contentChanges: events.map((e) => { @@ -220,30 +221,27 @@ export class ExtHostModelService { } } -export class BaseTextDocument implements vscode.TextDocument { - protected _uri: URI; - protected _lines: string[]; - protected _eol: string; - protected _languageId: string; - protected _versionId: number; - protected _isDirty: boolean; - protected _textLines: vscode.TextLine[]; - protected _lineStarts: PrefixSumComputer; +export class ExtHostDocumentData extends MirrorModel2 implements vscode.TextDocument { - constructor(uri: URI, lines: string[], eol: string, languageId: string, versionId: number, isDirty:boolean) { - this._uri = uri; - this._lines = lines; - this._textLines = []; - this._eol = eol; + private _proxy: MainThreadDocuments; + private _languageId: string; + private _isDirty: boolean; + private _textLines: vscode.TextLine[]; + + constructor(proxy: MainThreadDocuments, uri: URI, lines: string[], eol: string, + languageId: string, versionId: number, isDirty: boolean) { + + super(uri, lines, eol, versionId); + this._proxy = proxy; this._languageId = languageId; - this._versionId = versionId; this._isDirty = isDirty; + this._textLines = []; } dispose(): void { - this._lines.length = 0; this._textLines.length = 0; this._isDirty = false; + super.dispose(); } get uri(): URI { @@ -271,14 +269,22 @@ export class BaseTextDocument implements vscode.TextDocument { } save(): Thenable { - return Promise.reject('Not implemented'); + return this._proxy._trySaveDocument(this._uri); + } + + _acceptLanguageId(newLanguageId:string): void { + this._languageId = newLanguageId; + } + + _acceptIsDirty(isDirty:boolean): void { + this._isDirty = isDirty; } getText(range?: Range): string { if (range) { return this._getTextInRange(range); } else { - return this._lines.join(this._eol); + return super.getText(); } } @@ -367,17 +373,6 @@ export class BaseTextDocument implements vscode.TextDocument { return new Position(out.index, Math.min(out.remainder, lineLength)); } - private _ensureLineStarts(): void { - if (!this._lineStarts) { - const lineStartValues:number[] = []; - const eolLength = this._eol.length; - for (let i = 0, len = this._lines.length; i < len; i++) { - lineStartValues.push(this._lines[i].length + eolLength); - } - this._lineStarts = new PrefixSumComputer(lineStartValues); - } - } - // ---- range math validateRange(range:Range): Range { @@ -445,125 +440,6 @@ export class BaseTextDocument implements vscode.TextDocument { } } -export class ExtHostDocument extends BaseTextDocument { - - private _proxy: MainThreadDocuments; - - constructor(proxy: MainThreadDocuments, uri: URI, lines: string[], - eol: string, languageId: string, versionId: number, isDirty:boolean) { - super(uri, lines, eol, languageId, versionId, isDirty); - this._proxy = proxy; - } - - save(): Thenable { - return this._proxy._trySaveDocument(this._uri); - } - - _acceptLanguageId(newLanguageId:string): void { - this._languageId = newLanguageId; - } - - _acceptIsDirty(isDirty:boolean): void { - this._isDirty = isDirty; - } - - _acceptEvents(events: EditorCommon.IModelContentChangedEvent2[]): void { - // Update my lines - let lastVersionId = -1; - for (let i = 0, len = events.length; i < len; i++) { - let e = events[i]; - - this._acceptDeleteRange(e.range); - this._acceptInsertText({ - lineNumber: e.range.startLineNumber, - column: e.range.startColumn - }, e.text); - lastVersionId = Math.max(lastVersionId, e.versionId); - } - if (lastVersionId !== -1) { - this._versionId = lastVersionId; - } - } - - /** - * All changes to a line's text go through this method - */ - private _setLineText(lineIndex:number, newValue:string): void { - this._lines[lineIndex] = newValue; - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(lineIndex, this._lines[lineIndex].length + this._eol.length); - } - } - - private _acceptDeleteRange(range: EditorCommon.IRange): void { - - if (range.startLineNumber === range.endLineNumber) { - if (range.startColumn === range.endColumn) { - // Nothing to delete - return; - } - // Delete text on the affected line - this._setLineText(range.startLineNumber - 1, - this._lines[range.startLineNumber - 1].substring(0, range.startColumn - 1) - + this._lines[range.startLineNumber - 1].substring(range.endColumn - 1) - ); - return; - } - - // Take remaining text on last line and append it to remaining text on first line - this._setLineText(range.startLineNumber - 1, - this._lines[range.startLineNumber - 1].substring(0, range.startColumn - 1) - + this._lines[range.endLineNumber - 1].substring(range.endColumn - 1) - ); - - // Delete middle lines - this._lines.splice(range.startLineNumber, range.endLineNumber - range.startLineNumber); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.removeValues(range.startLineNumber, range.endLineNumber - range.startLineNumber); - } - } - - private _acceptInsertText(position: EditorCommon.IPosition, insertText:string): void { - if (insertText.length === 0) { - // Nothing to insert - return; - } - let insertLines = insertText.split(/\r\n|\r|\n/); - if (insertLines.length === 1) { - // Inserting text on one line - this._setLineText(position.lineNumber - 1, - this._lines[position.lineNumber - 1].substring(0, position.column - 1) - + insertLines[0] - + this._lines[position.lineNumber - 1].substring(position.column - 1) - ); - return; - } - - // Append overflowing text from first line to the end of text to insert - insertLines[insertLines.length - 1] += this._lines[position.lineNumber - 1].substring(position.column - 1); - - // Delete overflowing text from first line and insert text on first line - this._setLineText(position.lineNumber - 1, - this._lines[position.lineNumber - 1].substring(0, position.column - 1) - + insertLines[0] - ); - - // Insert new lines & store lengths - let newLengths:number[] = new Array(insertLines.length - 1); - for (let i = 1; i < insertLines.length; i++) { - this._lines.splice(position.lineNumber + i - 1, 0, insertLines[i]); - newLengths[i - 1] = insertLines[i].length + this._eol.length; - } - - if (this._lineStarts) { - // update prefix sum - this._lineStarts.insertValues(position.lineNumber, newLengths); - } - } -} - @Remotable.MainContext('MainThreadDocuments') export class MainThreadDocuments { private _modelService: IModelService; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index afa6c8671bf..2e1785849e7 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -508,7 +508,7 @@ class SuggestAdapter implements modes.ISuggestSupport { let defaultSuggestions: modes.ISuggestResult = { suggestions: [], - currentWord: ran ? doc.getText(new Range(ran.start, pos)) : '', + currentWord: ran ? doc.getText(new Range(ran.start.line, ran.start.character, pos.line, pos.character)) : '', }; let allSuggestions: modes.ISuggestResult[] = [defaultSuggestions]; diff --git a/src/vs/workbench/test/common/api/extHostDocuments.test.ts b/src/vs/workbench/test/common/api/extHostDocuments.test.ts index 4ed3ca34d37..726388cc970 100644 --- a/src/vs/workbench/test/common/api/extHostDocuments.test.ts +++ b/src/vs/workbench/test/common/api/extHostDocuments.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; -import {ExtHostDocument} from 'vs/workbench/api/common/extHostDocuments'; +import {ExtHostDocumentData} from 'vs/workbench/api/common/extHostDocuments'; import {Position} from 'vs/workbench/api/common/extHostTypes'; import {Range as CodeEditorRange} from 'vs/editor/common/core/range'; import * as EditorCommon from 'vs/editor/common/editorCommon'; @@ -15,7 +15,7 @@ import * as EditorCommon from 'vs/editor/common/editorCommon'; suite("PluginHostDocument", () => { - let doc: ExtHostDocument; + let doc: ExtHostDocumentData; function assertPositionAt(offset: number, line: number, character: number) { let position = doc.positionAt(offset); @@ -30,7 +30,7 @@ suite("PluginHostDocument", () => { } setup(function() { - doc = new ExtHostDocument(undefined, URI.file(''), [ + doc = new ExtHostDocumentData(undefined, URI.file(''), [ 'This is line one', //16 'and this is line number two', //27 'it is followed by #3', //20 @@ -66,7 +66,7 @@ suite("PluginHostDocument", () => { assert.equal(line.isEmptyOrWhitespace, false); assert.equal(line.firstNonWhitespaceCharacterIndex, 0); - doc._acceptEvents([{ + doc.onEvents([{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, text: '\t ', isRedoing: undefined, @@ -102,7 +102,7 @@ suite("PluginHostDocument", () => { test('offsetAt, after remove', function() { - doc._acceptEvents([{ + doc.onEvents([{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, text: '', isRedoing: undefined, @@ -118,7 +118,7 @@ suite("PluginHostDocument", () => { test('offsetAt, after replace', function() { - doc._acceptEvents([{ + doc.onEvents([{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, text: 'is could be', isRedoing: undefined, @@ -134,7 +134,7 @@ suite("PluginHostDocument", () => { test('offsetAt, after insert line', function() { - doc._acceptEvents([{ + doc.onEvents([{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 1, endColumn: 6 }, text: 'is could be\na line with number', isRedoing: undefined, @@ -153,7 +153,7 @@ suite("PluginHostDocument", () => { test('offsetAt, after remove line', function() { - doc._acceptEvents([{ + doc.onEvents([{ range: { startLineNumber: 1, startColumn: 3, endLineNumber: 2, endColumn: 6 }, text: '', isRedoing: undefined, @@ -193,7 +193,7 @@ suite("PluginHostDocument updates line mapping", () => { return '(' + position.line + ',' + position.character + ')'; } - function assertDocumentLineMapping(doc:ExtHostDocument, direction:AssertDocumentLineMappingDirection): void { + function assertDocumentLineMapping(doc:ExtHostDocumentData, direction:AssertDocumentLineMappingDirection): void { let allText = doc.getText(); let line = 0, character = 0, previousIsCarriageReturn = false; @@ -234,10 +234,10 @@ suite("PluginHostDocument updates line mapping", () => { } function testLineMappingDirectionAfterEvents(lines:string[], eol: string, direction:AssertDocumentLineMappingDirection, events:EditorCommon.IModelContentChangedEvent2[]): void { - let myDocument = new ExtHostDocument(undefined, URI.file(''), lines.slice(0), eol, 'text', 1, false); + let myDocument = new ExtHostDocumentData(undefined, URI.file(''), lines.slice(0), eol, 'text', 1, false); assertDocumentLineMapping(myDocument, direction); - myDocument._acceptEvents(events); + myDocument.onEvents(events); assertDocumentLineMapping(myDocument, direction); }