Remove LinesTextBuffer

This commit is contained in:
Alex Dima 2018-03-28 12:08:25 +02:00
parent 805d0785aa
commit 4af8a64ea6
11 changed files with 53 additions and 1113 deletions

View file

@ -1,660 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import * as strings from 'vs/base/common/strings';
import * as arrays from 'vs/base/common/arrays';
import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer';
import { ISingleEditOperationIdentifier, IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange } from 'vs/editor/common/model';
import { ITextSnapshot } from 'vs/platform/files/common/files';
export interface IValidatedEditOperation {
sortIndex: number;
identifier: ISingleEditOperationIdentifier;
range: Range;
rangeOffset: number;
rangeLength: number;
lines: string[];
forceMoveMarkers: boolean;
isAutoWhitespaceEdit: boolean;
}
/**
* A processed string with its EOL resolved ready to be turned into an editor model.
*/
export interface ITextSource {
/**
* The text split into lines.
*/
readonly lines: string[];
/**
* The BOM (leading character sequence of the file).
*/
readonly BOM: string;
/**
* The end of line sequence.
*/
readonly EOL: string;
/**
* The text contains Unicode characters classified as "R" or "AL".
*/
readonly containsRTL: boolean;
/**
* The text contains only characters inside the ASCII range 32-126 or \t \r \n
*/
readonly isBasicASCII: boolean;
}
class LinesTextBufferSnapshot implements ITextSnapshot {
private readonly _lines: string[];
private readonly _linesLength: number;
private readonly _eol: string;
private readonly _bom: string;
private _lineIndex: number;
constructor(lines: string[], eol: string, bom: string) {
this._lines = lines;
this._linesLength = this._lines.length;
this._eol = eol;
this._bom = bom;
this._lineIndex = 0;
}
public read(): string {
if (this._lineIndex >= this._linesLength) {
return null;
}
let result: string = null;
if (this._lineIndex === 0) {
result = this._bom + this._lines[this._lineIndex];
} else {
result = this._lines[this._lineIndex];
}
this._lineIndex++;
if (this._lineIndex < this._linesLength) {
result += this._eol;
}
return result;
}
}
export class LinesTextBuffer implements ITextBuffer {
private _lines: string[];
private _BOM: string;
private _EOL: string;
private _mightContainRTL: boolean;
private _mightContainNonBasicASCII: boolean;
private _lineStarts: PrefixSumComputer;
constructor(textSource: ITextSource) {
this._lines = textSource.lines.slice(0);
this._BOM = textSource.BOM;
this._EOL = textSource.EOL;
this._mightContainRTL = textSource.containsRTL;
this._mightContainNonBasicASCII = !textSource.isBasicASCII;
this._constructLineStarts();
}
private _constructLineStarts(): void {
const eolLength = this._EOL.length;
const linesLength = this._lines.length;
const lineStartValues = new Uint32Array(linesLength);
for (let i = 0; i < linesLength; i++) {
lineStartValues[i] = this._lines[i].length + eolLength;
}
this._lineStarts = new PrefixSumComputer(lineStartValues);
}
public equals(other: ITextBuffer): boolean {
if (!(other instanceof LinesTextBuffer)) {
return false;
}
if (this._BOM !== other._BOM) {
return false;
}
if (this._EOL !== other._EOL) {
return false;
}
if (this._lines.length !== other._lines.length) {
return false;
}
for (let i = 0, len = this._lines.length; i < len; i++) {
if (this._lines[i] !== other._lines[i]) {
return false;
}
}
return true;
}
public mightContainRTL(): boolean {
return this._mightContainRTL;
}
public mightContainNonBasicASCII(): boolean {
return this._mightContainNonBasicASCII;
}
public getBOM(): string {
return this._BOM;
}
public getEOL(): string {
return this._EOL;
}
public getOffsetAt(lineNumber: number, column: number): number {
return this._lineStarts.getAccumulatedValue(lineNumber - 2) + column - 1;
}
public getPositionAt(offset: number): Position {
offset = Math.floor(offset);
offset = Math.max(0, offset);
let out = this._lineStarts.getIndexOf(offset);
let lineLength = this._lines[out.index].length;
// Ensure we return a valid position
return new Position(out.index + 1, Math.min(out.remainder + 1, lineLength + 1));
}
public getRangeAt(offset: number, length: number): Range {
const startResult = this._lineStarts.getIndexOf(offset);
const startLineLength = this._lines[startResult.index].length;
const startColumn = Math.min(startResult.remainder + 1, startLineLength + 1);
const endResult = this._lineStarts.getIndexOf(offset + length);
const endLineLength = this._lines[endResult.index].length;
const endColumn = Math.min(endResult.remainder + 1, endLineLength + 1);
return new Range(startResult.index + 1, startColumn, endResult.index + 1, endColumn);
}
private _getEndOfLine(eol: EndOfLinePreference): string {
switch (eol) {
case EndOfLinePreference.LF:
return '\n';
case EndOfLinePreference.CRLF:
return '\r\n';
case EndOfLinePreference.TextDefined:
return this.getEOL();
}
throw new Error('Unknown EOL preference');
}
public getValueInRange(range: Range, eol: EndOfLinePreference): string {
if (range.isEmpty()) {
return '';
}
if (range.startLineNumber === range.endLineNumber) {
return this._lines[range.startLineNumber - 1].substring(range.startColumn - 1, range.endColumn - 1);
}
const lineEnding = this._getEndOfLine(eol);
const startLineIndex = range.startLineNumber - 1;
const endLineIndex = range.endLineNumber - 1;
let resultLines: string[] = [];
resultLines.push(this._lines[startLineIndex].substring(range.startColumn - 1));
for (let i = startLineIndex + 1; i < endLineIndex; i++) {
resultLines.push(this._lines[i]);
}
resultLines.push(this._lines[endLineIndex].substring(0, range.endColumn - 1));
return resultLines.join(lineEnding);
}
public createSnapshot(preserveBOM: boolean): ITextSnapshot {
return new LinesTextBufferSnapshot(this._lines.slice(0), this._EOL, preserveBOM ? this._BOM : '');
}
public getValueLengthInRange(range: Range, eol: EndOfLinePreference): number {
if (range.isEmpty()) {
return 0;
}
if (range.startLineNumber === range.endLineNumber) {
return (range.endColumn - range.startColumn);
}
let startOffset = this.getOffsetAt(range.startLineNumber, range.startColumn);
let endOffset = this.getOffsetAt(range.endLineNumber, range.endColumn);
return endOffset - startOffset;
}
public getLineCount(): number {
return this._lines.length;
}
public getLinesContent(): string[] {
return this._lines.slice(0);
}
public getLength(): number {
return this._lineStarts.getTotalValue();
}
public getLineContent(lineNumber: number): string {
return this._lines[lineNumber - 1];
}
public getLineCharCode(lineNumber: number, index: number): number {
return this._lines[lineNumber - 1].charCodeAt(index);
}
public getLineLength(lineNumber: number): number {
return this._lines[lineNumber - 1].length;
}
public getLineFirstNonWhitespaceColumn(lineNumber: number): number {
const result = strings.firstNonWhitespaceIndex(this._lines[lineNumber - 1]);
if (result === -1) {
return 0;
}
return result + 1;
}
public getLineLastNonWhitespaceColumn(lineNumber: number): number {
const result = strings.lastNonWhitespaceIndex(this._lines[lineNumber - 1]);
if (result === -1) {
return 0;
}
return result + 2;
}
//#region Editing
public setEOL(newEOL: '\r\n' | '\n'): void {
this._EOL = newEOL;
this._constructLineStarts();
}
private static _sortOpsAscending(a: IValidatedEditOperation, b: IValidatedEditOperation): number {
let r = Range.compareRangesUsingEnds(a.range, b.range);
if (r === 0) {
return a.sortIndex - b.sortIndex;
}
return r;
}
private static _sortOpsDescending(a: IValidatedEditOperation, b: IValidatedEditOperation): number {
let r = Range.compareRangesUsingEnds(a.range, b.range);
if (r === 0) {
return b.sortIndex - a.sortIndex;
}
return -r;
}
public applyEdits(rawOperations: IIdentifiedSingleEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult {
if (rawOperations.length === 0) {
return new ApplyEditsResult([], [], []);
}
let mightContainRTL = this._mightContainRTL;
let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
let canReduceOperations = true;
let operations: IValidatedEditOperation[] = [];
for (let i = 0; i < rawOperations.length; i++) {
let op = rawOperations[i];
if (canReduceOperations && op._isTracked) {
canReduceOperations = false;
}
let validatedRange = op.range;
if (!mightContainRTL && op.text) {
// check if the new inserted text contains RTL
mightContainRTL = strings.containsRTL(op.text);
}
if (!mightContainNonBasicASCII && op.text) {
mightContainNonBasicASCII = !strings.isBasicASCII(op.text);
}
operations[i] = {
sortIndex: i,
identifier: op.identifier || null,
range: validatedRange,
rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn),
rangeLength: this.getValueLengthInRange(validatedRange, EndOfLinePreference.TextDefined),
lines: op.text ? op.text.split(/\r\n|\r|\n/) : null,
forceMoveMarkers: op.forceMoveMarkers || false,
isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false
};
}
// Sort operations ascending
operations.sort(LinesTextBuffer._sortOpsAscending);
for (let i = 0, count = operations.length - 1; i < count; i++) {
let rangeEnd = operations[i].range.getEndPosition();
let nextRangeStart = operations[i + 1].range.getStartPosition();
if (nextRangeStart.isBefore(rangeEnd)) {
// overlapping ranges
throw new Error('Overlapping ranges are not allowed!');
}
}
if (canReduceOperations) {
operations = this._reduceOperations(operations);
}
// Delta encode operations
let reverseRanges = LinesTextBuffer._getInverseEditRanges(operations);
let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = [];
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) {
// Record already the future line numbers that might be auto whitespace removal candidates on next edit
for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
let currentLineContent = '';
if (lineNumber === reverseRange.startLineNumber) {
currentLineContent = this.getLineContent(op.range.startLineNumber);
if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
continue;
}
}
newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
}
}
}
let reverseOperations: IIdentifiedSingleEditOperation[] = [];
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
reverseOperations[i] = {
identifier: op.identifier,
range: reverseRange,
text: this.getValueInRange(op.range, EndOfLinePreference.TextDefined),
forceMoveMarkers: op.forceMoveMarkers
};
}
this._mightContainRTL = mightContainRTL;
this._mightContainNonBasicASCII = mightContainNonBasicASCII;
const contentChanges = this._doApplyEdits(operations);
let trimAutoWhitespaceLineNumbers: number[] = null;
if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) {
// sort line numbers auto whitespace removal candidates for next edit descending
newTrimAutoWhitespaceCandidates.sort((a, b) => b.lineNumber - a.lineNumber);
trimAutoWhitespaceLineNumbers = [];
for (let i = 0, len = newTrimAutoWhitespaceCandidates.length; i < len; i++) {
let lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber;
if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) {
// Do not have the same line number twice
continue;
}
let prevContent = newTrimAutoWhitespaceCandidates[i].oldContent;
let lineContent = this.getLineContent(lineNumber);
if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) {
continue;
}
trimAutoWhitespaceLineNumbers.push(lineNumber);
}
}
return new ApplyEditsResult(
reverseOperations,
contentChanges,
trimAutoWhitespaceLineNumbers
);
}
/**
* Transform operations such that they represent the same logic edit,
* but that they also do not cause OOM crashes.
*/
private _reduceOperations(operations: IValidatedEditOperation[]): IValidatedEditOperation[] {
if (operations.length < 1000) {
// We know from empirical testing that a thousand edits work fine regardless of their shape.
return operations;
}
// At one point, due to how events are emitted and how each operation is handled,
// some operations can trigger a high ammount of temporary string allocations,
// that will immediately get edited again.
// e.g. a formatter inserting ridiculous ammounts of \n on a model with a single line
// Therefore, the strategy is to collapse all the operations into a huge single edit operation
return [this._toSingleEditOperation(operations)];
}
_toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation {
let forceMoveMarkers = false,
firstEditRange = operations[0].range,
lastEditRange = operations[operations.length - 1].range,
entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn),
lastEndLineNumber = firstEditRange.startLineNumber,
lastEndColumn = firstEditRange.startColumn,
result: string[] = [];
for (let i = 0, len = operations.length; i < len; i++) {
let operation = operations[i],
range = operation.range;
forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers;
// (1) -- Push old text
for (let lineNumber = lastEndLineNumber; lineNumber < range.startLineNumber; lineNumber++) {
if (lineNumber === lastEndLineNumber) {
result.push(this._lines[lineNumber - 1].substring(lastEndColumn - 1));
} else {
result.push('\n');
result.push(this._lines[lineNumber - 1]);
}
}
if (range.startLineNumber === lastEndLineNumber) {
result.push(this._lines[range.startLineNumber - 1].substring(lastEndColumn - 1, range.startColumn - 1));
} else {
result.push('\n');
result.push(this._lines[range.startLineNumber - 1].substring(0, range.startColumn - 1));
}
// (2) -- Push new text
if (operation.lines) {
for (let j = 0, lenJ = operation.lines.length; j < lenJ; j++) {
if (j !== 0) {
result.push('\n');
}
result.push(operation.lines[j]);
}
}
lastEndLineNumber = operation.range.endLineNumber;
lastEndColumn = operation.range.endColumn;
}
return {
sortIndex: 0,
identifier: operations[0].identifier,
range: entireEditRange,
rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn),
rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined),
lines: result.join('').split('\n'),
forceMoveMarkers: forceMoveMarkers,
isAutoWhitespaceEdit: false
};
}
private _setLineContent(lineNumber: number, content: string): void {
this._lines[lineNumber - 1] = content;
this._lineStarts.changeValue(lineNumber - 1, content.length + this._EOL.length);
}
private _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] {
// Sort operations descending
operations.sort(LinesTextBuffer._sortOpsDescending);
let contentChanges: IInternalModelContentChange[] = [];
for (let i = 0, len = operations.length; i < len; i++) {
const op = operations[i];
const startLineNumber = op.range.startLineNumber;
const startColumn = op.range.startColumn;
const endLineNumber = op.range.endLineNumber;
const endColumn = op.range.endColumn;
if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.lines || op.lines.length === 0)) {
// no-op
continue;
}
const deletingLinesCnt = endLineNumber - startLineNumber;
const insertingLinesCnt = (op.lines ? op.lines.length - 1 : 0);
const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt);
for (let j = editingLinesCnt; j >= 0; j--) {
const editLineNumber = startLineNumber + j;
let editText = (op.lines ? op.lines[j] : '');
if (editLineNumber === startLineNumber || editLineNumber === endLineNumber) {
const editStartColumn = (editLineNumber === startLineNumber ? startColumn : 1);
const editEndColumn = (editLineNumber === endLineNumber ? endColumn : this.getLineLength(editLineNumber) + 1);
editText = (
this._lines[editLineNumber - 1].substring(0, editStartColumn - 1)
+ editText
+ this._lines[editLineNumber - 1].substring(editEndColumn - 1)
);
}
this._setLineContent(editLineNumber, editText);
}
if (editingLinesCnt < deletingLinesCnt) {
// Must delete some lines
const spliceStartLineNumber = startLineNumber + editingLinesCnt;
const endLineRemains = this._lines[endLineNumber - 1].substring(endColumn - 1);
// Reconstruct first line
this._setLineContent(spliceStartLineNumber, this._lines[spliceStartLineNumber - 1] + endLineRemains);
this._lines.splice(spliceStartLineNumber, endLineNumber - spliceStartLineNumber);
this._lineStarts.removeValues(spliceStartLineNumber, endLineNumber - spliceStartLineNumber);
}
if (editingLinesCnt < insertingLinesCnt) {
// Must insert some lines
const spliceLineNumber = startLineNumber + editingLinesCnt;
let spliceColumn = (spliceLineNumber === startLineNumber ? startColumn : 1);
if (op.lines) {
spliceColumn += op.lines[editingLinesCnt].length;
}
// Split last line
const leftoverLine = this._lines[spliceLineNumber - 1].substring(spliceColumn - 1);
this._setLineContent(spliceLineNumber, this._lines[spliceLineNumber - 1].substring(0, spliceColumn - 1));
// Lines in the middle
let newLines: string[] = new Array<string>(insertingLinesCnt - editingLinesCnt);
let newLinesLengths = new Uint32Array(insertingLinesCnt - editingLinesCnt);
for (let j = editingLinesCnt + 1; j <= insertingLinesCnt; j++) {
newLines[j - editingLinesCnt - 1] = op.lines[j];
newLinesLengths[j - editingLinesCnt - 1] = op.lines[j].length + this._EOL.length;
}
newLines[newLines.length - 1] += leftoverLine;
newLinesLengths[newLines.length - 1] += leftoverLine.length;
this._lines = arrays.arrayInsert(this._lines, startLineNumber + editingLinesCnt, newLines);
this._lineStarts.insertValues(startLineNumber + editingLinesCnt, newLinesLengths);
}
const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
const text = (op.lines ? op.lines.join(this.getEOL()) : '');
contentChanges.push({
range: contentChangeRange,
rangeLength: op.rangeLength,
text: text,
rangeOffset: op.rangeOffset,
forceMoveMarkers: op.forceMoveMarkers
});
}
return contentChanges;
}
/**
* Assumes `operations` are validated and sorted ascending
*/
public static _getInverseEditRanges(operations: IValidatedEditOperation[]): Range[] {
let result: Range[] = [];
let prevOpEndLineNumber: number;
let prevOpEndColumn: number;
let prevOp: IValidatedEditOperation = null;
for (let i = 0, len = operations.length; i < len; i++) {
let op = operations[i];
let startLineNumber: number;
let startColumn: number;
if (prevOp) {
if (prevOp.range.endLineNumber === op.range.startLineNumber) {
startLineNumber = prevOpEndLineNumber;
startColumn = prevOpEndColumn + (op.range.startColumn - prevOp.range.endColumn);
} else {
startLineNumber = prevOpEndLineNumber + (op.range.startLineNumber - prevOp.range.endLineNumber);
startColumn = op.range.startColumn;
}
} else {
startLineNumber = op.range.startLineNumber;
startColumn = op.range.startColumn;
}
let resultRange: Range;
if (op.lines && op.lines.length > 0) {
// the operation inserts something
let lineCount = op.lines.length;
let firstLine = op.lines[0];
let lastLine = op.lines[lineCount - 1];
if (lineCount === 1) {
// single line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLine.length);
} else {
// multi line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLine.length + 1);
}
} else {
// There is nothing to insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn);
}
prevOpEndLineNumber = resultRange.endLineNumber;
prevOpEndColumn = resultRange.endColumn;
result.push(resultRange);
prevOp = op;
}
return result;
}
//#endregion
}

View file

@ -1,139 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as strings from 'vs/base/common/strings';
import { CharCode } from 'vs/base/common/charCode';
import { ITextBufferBuilder, ITextBufferFactory, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model';
import { IRawTextSource, TextSource } from 'vs/editor/common/model/linesTextBuffer/textSource';
import { LinesTextBuffer } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer';
export class TextBufferFactory implements ITextBufferFactory {
constructor(public readonly rawTextSource: IRawTextSource) {
}
public create(defaultEOL: DefaultEndOfLine): ITextBuffer {
const textSource = TextSource.fromRawTextSource(this.rawTextSource, defaultEOL);
return new LinesTextBuffer(textSource);
}
public getFirstLineText(lengthLimit: number): string {
return this.rawTextSource.lines[0].substr(0, lengthLimit);
}
}
class ModelLineBasedBuilder {
private BOM: string;
private lines: string[];
private currLineIndex: number;
constructor() {
this.BOM = '';
this.lines = [];
this.currLineIndex = 0;
}
public acceptLines(lines: string[]): void {
if (this.currLineIndex === 0) {
// Remove the BOM (if present)
if (strings.startsWithUTF8BOM(lines[0])) {
this.BOM = strings.UTF8_BOM_CHARACTER;
lines[0] = lines[0].substr(1);
}
}
for (let i = 0, len = lines.length; i < len; i++) {
this.lines[this.currLineIndex++] = lines[i];
}
}
public finish(carriageReturnCnt: number, containsRTL: boolean, isBasicASCII: boolean): TextBufferFactory {
return new TextBufferFactory({
BOM: this.BOM,
lines: this.lines,
containsRTL: containsRTL,
totalCRCount: carriageReturnCnt,
isBasicASCII,
});
}
}
export class LinesTextBufferBuilder implements ITextBufferBuilder {
private leftoverPrevChunk: string;
private leftoverEndsInCR: boolean;
private totalCRCount: number;
private lineBasedBuilder: ModelLineBasedBuilder;
private containsRTL: boolean;
private isBasicASCII: boolean;
constructor() {
this.leftoverPrevChunk = '';
this.leftoverEndsInCR = false;
this.totalCRCount = 0;
this.lineBasedBuilder = new ModelLineBasedBuilder();
this.containsRTL = false;
this.isBasicASCII = true;
}
private _updateCRCount(chunk: string): void {
// Count how many \r are present in chunk to determine the majority EOL sequence
let chunkCarriageReturnCnt = 0;
let lastCarriageReturnIndex = -1;
while ((lastCarriageReturnIndex = chunk.indexOf('\r', lastCarriageReturnIndex + 1)) !== -1) {
chunkCarriageReturnCnt++;
}
this.totalCRCount += chunkCarriageReturnCnt;
}
public acceptChunk(chunk: string): void {
if (chunk.length === 0) {
return;
}
this._updateCRCount(chunk);
if (!this.containsRTL) {
this.containsRTL = strings.containsRTL(chunk);
}
if (this.isBasicASCII) {
this.isBasicASCII = strings.isBasicASCII(chunk);
}
// Avoid dealing with a chunk that ends in \r (push the \r to the next chunk)
if (this.leftoverEndsInCR) {
chunk = '\r' + chunk;
}
if (chunk.charCodeAt(chunk.length - 1) === CharCode.CarriageReturn) {
this.leftoverEndsInCR = true;
chunk = chunk.substr(0, chunk.length - 1);
} else {
this.leftoverEndsInCR = false;
}
let lines = chunk.split(/\r\n|\r|\n/);
if (lines.length === 1) {
// no \r or \n encountered
this.leftoverPrevChunk += lines[0];
return;
}
lines[0] = this.leftoverPrevChunk + lines[0];
this.lineBasedBuilder.acceptLines(lines.slice(0, lines.length - 1));
this.leftoverPrevChunk = lines[lines.length - 1];
}
public finish(): TextBufferFactory {
let finalLines = [this.leftoverPrevChunk];
if (this.leftoverEndsInCR) {
finalLines.push('');
}
this.lineBasedBuilder.acceptLines(finalLines);
return this.lineBasedBuilder.finish(this.totalCRCount, this.containsRTL, this.isBasicASCII);
}
}

View file

@ -1,66 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { DefaultEndOfLine } from 'vs/editor/common/model';
import { ITextSource } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer';
/**
* A processed string ready to be turned into an editor model.
*/
export interface IRawTextSource {
/**
* The text split into lines.
*/
readonly lines: string[];
/**
* The BOM (leading character sequence of the file).
*/
readonly BOM: string;
/**
* The number of lines ending with '\r\n'
*/
readonly totalCRCount: number;
/**
* The text contains Unicode characters classified as "R" or "AL".
*/
readonly containsRTL: boolean;
/**
* The text contains only characters inside the ASCII range 32-126 or \t \r \n
*/
readonly isBasicASCII: boolean;
}
export class TextSource {
/**
* if text source is empty or with precisely one line, returns null. No end of line is detected.
* if text source contains more lines ending with '\r\n', returns '\r\n'.
* Otherwise returns '\n'. More lines end with '\n'.
*/
private static _getEOL(rawTextSource: IRawTextSource, defaultEOL: DefaultEndOfLine): '\r\n' | '\n' {
const lineFeedCnt = rawTextSource.lines.length - 1;
if (lineFeedCnt === 0) {
// This is an empty file or a file with precisely one line
return (defaultEOL === DefaultEndOfLine.LF ? '\n' : '\r\n');
}
if (rawTextSource.totalCRCount > lineFeedCnt / 2) {
// More than half of the file contains \r\n ending lines
return '\r\n';
}
// At least one line more ends in \n
return '\n';
}
public static fromRawTextSource(rawTextSource: IRawTextSource, defaultEOL: DefaultEndOfLine): ITextSource {
return {
lines: rawTextSource.lines,
BOM: rawTextSource.BOM,
EOL: TextSource._getEOL(rawTextSource, defaultEOL),
containsRTL: rawTextSource.containsRTL,
isBasicASCII: rawTextSource.isBasicASCII,
};
}
}

View file

@ -7,12 +7,22 @@
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import * as strings from 'vs/base/common/strings';
import { IValidatedEditOperation } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer';
import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase';
import { IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange, FindMatch } from 'vs/editor/common/model';
import { IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange, FindMatch, ISingleEditOperationIdentifier } from 'vs/editor/common/model';
import { ITextSnapshot } from 'vs/platform/files/common/files';
import { SearchData } from 'vs/editor/common/model/textModelSearch';
export interface IValidatedEditOperation {
sortIndex: number;
identifier: ISingleEditOperationIdentifier;
range: Range;
rangeOffset: number;
rangeLength: number;
lines: string[];
forceMoveMarkers: boolean;
isAutoWhitespaceEdit: boolean;
}
export class PieceTreeTextBuffer implements ITextBuffer {
private _pieceTree: PieceTreeBase;
private _BOM: string;
@ -279,9 +289,9 @@ export class PieceTreeTextBuffer implements ITextBuffer {
}
/**
* Transform operations such that they represent the same logic edit,
* but that they also do not cause OOM crashes.
*/
* Transform operations such that they represent the same logic edit,
* but that they also do not cause OOM crashes.
*/
private _reduceOperations(operations: IValidatedEditOperation[]): IValidatedEditOperation[] {
if (operations.length < 1000) {
// We know from empirical testing that a thousand edits work fine regardless of their shape.

View file

@ -33,23 +33,10 @@ import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { TextModelSearch, SearchParams, SearchData } from 'vs/editor/common/model/textModelSearch';
import { TPromise } from 'vs/base/common/winjs.base';
import { IStringStream, ITextSnapshot } from 'vs/platform/files/common/files';
import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder';
import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
export enum TextBufferType {
LinesArray,
PieceTree
}
// Here is the master switch for the text buffer implementation:
export const OPTIONS = {
TEXT_BUFFER_IMPLEMENTATION: TextBufferType.PieceTree
};
function createTextBufferBuilder() {
if (OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) {
return new PieceTreeTextBufferBuilder();
}
return new LinesTextBufferBuilder();
return new PieceTreeTextBufferBuilder();
}
export function createTextBufferFactory(text: string): model.ITextBufferFactory {
@ -999,7 +986,7 @@ export class TextModel extends Disposable implements model.ITextModel {
searchRange = this.getFullModelRange();
}
if (!isRegex && searchString.indexOf('\n') < 0 && OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) {
if (!isRegex && searchString.indexOf('\n') < 0) {
// not regex, not multi line
const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators);
const searchData = searchParams.parseSearchRequest();
@ -1018,7 +1005,7 @@ export class TextModel extends Disposable implements model.ITextModel {
this._assertNotDisposed();
const searchStart = this.validatePosition(rawSearchStart);
if (!isRegex && searchString.indexOf('\n') < 0 && OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) {
if (!isRegex && searchString.indexOf('\n') < 0) {
const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators);
const searchData = searchParams.parseSearchRequest();
const lineCount = this.getLineCount();

View file

@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { ITextBufferBuilder, ITextBufferFactory, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model';
import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder';
import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
export function doBenchmark<T>(id: string, ts: T[], fn: (t: T) => void) {
@ -56,7 +55,7 @@ export class BenchmarkSuite {
for (let i = 0; i < this.benchmarks.length; i++) {
let benchmark = this.benchmarks[i];
let columns: string[] = [benchmark.name];
[new LinesTextBufferBuilder(), new PieceTreeTextBufferBuilder()].forEach((builder: ITextBufferBuilder) => {
[new PieceTreeTextBufferBuilder()].forEach((builder: ITextBufferBuilder) => {
let timeDiffTotal = 0.0;
for (let j = 0; j < this.iterations; j++) {
let factory = benchmark.buildBuffer(builder);

View file

@ -4,13 +4,11 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder';
import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
import { ITextBufferBuilder } from 'vs/editor/common/model';
import { generateRandomChunkWithLF } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils';
import { doBenchmark } from 'vs/editor/test/common/model/benchmark/benchmarkUtils';
let linesTextBufferBuilder = new LinesTextBufferBuilder();
let pieceTreeTextBufferBuilder = new PieceTreeTextBufferBuilder();
let chunks = [];
@ -30,5 +28,5 @@ let modelBuildBenchmark = function (id: string, builders: ITextBufferBuilder[],
console.log(`|model builder\t|line buffer\t|piece table\t|`);
console.log('|---|---|---|');
for (let i of [10, 100]) {
modelBuildBenchmark(`${i} random chunks`, [linesTextBufferBuilder, pieceTreeTextBufferBuilder], i);
modelBuildBenchmark(`${i} random chunks`, [pieceTreeTextBufferBuilder], i);
}

View file

@ -7,9 +7,11 @@
import * as assert from 'assert';
import { Range } from 'vs/editor/common/core/range';
import { LinesTextBuffer, IValidatedEditOperation } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer';
import { PieceTreeTextBuffer, IValidatedEditOperation } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { DefaultEndOfLine } from 'vs/editor/common/model';
suite('LinesTextBuffer._getInverseEdits', () => {
suite('PieceTreeTextBuffer._getInverseEdits', () => {
function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): IValidatedEditOperation {
return {
@ -29,7 +31,7 @@ suite('LinesTextBuffer._getInverseEdits', () => {
}
function assertInverseEdits(ops: IValidatedEditOperation[], expected: Range[]): void {
var actual = LinesTextBuffer._getInverseEditRanges(ops);
var actual = PieceTreeTextBuffer._getInverseEditRanges(ops);
assert.deepEqual(actual, expected);
}
@ -260,7 +262,7 @@ suite('LinesTextBuffer._getInverseEdits', () => {
});
});
suite('LinesTextBuffer._toSingleEditOperation', () => {
suite('PieceTreeTextBuffer._toSingleEditOperation', () => {
function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeOffset: number, rangeLength: number, text: string[]): IValidatedEditOperation {
return {
@ -276,13 +278,7 @@ suite('LinesTextBuffer._toSingleEditOperation', () => {
}
function testToSingleEditOperation(original: string[], edits: IValidatedEditOperation[], expected: IValidatedEditOperation): void {
const textBuffer = new LinesTextBuffer({
BOM: '',
EOL: '\n',
containsRTL: false,
isBasicASCII: true,
lines: original
});
const textBuffer = <PieceTreeTextBuffer>createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF);
const actual = textBuffer._toSingleEditOperation(edits);
assert.deepEqual(actual, expected);

View file

@ -5,150 +5,69 @@
'use strict';
import * as assert from 'assert';
import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder';
import { ITextModelCreationOptions } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { DefaultEndOfLine } from 'vs/editor/common/model';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import * as strings from 'vs/base/common/strings';
import { IRawTextSource } from 'vs/editor/common/model/linesTextBuffer/textSource';
import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer';
class RawTextSource {
public static fromString(rawText: string): IRawTextSource {
// Count the number of lines that end with \r\n
let carriageReturnCnt = 0;
let lastCarriageReturnIndex = -1;
while ((lastCarriageReturnIndex = rawText.indexOf('\r', lastCarriageReturnIndex + 1)) !== -1) {
carriageReturnCnt++;
}
export function testTextBufferFactory(text: string, eol: string, mightContainNonBasicASCII: boolean, mightContainRTL: boolean): void {
const textBuffer = <PieceTreeTextBuffer>createTextBufferFactory(text).create(DefaultEndOfLine.LF);
const containsRTL = strings.containsRTL(rawText);
const isBasicASCII = (containsRTL ? false : strings.isBasicASCII(rawText));
// Split the text into lines
const lines = rawText.split(/\r\n|\r|\n/);
// Remove the BOM (if present)
let BOM = '';
if (strings.startsWithUTF8BOM(lines[0])) {
BOM = strings.UTF8_BOM_CHARACTER;
lines[0] = lines[0].substr(1);
}
return {
BOM: BOM,
lines: lines,
containsRTL: containsRTL,
isBasicASCII: isBasicASCII,
totalCRCount: carriageReturnCnt
};
}
}
export function testModelBuilder(chunks: string[], opts: ITextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS): void {
let expectedTextSource = RawTextSource.fromString(chunks.join(''));
let builder = new LinesTextBufferBuilder();
for (let i = 0, len = chunks.length; i < len; i++) {
builder.acceptChunk(chunks[i]);
}
let actual = builder.finish();
assert.deepEqual(actual.rawTextSource, expectedTextSource);
assert.equal(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII);
assert.equal(textBuffer.mightContainRTL(), mightContainRTL);
assert.equal(textBuffer.getEOL(), eol);
}
suite('ModelBuilder', () => {
test('no chunks', () => {
testModelBuilder([]);
test('t1', () => {
testTextBufferFactory('', '\n', false, false);
});
test('single empty chunk', () => {
testModelBuilder(['']);
test('t2', () => {
testTextBufferFactory('Hello world', '\n', false, false);
});
test('single line in one chunk', () => {
testModelBuilder(['Hello world']);
test('t3', () => {
testTextBufferFactory('Hello world\nHow are you?', '\n', false, false);
});
test('single line in multiple chunks', () => {
testModelBuilder(['Hello', ' ', 'world']);
});
test('two lines in single chunk', () => {
testModelBuilder(['Hello world\nHow are you?']);
});
test('two lines in multiple chunks 1', () => {
testModelBuilder(['Hello worl', 'd\nHow are you?']);
});
test('two lines in multiple chunks 2', () => {
testModelBuilder(['Hello worl', 'd', '\n', 'H', 'ow are you?']);
});
test('two lines in multiple chunks 3', () => {
testModelBuilder(['Hello worl', 'd', '\nHow are you?']);
});
test('multiple lines in single chunks', () => {
testModelBuilder(['Hello world\nHow are you?\nIs everything good today?\nDo you enjoy the weather?']);
});
test('multiple lines in multiple chunks 1', () => {
testModelBuilder(['Hello world\nHow are you', '?\nIs everything good today?\nDo you enjoy the weather?']);
});
test('multiple lines in multiple chunks 1', () => {
testModelBuilder(['Hello world', '\nHow are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']);
});
test('multiple lines in multiple chunks 1', () => {
testModelBuilder(['Hello world\n', 'How are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']);
test('t4', () => {
testTextBufferFactory('Hello world\nHow are you?\nIs everything good today?\nDo you enjoy the weather?', '\n', false, false);
});
test('carriage return detection (1 \\r\\n 2 \\n)', () => {
testModelBuilder(['Hello world\r\n', 'How are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']);
testTextBufferFactory('Hello world\r\nHow are you?\nIs everything good today?\nDo you enjoy the weather?', '\n', false, false);
});
test('carriage return detection (2 \\r\\n 1 \\n)', () => {
testModelBuilder(['Hello world\r\n', 'How are you', '?\r\nIs everything good today?', '\nDo you enjoy the weather?']);
testTextBufferFactory('Hello world\r\nHow are you?\r\nIs everything good today?\nDo you enjoy the weather?', '\r\n', false, false);
});
test('carriage return detection (3 \\r\\n 0 \\n)', () => {
testModelBuilder(['Hello world\r\n', 'How are you', '?\r\nIs everything good today?', '\r\nDo you enjoy the weather?']);
});
test('carriage return detection (isolated \\r)', () => {
testModelBuilder(['Hello world', '\r', '\n', 'How are you', '?', '\r', '\n', 'Is everything good today?', '\r', '\n', 'Do you enjoy the weather?']);
testTextBufferFactory('Hello world\r\nHow are you?\r\nIs everything good today?\r\nDo you enjoy the weather?', '\r\n', false, false);
});
test('BOM handling', () => {
testModelBuilder([strings.UTF8_BOM_CHARACTER + 'Hello world!']);
testTextBufferFactory(strings.UTF8_BOM_CHARACTER + 'Hello world!', '\n', false, false);
});
test('BOM handling', () => {
testModelBuilder([strings.UTF8_BOM_CHARACTER, 'Hello world!']);
});
test('RTL handling 1', () => {
testModelBuilder(['Hello world!', 'זוהי עובדה מבוססת שדעתו']);
testTextBufferFactory(strings.UTF8_BOM_CHARACTER + 'Hello world!', '\n', false, false);
});
test('RTL handling 2', () => {
testModelBuilder(['Hello world!זוהי עובדה מבוססת שדעתו']);
testTextBufferFactory('Hello world!זוהי עובדה מבוססת שדעתו', '\n', true, true);
});
test('RTL handling 3', () => {
testModelBuilder(['Hello world!זוהי \nעובדה מבוססת שדעתו']);
testTextBufferFactory('Hello world!זוהי \nעובדה מבוססת שדעתו', '\n', true, true);
});
test('ASCII handling 1', () => {
testModelBuilder(['Hello world!!\nHow do you do?']);
testTextBufferFactory('Hello world!!\nHow do you do?', '\n', false, false);
});
test('ASCII handling 1', () => {
testModelBuilder(['Hello world!!\nHow do you do?Züricha📚📚b']);
});
test('issue #32819: some special string cannot be displayed completely', () => {
testModelBuilder(['123']);
test('ASCII handling 2', () => {
testTextBufferFactory('Hello world!!\nHow do you do?Züricha📚📚b', '\n', true, false);
});
});

View file

@ -1,97 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { testModelBuilder } from './linesTextBufferBuilder.test';
import { getRandomInt, getRandomEOLSequence, getRandomString } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils';
const GENERATE_TESTS = false;
suite('ModelBuilder Auto Tests', () => {
test('auto1', () => {
testModelBuilder(['sarjniow', '\r', '\nbpb', 'ofb', '\njzldgxx', '\r\nkzwfjysng']);
});
test('auto2', () => {
testModelBuilder(['i', 'yyernubi\r\niimgn\n', 'ut\r']);
});
});
function generateRandomFile(): string {
let lineCount = getRandomInt(1, 10);
let mixedEOLSequence = getRandomInt(1, 2) === 1 ? true : false;
let fixedEOL = getRandomEOLSequence();
let lines: string[] = [];
for (let i = 0; i < lineCount; i++) {
if (i !== 0) {
if (mixedEOLSequence) {
lines.push(getRandomEOLSequence());
} else {
lines.push(fixedEOL);
}
}
lines.push(getRandomString(0, 10));
}
return lines.join('');
}
function generateRandomChunks(file: string): string[] {
let result: string[] = [];
let cnt = getRandomInt(1, 20);
let maxOffset = file.length;
while (cnt > 0 && maxOffset > 0) {
let offset = getRandomInt(0, maxOffset);
result.unshift(file.substring(offset, maxOffset));
// let length = getRandomInt(0, maxOffset - offset);
// let text = generateFile(true);
// result.push({
// offset: offset,
// length: length,
// text: text
// });
maxOffset = offset;
cnt--;
}
if (maxOffset !== 0) {
result.unshift(file.substring(0, maxOffset));
}
return result;
}
function testRandomFile(file: string): boolean {
let tests = getRandomInt(5, 10);
for (let i = 0; i < tests; i++) {
let chunks = generateRandomChunks(file);
try {
testModelBuilder(chunks);
} catch (err) {
console.log(err);
console.log(JSON.stringify(chunks));
return false;
}
}
return true;
}
if (GENERATE_TESTS) {
let number = 1;
while (true) {
console.log('------BEGIN NEW TEST: ' + number);
if (!testRandomFile(generateRandomFile())) {
break;
}
console.log('------END NEW TEST: ' + (number++));
}
}

View file

@ -13,10 +13,3 @@ import './electron-browser/toggleMultiCursorModifier';
import './electron-browser/toggleRenderControlCharacter';
import './electron-browser/toggleRenderWhitespace';
import './electron-browser/toggleWordWrap';
import { OPTIONS, TextBufferType } from 'vs/editor/common/model/textModel';
// Configure text buffer implementation
if (process.env['VSCODE_PIECE_TREE']) {
console.log(`Using TextBufferType.PieceTree (env variable VSCODE_PIECE_TREE)`);
OPTIONS.TEXT_BUFFER_IMPLEMENTATION = TextBufferType.PieceTree;
}