提交 805d0785 编写于 作者: A Alex Dima

Remove ChunksTextBuffer

上级 ef7763a2
/*---------------------------------------------------------------------------------------------
* 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 { CharCode } from 'vs/base/common/charCode';
export class LeafOffsetLenEdit {
constructor(
public readonly start: number,
public readonly length: number,
public readonly text: string
) { }
}
export class BufferPiece {
private readonly _str: string;
public get text(): string { return this._str; }
private readonly _lineStarts: Uint32Array;
constructor(str: string, lineStarts: Uint32Array = null) {
this._str = str;
if (lineStarts === null) {
this._lineStarts = createLineStartsFast(str);
} else {
this._lineStarts = lineStarts;
}
}
public length(): number {
return this._str.length;
}
public newLineCount(): number {
return this._lineStarts.length;
}
public lineStartFor(relativeLineIndex: number): number {
return this._lineStarts[relativeLineIndex];
}
public charCodeAt(index: number): number {
return this._str.charCodeAt(index);
}
public substr(from: number, length: number): string {
return this._str.substr(from, length);
}
public findLineStartBeforeOffset(offset: number): number {
if (this._lineStarts.length === 0 || offset < this._lineStarts[0]) {
return -1;
}
let low = 0, high = this._lineStarts.length - 1;
while (low < high) {
let mid = low + Math.ceil((high - low) / 2);
let lineStart = this._lineStarts[mid];
if (offset === lineStart) {
return mid;
} else if (offset < lineStart) {
high = mid - 1;
} else {
low = mid;
}
}
return low;
}
public findLineFirstNonWhitespaceIndex(searchStartOffset: number): number {
for (let i = searchStartOffset, len = this._str.length; i < len; i++) {
const chCode = this._str.charCodeAt(i);
if (chCode === CharCode.CarriageReturn || chCode === CharCode.LineFeed) {
// Reached EOL
return -2;
}
if (chCode !== CharCode.Space && chCode !== CharCode.Tab) {
return i;
}
}
return -1;
}
public findLineLastNonWhitespaceIndex(searchStartOffset: number): number {
for (let i = searchStartOffset - 1; i >= 0; i--) {
const chCode = this._str.charCodeAt(i);
if (chCode === CharCode.CarriageReturn || chCode === CharCode.LineFeed) {
// Reached EOL
return -2;
}
if (chCode !== CharCode.Space && chCode !== CharCode.Tab) {
return i;
}
}
return -1;
}
public static normalizeEOL(target: BufferPiece, eol: '\r\n' | '\n'): BufferPiece {
return new BufferPiece(target._str.replace(/\r\n|\r|\n/g, eol));
}
public static deleteLastChar(target: BufferPiece): BufferPiece {
const targetCharsLength = target.length();
const targetLineStartsLength = target.newLineCount();
const targetLineStarts = target._lineStarts;
let newLineStartsLength;
if (targetLineStartsLength > 0 && targetLineStarts[targetLineStartsLength - 1] === targetCharsLength) {
newLineStartsLength = targetLineStartsLength - 1;
} else {
newLineStartsLength = targetLineStartsLength;
}
let newLineStarts = new Uint32Array(newLineStartsLength);
newLineStarts.set(targetLineStarts);
return new BufferPiece(
target._str.substr(0, targetCharsLength - 1),
newLineStarts
);
}
public static insertFirstChar(target: BufferPiece, character: number): BufferPiece {
const targetLineStartsLength = target.newLineCount();
const targetLineStarts = target._lineStarts;
const insertLineStart = ((character === CharCode.CarriageReturn && (targetLineStartsLength === 0 || targetLineStarts[0] !== 1 || target.charCodeAt(0) !== CharCode.LineFeed)) || (character === CharCode.LineFeed));
const newLineStartsLength = (insertLineStart ? targetLineStartsLength + 1 : targetLineStartsLength);
let newLineStarts = new Uint32Array(newLineStartsLength);
if (insertLineStart) {
newLineStarts[0] = 1;
for (let i = 0; i < targetLineStartsLength; i++) {
newLineStarts[i + 1] = targetLineStarts[i] + 1;
}
} else {
for (let i = 0; i < targetLineStartsLength; i++) {
newLineStarts[i] = targetLineStarts[i] + 1;
}
}
return new BufferPiece(
String.fromCharCode(character) + target._str,
newLineStarts
);
}
public static join(first: BufferPiece, second: BufferPiece): BufferPiece {
const firstCharsLength = first._str.length;
const firstLineStartsLength = first._lineStarts.length;
const secondLineStartsLength = second._lineStarts.length;
const firstLineStarts = first._lineStarts;
const secondLineStarts = second._lineStarts;
const newLineStartsLength = firstLineStartsLength + secondLineStartsLength;
let newLineStarts = new Uint32Array(newLineStartsLength);
newLineStarts.set(firstLineStarts, 0);
for (let i = 0; i < secondLineStartsLength; i++) {
newLineStarts[i + firstLineStartsLength] = secondLineStarts[i] + firstCharsLength;
}
return new BufferPiece(first._str + second._str, newLineStarts);
}
public static replaceOffsetLen(target: BufferPiece, edits: LeafOffsetLenEdit[], idealLeafLength: number, maxLeafLength: number, result: BufferPiece[]): void {
const editsSize = edits.length;
const originalCharsLength = target.length();
if (editsSize === 1 && edits[0].text.length === 0 && edits[0].start === 0 && edits[0].length === originalCharsLength) {
// special case => deleting everything
return;
}
let pieces: string[] = new Array<string>(2 * editsSize + 1);
let originalFromIndex = 0;
let piecesTextLength = 0;
for (let i = 0; i < editsSize; i++) {
const edit = edits[i];
const originalText = target._str.substr(originalFromIndex, edit.start - originalFromIndex);
pieces[2 * i] = originalText;
piecesTextLength += originalText.length;
originalFromIndex = edit.start + edit.length;
pieces[2 * i + 1] = edit.text;
piecesTextLength += edit.text.length;
}
// maintain the chars that survive to the right of the last edit
let text = target._str.substr(originalFromIndex, originalCharsLength - originalFromIndex);
pieces[2 * editsSize] = text;
piecesTextLength += text.length;
let targetDataLength = piecesTextLength > maxLeafLength ? idealLeafLength : piecesTextLength;
let targetDataOffset = 0;
let data: string = '';
for (let pieceIndex = 0, pieceCount = pieces.length; pieceIndex < pieceCount; pieceIndex++) {
const pieceText = pieces[pieceIndex];
const pieceLength = pieceText.length;
if (pieceLength === 0) {
continue;
}
let pieceOffset = 0;
while (pieceOffset < pieceLength) {
if (targetDataOffset >= targetDataLength) {
result.push(new BufferPiece(data));
targetDataLength = piecesTextLength > maxLeafLength ? idealLeafLength : piecesTextLength;
targetDataOffset = 0;
data = '';
}
let writingCnt = min(pieceLength - pieceOffset, targetDataLength - targetDataOffset);
data += pieceText.substr(pieceOffset, writingCnt);
pieceOffset += writingCnt;
targetDataOffset += writingCnt;
piecesTextLength -= writingCnt;
// check that the buffer piece does not end in a \r or high surrogate
if (targetDataOffset === targetDataLength && piecesTextLength > 0) {
const lastChar = data.charCodeAt(targetDataLength - 1);
if (lastChar === CharCode.CarriageReturn || (0xD800 <= lastChar && lastChar <= 0xDBFF)) {
// move lastChar over to next buffer piece
targetDataLength -= 1;
pieceOffset -= 1;
targetDataOffset -= 1;
piecesTextLength += 1;
data = data.substr(0, data.length - 1);
}
}
}
}
result.push(new BufferPiece(data));
}
}
function min(a: number, b: number): number {
return (a < b ? a : b);
}
export function createUint32Array(arr: number[]): Uint32Array {
let r = new Uint32Array(arr.length);
r.set(arr, 0);
return r;
}
export class LineStarts {
constructor(
public readonly lineStarts: Uint32Array,
public readonly cr: number,
public readonly lf: number,
public readonly crlf: number,
public readonly isBasicASCII: boolean
) { }
}
export function createLineStartsFast(str: string): Uint32Array {
let r: number[] = [], rLength = 0;
for (let i = 0, len = str.length; i < len; i++) {
const chr = str.charCodeAt(i);
if (chr === CharCode.CarriageReturn) {
if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) {
// \r\n... case
r[rLength++] = i + 2;
i++; // skip \n
} else {
// \r... case
r[rLength++] = i + 1;
}
} else if (chr === CharCode.LineFeed) {
r[rLength++] = i + 1;
}
}
return createUint32Array(r);
}
export function createLineStarts(r: number[], str: string): LineStarts {
r.length = 0;
let rLength = 0;
let cr = 0, lf = 0, crlf = 0;
let isBasicASCII = true;
for (let i = 0, len = str.length; i < len; i++) {
const chr = str.charCodeAt(i);
if (chr === CharCode.CarriageReturn) {
if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) {
// \r\n... case
crlf++;
r[rLength++] = i + 2;
i++; // skip \n
} else {
cr++;
// \r... case
r[rLength++] = i + 1;
}
} else if (chr === CharCode.LineFeed) {
lf++;
r[rLength++] = i + 1;
} else {
if (isBasicASCII) {
if (chr !== CharCode.Tab && (chr < 32 || chr > 126)) {
isBasicASCII = false;
}
}
}
}
const result = new LineStarts(createUint32Array(r), cr, lf, crlf, isBasicASCII);
r.length = 0;
return result;
}
/*---------------------------------------------------------------------------------------------
* 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 { CharCode } from 'vs/base/common/charCode';
import { ITextBuffer, EndOfLinePreference, IIdentifiedSingleEditOperation, ApplyEditsResult, ISingleEditOperationIdentifier, IInternalModelContentChange } from 'vs/editor/common/model';
import { BufferPiece, LeafOffsetLenEdit } from 'vs/editor/common/model/chunksTextBuffer/bufferPiece';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import * as strings from 'vs/base/common/strings';
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;
}
export class ChunksTextBuffer implements ITextBuffer {
private _BOM: string;
private _actual: Buffer;
private _mightContainRTL: boolean;
private _mightContainNonBasicASCII: boolean;
constructor(pieces: BufferPiece[], _averageChunkSize: number, BOM: string, eol: '\r\n' | '\n', containsRTL: boolean, isBasicASCII: boolean) {
this._BOM = BOM;
const averageChunkSize = Math.floor(Math.min(65536.0, Math.max(128.0, _averageChunkSize)));
const delta = Math.floor(averageChunkSize / 3);
const min = averageChunkSize - delta;
const max = 2 * min;
this._actual = new Buffer(pieces, min, max, eol);
this._mightContainRTL = containsRTL;
this._mightContainNonBasicASCII = !isBasicASCII;
}
equals(other: ITextBuffer): boolean {
if (!(other instanceof ChunksTextBuffer)) {
return false;
}
return this._actual.equals(other._actual);
}
mightContainRTL(): boolean {
return this._mightContainRTL;
}
mightContainNonBasicASCII(): boolean {
return this._mightContainNonBasicASCII;
}
getBOM(): string {
return this._BOM;
}
getEOL(): string {
return this._actual.getEOL();
}
getOffsetAt(lineNumber: number, column: number): number {
return this._actual.convertPositionToOffset(lineNumber, column);
}
getPositionAt(offset: number): Position {
return this._actual.convertOffsetToPosition(offset);
}
getRangeAt(offset: number, length: number): Range {
return this._actual.convertOffsetLenToRange(offset, length);
}
getValueInRange(range: Range, eol: EndOfLinePreference): string {
if (range.isEmpty()) {
return '';
}
const text = this._actual.getValueInRange(range);
switch (eol) {
case EndOfLinePreference.TextDefined:
return text;
case EndOfLinePreference.LF:
if (this.getEOL() === '\n') {
return text;
} else {
return text.replace(/\r\n/g, '\n');
}
case EndOfLinePreference.CRLF:
if (this.getEOL() === '\r\n') {
return text;
} else {
return text.replace(/\n/g, '\r\n');
}
}
return null;
}
public createSnapshot(preserveBOM: boolean): ITextSnapshot {
return this._actual.createSnapshot(preserveBOM ? this._BOM : '');
}
getValueLengthInRange(range: Range, eol: EndOfLinePreference): number {
if (range.isEmpty()) {
return 0;
}
const eolCount = range.endLineNumber - range.startLineNumber;
const result = this._actual.getValueLengthInRange(range);
switch (eol) {
case EndOfLinePreference.TextDefined:
return result;
case EndOfLinePreference.LF:
if (this.getEOL() === '\n') {
return result;
} else {
return result - eolCount; // \r\n => \n
}
case EndOfLinePreference.CRLF:
if (this.getEOL() === '\r\n') {
return result;
} else {
return result + eolCount; // \n => \r\n
}
}
return 0;
}
public getLength(): number {
return this._actual.getLength();
}
getLineCount(): number {
return this._actual.getLineCount();
}
getLinesContent(): string[] {
return this._actual.getLinesContent();
}
getLineContent(lineNumber: number): string {
return this._actual.getLineContent(lineNumber);
}
getLineCharCode(lineNumber: number, index: number): number {
return this._actual.getLineCharCode(lineNumber, index);
}
getLineLength(lineNumber: number): number {
return this._actual.getLineLength(lineNumber);
}
getLineFirstNonWhitespaceColumn(lineNumber: number): number {
const result = this._actual.getLineFirstNonWhitespaceIndex(lineNumber);
if (result === -1) {
return 0;
}
return result + 1;
}
getLineLastNonWhitespaceColumn(lineNumber: number): number {
const result = this._actual.getLineLastNonWhitespaceIndex(lineNumber);
if (result === -1) {
return 0;
}
return result + 1;
}
setEOL(newEOL: '\r\n' | '\n'): void {
if (this.getEOL() === newEOL) {
// nothing to do...
return;
}
this._actual.setEOL(newEOL);
}
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;
}
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(ChunksTextBuffer._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 = ChunksTextBuffer._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.getLineContent(lineNumber).substring(lastEndColumn - 1));
} else {
result.push('\n');
result.push(this.getLineContent(lineNumber));
}
}
if (range.startLineNumber === lastEndLineNumber) {
result.push(this.getLineContent(range.startLineNumber).substring(lastEndColumn - 1, range.startColumn - 1));
} else {
result.push('\n');
result.push(this.getLineContent(range.startLineNumber).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 _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] {
// Sort operations descending
operations.sort(ChunksTextBuffer._sortOpsDescending);
let contentChanges: IInternalModelContentChange[] = [];
let edits: OffsetLenEdit[] = [];
for (let i = 0, len = operations.length; i < len; i++) {
const op = operations[i];
const text = (op.lines ? op.lines.join(this.getEOL()) : '');
edits[i] = new OffsetLenEdit(op.sortIndex, op.rangeOffset, op.rangeLength, text);
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;
}
contentChanges.push({
range: op.range,
rangeLength: op.rangeLength,
text: text,
rangeOffset: op.rangeOffset,
forceMoveMarkers: op.forceMoveMarkers
});
}
this._actual.replaceOffsetLen(edits);
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;
}
}
class BufferNodes {
public length: Uint32Array;
public newLineCount: Uint32Array;
constructor(count: number) {
this.length = new Uint32Array(count);
this.newLineCount = new Uint32Array(count);
}
}
class BufferCursor {
constructor(
public offset: number,
public leafIndex: number,
public leafStartOffset: number,
public leafStartNewLineCount: number
) { }
public set(offset: number, leafIndex: number, leafStartOffset: number, leafStartNewLineCount: number) {
this.offset = offset;
this.leafIndex = leafIndex;
this.leafStartOffset = leafStartOffset;
this.leafStartNewLineCount = leafStartNewLineCount;
}
}
class OffsetLenEdit {
constructor(
public readonly initialIndex: number,
public readonly offset: number,
public length: number,
public text: string
) { }
}
class InternalOffsetLenEdit {
constructor(
public readonly startLeafIndex: number,
public readonly startInnerOffset: number,
public readonly endLeafIndex: number,
public readonly endInnerOffset: number,
public text: string
) { }
}
class LeafReplacement {
constructor(
public readonly startLeafIndex: number,
public readonly endLeafIndex: number,
public readonly replacements: BufferPiece[]
) { }
}
const BUFFER_CURSOR_POOL_SIZE = 10;
const BufferCursorPool = new class {
private _pool: BufferCursor[];
private _len: number;
constructor() {
this._pool = [];
for (let i = 0; i < BUFFER_CURSOR_POOL_SIZE; i++) {
this._pool[i] = new BufferCursor(0, 0, 0, 0);
}
this._len = this._pool.length;
}
public put(cursor: BufferCursor): void {
if (this._len > this._pool.length) {
// oh, well
return;
}
this._pool[this._len++] = cursor;
}
public take(): BufferCursor {
if (this._len === 0) {
// oh, well
console.log(`insufficient BufferCursor pool`);
return new BufferCursor(0, 0, 0, 0);
}
const result = this._pool[this._len - 1];
this._pool[this._len--] = null;
return result;
}
};
class BufferSnapshot implements ITextSnapshot {
private readonly _pieces: BufferPiece[];
private readonly _piecesLength: number;
private readonly _BOM: string;
private _piecesIndex: number;
constructor(pieces: BufferPiece[], BOM: string) {
this._pieces = pieces;
this._piecesLength = this._pieces.length;
this._BOM = BOM;
this._piecesIndex = 0;
}
public read(): string {
if (this._piecesIndex >= this._piecesLength) {
return null;
}
let result: string = null;
if (this._piecesIndex === 0) {
result = this._BOM + this._pieces[this._piecesIndex].text;
} else {
result = this._pieces[this._piecesIndex].text;
}
this._piecesIndex++;
return result;
}
}
class Buffer {
private _minLeafLength: number;
private _maxLeafLength: number;
private _idealLeafLength: number;
private _eol: '\r\n' | '\n';
private _eolLength: number;
private _leafs: BufferPiece[];
private _nodes: BufferNodes;
private _nodesCount: number;
private _leafsStart: number;
private _leafsEnd: number;
constructor(pieces: BufferPiece[], minLeafLength: number, maxLeafLength: number, eol: '\r\n' | '\n') {
if (!(2 * minLeafLength >= maxLeafLength)) {
throw new Error(`assertion violation`);
}
this._minLeafLength = minLeafLength;
this._maxLeafLength = maxLeafLength;
this._idealLeafLength = (minLeafLength + maxLeafLength) >>> 1;
this._eol = eol;
this._eolLength = this._eol.length;
this._leafs = pieces;
this._nodes = null;
this._nodesCount = 0;
this._leafsStart = 0;
this._leafsEnd = 0;
this._rebuildNodes();
}
equals(other: Buffer): boolean {
return Buffer.equals(this, other);
}
private static equals(a: Buffer, b: Buffer): boolean {
const aLength = a.getLength();
const bLength = b.getLength();
if (aLength !== bLength) {
return false;
}
if (a.getLineCount() !== b.getLineCount()) {
return false;
}
let remaining = aLength;
let aLeafIndex = -1, aLeaf = null, aLeafLength = 0, aLeafRemaining = 0;
let bLeafIndex = -1, bLeaf = null, bLeafLength = 0, bLeafRemaining = 0;
while (remaining > 0) {
if (aLeafRemaining === 0) {
aLeafIndex++;
aLeaf = a._leafs[aLeafIndex];
aLeafLength = aLeaf.length();
aLeafRemaining = aLeafLength;
}
if (bLeafRemaining === 0) {
bLeafIndex++;
bLeaf = b._leafs[bLeafIndex];
bLeafLength = bLeaf.length();
bLeafRemaining = bLeafLength;
}
let consuming = Math.min(aLeafRemaining, bLeafRemaining);
let aStr = aLeaf.substr(aLeafLength - aLeafRemaining, consuming);
let bStr = bLeaf.substr(bLeafLength - bLeafRemaining, consuming);
if (aStr !== bStr) {
return false;
}
remaining -= consuming;
aLeafRemaining -= consuming;
bLeafRemaining -= consuming;
}
return true;
}
public getEOL(): string {
return this._eol;
}
private _rebuildNodes() {
const leafsCount = this._leafs.length;
this._nodesCount = (1 << log2(leafsCount));
this._leafsStart = this._nodesCount;
this._leafsEnd = this._leafsStart + leafsCount;
this._nodes = new BufferNodes(this._nodesCount);
for (let i = this._nodesCount - 1; i >= 1; i--) {
this._updateSingleNode(i);
}
}
private _updateSingleNode(nodeIndex: number): void {
const left = LEFT_CHILD(nodeIndex);
const right = RIGHT_CHILD(nodeIndex);
let length = 0;
let newLineCount = 0;
if (this.IS_NODE(left)) {
length += this._nodes.length[left];
newLineCount += this._nodes.newLineCount[left];
} else if (this.IS_LEAF(left)) {
const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(left)];
length += leaf.length();
newLineCount += leaf.newLineCount();
}
if (this.IS_NODE(right)) {
length += this._nodes.length[right];
newLineCount += this._nodes.newLineCount[right];
} else if (this.IS_LEAF(right)) {
const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(right)];
length += leaf.length();
newLineCount += leaf.newLineCount();
}
this._nodes.length[nodeIndex] = length;
this._nodes.newLineCount[nodeIndex] = newLineCount;
}
private _findOffset(offset: number, result: BufferCursor): boolean {
if (offset > this._nodes.length[1]) {
return false;
}
let it = 1;
let searchOffset = offset;
let leafStartOffset = 0;
let leafStartNewLineCount = 0;
while (!this.IS_LEAF(it)) {
const left = LEFT_CHILD(it);
const right = RIGHT_CHILD(it);
let leftNewLineCount = 0;
let leftLength = 0;
if (this.IS_NODE(left)) {
leftNewLineCount = this._nodes.newLineCount[left];
leftLength = this._nodes.length[left];
} else if (this.IS_LEAF(left)) {
const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(left)];
leftNewLineCount = leaf.newLineCount();
leftLength = leaf.length();
}
let rightLength = 0;
if (this.IS_NODE(right)) {
rightLength += this._nodes.length[right];
} else if (this.IS_LEAF(right)) {
rightLength += this._leafs[this.NODE_TO_LEAF_INDEX(right)].length();
}
if (searchOffset < leftLength || rightLength === 0) {
// go left
it = left;
} else {
// go right
searchOffset -= leftLength;
leafStartOffset += leftLength;
leafStartNewLineCount += leftNewLineCount;
it = right;
}
}
it = this.NODE_TO_LEAF_INDEX(it);
result.set(offset, it, leafStartOffset, leafStartNewLineCount);
return true;
}
private _findOffsetCloseAfter(offset: number, start: BufferCursor, result: BufferCursor): boolean {
if (offset > this._nodes.length[1]) {
return false;
}
let innerOffset = offset - start.leafStartOffset;
const leafsCount = this._leafs.length;
let leafIndex = start.leafIndex;
let leafStartOffset = start.leafStartOffset;
let leafStartNewLineCount = start.leafStartNewLineCount;
while (true) {
const leaf = this._leafs[leafIndex];
if (innerOffset < leaf.length() || (innerOffset === leaf.length() && leafIndex + 1 === leafsCount)) {
result.set(offset, leafIndex, leafStartOffset, leafStartNewLineCount);
return true;
}
leafIndex++;
if (leafIndex >= leafsCount) {
result.set(offset, leafIndex, leafStartOffset, leafStartNewLineCount);
return true;
}
leafStartOffset += leaf.length();
leafStartNewLineCount += leaf.newLineCount();
innerOffset -= leaf.length();
}
}
private _findLineStart(lineNumber: number, result: BufferCursor): boolean {
let lineIndex = lineNumber - 1;
if (lineIndex < 0 || lineIndex > this._nodes.newLineCount[1]) {
result.set(0, 0, 0, 0);
return false;
}
let it = 1;
let leafStartOffset = 0;
let leafStartNewLineCount = 0;
while (!this.IS_LEAF(it)) {
const left = LEFT_CHILD(it);
const right = RIGHT_CHILD(it);
let leftNewLineCount = 0;
let leftLength = 0;
if (this.IS_NODE(left)) {
leftNewLineCount = this._nodes.newLineCount[left];
leftLength = this._nodes.length[left];
} else if (this.IS_LEAF(left)) {
const leaf = this._leafs[this.NODE_TO_LEAF_INDEX(left)];
leftNewLineCount = leaf.newLineCount();
leftLength = leaf.length();
}
if (lineIndex <= leftNewLineCount) {
// go left
it = left;
continue;
}
// go right
lineIndex -= leftNewLineCount;
leafStartOffset += leftLength;
leafStartNewLineCount += leftNewLineCount;
it = right;
}
it = this.NODE_TO_LEAF_INDEX(it);
const innerLineStartOffset = (lineIndex === 0 ? 0 : this._leafs[it].lineStartFor(lineIndex - 1));
result.set(leafStartOffset + innerLineStartOffset, it, leafStartOffset, leafStartNewLineCount);
return true;
}
private _findLineEnd(start: BufferCursor, lineNumber: number, result: BufferCursor): void {
let innerLineIndex = lineNumber - 1 - start.leafStartNewLineCount;
const leafsCount = this._leafs.length;
let leafIndex = start.leafIndex;
let leafStartOffset = start.leafStartOffset;
let leafStartNewLineCount = start.leafStartNewLineCount;
while (true) {
const leaf = this._leafs[leafIndex];
if (innerLineIndex < leaf.newLineCount()) {
const lineEndOffset = this._leafs[leafIndex].lineStartFor(innerLineIndex);
result.set(leafStartOffset + lineEndOffset, leafIndex, leafStartOffset, leafStartNewLineCount);
return;
}
leafIndex++;
if (leafIndex >= leafsCount) {
result.set(leafStartOffset + leaf.length(), leafIndex - 1, leafStartOffset, leafStartNewLineCount);
return;
}
leafStartOffset += leaf.length();
leafStartNewLineCount += leaf.newLineCount();
innerLineIndex = 0;
}
}
private _findLine(lineNumber: number, start: BufferCursor, end: BufferCursor): boolean {
if (!this._findLineStart(lineNumber, start)) {
return false;
}
this._findLineEnd(start, lineNumber, end);
return true;
}
public getLength(): number {
return this._nodes.length[1];
}
public getLineCount(): number {
return this._nodes.newLineCount[1] + 1;
}
public getLineContent(lineNumber: number): string {
const start = BufferCursorPool.take();
const end = BufferCursorPool.take();
if (!this._findLine(lineNumber, start, end)) {
BufferCursorPool.put(start);
BufferCursorPool.put(end);
throw new Error(`Line not found`);
}
let result: string;
if (lineNumber === this.getLineCount()) {
// last line is not trailed by an eol
result = this.extractString(start, end.offset - start.offset);
} else {
result = this.extractString(start, end.offset - start.offset - this._eolLength);
}
BufferCursorPool.put(start);
BufferCursorPool.put(end);
return result;
}
public getLineCharCode(lineNumber: number, index: number): number {
const start = BufferCursorPool.take();
if (!this._findLineStart(lineNumber, start)) {
BufferCursorPool.put(start);
throw new Error(`Line not found`);
}
const tmp = BufferCursorPool.take();
this._findOffsetCloseAfter(start.offset + index, start, tmp);
const result = this._leafs[tmp.leafIndex].charCodeAt(tmp.offset - tmp.leafStartNewLineCount);
BufferCursorPool.put(tmp);
BufferCursorPool.put(start);
return result;
}
public getLineLength(lineNumber: number): number {
const start = BufferCursorPool.take();
const end = BufferCursorPool.take();
if (!this._findLine(lineNumber, start, end)) {
BufferCursorPool.put(start);
BufferCursorPool.put(end);
throw new Error(`Line not found`);
}
let result: number;
if (lineNumber === this.getLineCount()) {
// last line is not trailed by an eol
result = end.offset - start.offset;
} else {
result = end.offset - start.offset - this._eolLength;
}
BufferCursorPool.put(start);
BufferCursorPool.put(end);
return result;
}
public getLineFirstNonWhitespaceIndex(lineNumber: number): number {
const start = BufferCursorPool.take();
if (!this._findLineStart(lineNumber, start)) {
BufferCursorPool.put(start);
throw new Error(`Line not found`);
}
let leafIndex = start.leafIndex;
let searchStartOffset = start.offset - start.leafStartOffset;
BufferCursorPool.put(start);
const leafsCount = this._leafs.length;
let totalDelta = 0;
while (true) {
const leaf = this._leafs[leafIndex];
const leafResult = leaf.findLineFirstNonWhitespaceIndex(searchStartOffset);
if (leafResult === -2) {
// reached EOL
return -1;
}
if (leafResult !== -1) {
return (leafResult - searchStartOffset) + totalDelta;
}
leafIndex++;
if (leafIndex >= leafsCount) {
return -1;
}
totalDelta += (leaf.length() - searchStartOffset);
searchStartOffset = 0;
}
}
public getLineLastNonWhitespaceIndex(lineNumber: number): number {
const start = BufferCursorPool.take();
const end = BufferCursorPool.take();
if (!this._findLineStart(lineNumber, start)) {
BufferCursorPool.put(start);
BufferCursorPool.put(end);
throw new Error(`Line not found`);
}
this._findLineEnd(start, lineNumber, end);
const startOffset = start.offset;
const endOffset = end.offset;
let leafIndex = end.leafIndex;
let searchStartOffset = end.offset - end.leafStartOffset - this._eolLength;
BufferCursorPool.put(start);
BufferCursorPool.put(end);
let totalDelta = 0;
while (true) {
const leaf = this._leafs[leafIndex];
const leafResult = leaf.findLineLastNonWhitespaceIndex(searchStartOffset);
if (leafResult === -2) {
// reached EOL
return -1;
}
if (leafResult !== -1) {
const delta = (searchStartOffset - 1 - leafResult);
const absoluteOffset = (endOffset - this._eolLength) - delta - totalDelta;
return absoluteOffset - startOffset;
}
leafIndex--;
if (leafIndex < 0) {
return -1;
}
totalDelta += searchStartOffset;
searchStartOffset = leaf.length();
}
}
public getLinesContent(): string[] {
let result: string[] = new Array<string>(this.getLineCount());
let resultIndex = 0;
let currentLine = '';
for (let leafIndex = 0, leafsCount = this._leafs.length; leafIndex < leafsCount; leafIndex++) {
const leaf = this._leafs[leafIndex];
const leafNewLineCount = leaf.newLineCount();
if (leafNewLineCount === 0) {
// special case => push entire leaf text
currentLine += leaf.text;
continue;
}
let leafSubstrOffset = 0;
for (let newLineIndex = 0; newLineIndex < leafNewLineCount; newLineIndex++) {
const newLineStart = leaf.lineStartFor(newLineIndex);
currentLine += leaf.substr(leafSubstrOffset, newLineStart - leafSubstrOffset - this._eolLength);
result[resultIndex++] = currentLine;
currentLine = '';
leafSubstrOffset = newLineStart;
}
currentLine += leaf.substr(leafSubstrOffset, leaf.length());
}
result[resultIndex++] = currentLine;
return result;
}
public extractString(start: BufferCursor, len: number): string {
if (!(start.offset + len <= this._nodes.length[1])) {
throw new Error(`assertion violation`);
}
let innerLeafOffset = start.offset - start.leafStartOffset;
let leafIndex = start.leafIndex;
let res = '';
while (len > 0) {
const leaf = this._leafs[leafIndex];
const cnt = Math.min(len, leaf.length() - innerLeafOffset);
res += leaf.substr(innerLeafOffset, cnt);
len -= cnt;
innerLeafOffset = 0;
if (len === 0) {
break;
}
leafIndex++;
}
return res;
}
private _getOffsetAt(lineNumber: number, column: number, result: BufferCursor): boolean {
const lineStart = BufferCursorPool.take();
if (!this._findLineStart(lineNumber, lineStart)) {
BufferCursorPool.put(lineStart);
return false;
}
const startOffset = lineStart.offset + column - 1;
if (!this._findOffsetCloseAfter(startOffset, lineStart, result)) {
BufferCursorPool.put(lineStart);
return false;
}
BufferCursorPool.put(lineStart);
return true;
}
public convertPositionToOffset(lineNumber: number, column: number): number {
const r = BufferCursorPool.take();
if (!this._findLineStart(lineNumber, r)) {
BufferCursorPool.put(r);
throw new Error(`Position not found`);
}
const result = r.offset + column - 1;
BufferCursorPool.put(r);
return result;
}
/**
* returns `lineNumber`
*/
private _findLineStartBeforeOffsetInLeaf(offset: number, leafIndex: number, leafStartOffset: number, leafStartNewLineCount: number, result: BufferCursor): number {
const leaf = this._leafs[leafIndex];
const lineStartIndex = leaf.findLineStartBeforeOffset(offset - leafStartOffset);
const lineStartOffset = leafStartOffset + leaf.lineStartFor(lineStartIndex);
result.set(lineStartOffset, leafIndex, leafStartOffset, leafStartNewLineCount);
return leafStartNewLineCount + lineStartIndex + 2;
}
/**
* returns `lineNumber`.
*/
private _findLineStartBeforeOffset(offset: number, location: BufferCursor, result: BufferCursor): number {
let leafIndex = location.leafIndex;
let leafStartOffset = location.leafStartOffset;
let leafStartNewLineCount = location.leafStartNewLineCount;
while (true) {
const leaf = this._leafs[leafIndex];
if (leaf.newLineCount() >= 1 && leaf.lineStartFor(0) + leafStartOffset <= offset) {
// must be in this leaf
return this._findLineStartBeforeOffsetInLeaf(offset, leafIndex, leafStartOffset, leafStartNewLineCount, result);
}
// continue looking in previous leaf
leafIndex--;
if (leafIndex < 0) {
result.set(0, 0, 0, 0);
return 1;
}
leafStartOffset -= this._leafs[leafIndex].length();
leafStartNewLineCount -= this._leafs[leafIndex].newLineCount();
}
}
public convertOffsetToPosition(offset: number): Position {
const r = BufferCursorPool.take();
const lineStart = BufferCursorPool.take();
if (!this._findOffset(offset, r)) {
BufferCursorPool.put(r);
BufferCursorPool.put(lineStart);
throw new Error(`Offset not found`);
}
const lineNumber = this._findLineStartBeforeOffset(offset, r, lineStart);
const column = offset - lineStart.offset + 1;
BufferCursorPool.put(r);
BufferCursorPool.put(lineStart);
return new Position(lineNumber, column);
}
public convertOffsetLenToRange(offset: number, len: number): Range {
const r = BufferCursorPool.take();
const lineStart = BufferCursorPool.take();
if (!this._findOffset(offset, r)) {
BufferCursorPool.put(r);
BufferCursorPool.put(lineStart);
throw new Error(`Offset not found`);
}
const startLineNumber = this._findLineStartBeforeOffset(offset, r, lineStart);
const startColumn = offset - lineStart.offset + 1;
if (!this._findOffset(offset + len, r)) {
BufferCursorPool.put(r);
BufferCursorPool.put(lineStart);
throw new Error(`Offset not found`);
}
const endLineNumber = this._findLineStartBeforeOffset(offset + len, r, lineStart);
const endColumn = offset + len - lineStart.offset + 1;
BufferCursorPool.put(r);
BufferCursorPool.put(lineStart);
return new Range(startLineNumber, startColumn, endLineNumber, endColumn);
}
public getValueInRange(range: Range): string {
const start = BufferCursorPool.take();
if (!this._getOffsetAt(range.startLineNumber, range.startColumn, start)) {
BufferCursorPool.put(start);
throw new Error(`Line not found`);
}
const endOffset = this.convertPositionToOffset(range.endLineNumber, range.endColumn);
const result = this.extractString(start, endOffset - start.offset);
BufferCursorPool.put(start);
return result;
}
public createSnapshot(BOM: string): ITextSnapshot {
return new BufferSnapshot(this._leafs, BOM);
}
public getValueLengthInRange(range: Range): number {
const startOffset = this.convertPositionToOffset(range.startLineNumber, range.startColumn);
const endOffset = this.convertPositionToOffset(range.endLineNumber, range.endColumn);
return endOffset - startOffset;
}
//#region Editing
private _mergeAdjacentEdits(edits: OffsetLenEdit[]): OffsetLenEdit[] {
// Check if we must merge adjacent edits
let merged: OffsetLenEdit[] = [], mergedLength = 0;
let prev = edits[0];
for (let i = 1, len = edits.length; i < len; i++) {
const curr = edits[i];
if (prev.offset + prev.length === curr.offset) {
// merge into `prev`
prev.length = prev.length + curr.length;
prev.text = prev.text + curr.text;
} else {
merged[mergedLength++] = prev;
prev = curr;
}
}
merged[mergedLength++] = prev;
return merged;
}
private _resolveEdits(edits: OffsetLenEdit[]): InternalOffsetLenEdit[] {
edits = this._mergeAdjacentEdits(edits);
let result: InternalOffsetLenEdit[] = [];
let tmp = new BufferCursor(0, 0, 0, 0);
let tmp2 = new BufferCursor(0, 0, 0, 0);
for (let i = 0, len = edits.length; i < len; i++) {
const edit = edits[i];
let text = edit.text;
this._findOffset(edit.offset, tmp);
let startLeafIndex = tmp.leafIndex;
let startInnerOffset = tmp.offset - tmp.leafStartOffset;
if (startInnerOffset > 0) {
const startLeaf = this._leafs[startLeafIndex];
const charBefore = startLeaf.charCodeAt(startInnerOffset - 1);
if (charBefore === CharCode.CarriageReturn) {
// include the replacement of \r in the edit
text = '\r' + text;
this._findOffsetCloseAfter(edit.offset - 1, tmp, tmp2);
startLeafIndex = tmp2.leafIndex;
startInnerOffset = tmp2.offset - tmp2.leafStartOffset;
// this._findOffset(edit.offset - 1, tmp);
// startLeafIndex = tmp.leafIndex;
// startInnerOffset = tmp.offset - tmp.leafStartOffset;
}
}
this._findOffset(edit.offset + edit.length, tmp);
let endLeafIndex = tmp.leafIndex;
let endInnerOffset = tmp.offset - tmp.leafStartOffset;
const endLeaf = this._leafs[endLeafIndex];
if (endInnerOffset < endLeaf.length()) {
const charAfter = endLeaf.charCodeAt(endInnerOffset);
if (charAfter === CharCode.LineFeed) {
// include the replacement of \n in the edit
text = text + '\n';
this._findOffsetCloseAfter(edit.offset + edit.length + 1, tmp, tmp2);
endLeafIndex = tmp2.leafIndex;
endInnerOffset = tmp2.offset - tmp2.leafStartOffset;
// this._findOffset(edit.offset + edit.length + 1, tmp);
// endLeafIndex = tmp.leafIndex;
// endInnerOffset = tmp.offset - tmp.leafStartOffset;
}
}
result[i] = new InternalOffsetLenEdit(
startLeafIndex, startInnerOffset,
endLeafIndex, endInnerOffset,
text
);
}
return result;
}
private _pushLeafReplacement(startLeafIndex: number, endLeafIndex: number, replacements: LeafReplacement[]): LeafReplacement {
const res = new LeafReplacement(startLeafIndex, endLeafIndex, []);
replacements.push(res);
return res;
}
private _flushLeafEdits(accumulatedLeafIndex: number, accumulatedLeafEdits: LeafOffsetLenEdit[], replacements: LeafReplacement[]): void {
if (accumulatedLeafEdits.length > 0) {
const rep = this._pushLeafReplacement(accumulatedLeafIndex, accumulatedLeafIndex, replacements);
BufferPiece.replaceOffsetLen(this._leafs[accumulatedLeafIndex], accumulatedLeafEdits, this._idealLeafLength, this._maxLeafLength, rep.replacements);
}
accumulatedLeafEdits.length = 0;
}
private _pushLeafEdits(start: number, length: number, text: string, accumulatedLeafEdits: LeafOffsetLenEdit[]): void {
if (length !== 0 || text.length !== 0) {
accumulatedLeafEdits.push(new LeafOffsetLenEdit(start, length, text));
}
}
private _appendLeaf(leaf: BufferPiece, leafs: BufferPiece[], prevLeaf: BufferPiece): BufferPiece {
if (prevLeaf === null) {
leafs.push(leaf);
prevLeaf = leaf;
return prevLeaf;
}
let prevLeafLength = prevLeaf.length();
let currLeafLength = leaf.length();
if ((prevLeafLength < this._minLeafLength || currLeafLength < this._minLeafLength) && prevLeafLength + currLeafLength <= this._maxLeafLength) {
const joinedLeaf = BufferPiece.join(prevLeaf, leaf);
leafs[leafs.length - 1] = joinedLeaf;
prevLeaf = joinedLeaf;
return prevLeaf;
}
const lastChar = prevLeaf.charCodeAt(prevLeafLength - 1);
const firstChar = leaf.charCodeAt(0);
if (
(lastChar >= 0xd800 && lastChar <= 0xdbff) || (lastChar === CharCode.CarriageReturn && firstChar === CharCode.LineFeed)
) {
const modifiedPrevLeaf = BufferPiece.deleteLastChar(prevLeaf);
leafs[leafs.length - 1] = modifiedPrevLeaf;
const modifiedLeaf = BufferPiece.insertFirstChar(leaf, lastChar);
leaf = modifiedLeaf;
}
leafs.push(leaf);
prevLeaf = leaf;
return prevLeaf;
}
private static _compareEdits(a: OffsetLenEdit, b: OffsetLenEdit): number {
if (a.offset === b.offset) {
if (a.length === b.length) {
return (a.initialIndex - b.initialIndex);
}
return (a.length - b.length);
}
return a.offset - b.offset;
}
public replaceOffsetLen(_edits: OffsetLenEdit[]): void {
_edits.sort(Buffer._compareEdits);
const initialLeafLength = this._leafs.length;
const edits = this._resolveEdits(_edits);
let accumulatedLeafIndex = 0;
let accumulatedLeafEdits: LeafOffsetLenEdit[] = [];
let replacements: LeafReplacement[] = [];
for (let i = 0, len = edits.length; i < len; i++) {
const edit = edits[i];
const startLeafIndex = edit.startLeafIndex;
const endLeafIndex = edit.endLeafIndex;
if (startLeafIndex !== accumulatedLeafIndex) {
this._flushLeafEdits(accumulatedLeafIndex, accumulatedLeafEdits, replacements);
accumulatedLeafIndex = startLeafIndex;
}
const leafEditStart = edit.startInnerOffset;
const leafEditEnd = (startLeafIndex === endLeafIndex ? edit.endInnerOffset : this._leafs[startLeafIndex].length());
this._pushLeafEdits(leafEditStart, leafEditEnd - leafEditStart, edit.text, accumulatedLeafEdits);
if (startLeafIndex < endLeafIndex) {
this._flushLeafEdits(accumulatedLeafIndex, accumulatedLeafEdits, replacements);
accumulatedLeafIndex = endLeafIndex;
// delete leafs in the middle
if (startLeafIndex + 1 < endLeafIndex) {
this._pushLeafReplacement(startLeafIndex + 1, endLeafIndex - 1, replacements);
}
// delete on last leaf
const leafEditStart = 0;
const leafEditEnd = edit.endInnerOffset;
this._pushLeafEdits(leafEditStart, leafEditEnd - leafEditStart, '', accumulatedLeafEdits);
}
}
this._flushLeafEdits(accumulatedLeafIndex, accumulatedLeafEdits, replacements);
let leafs: BufferPiece[] = [];
let leafIndex = 0;
let prevLeaf: BufferPiece = null;
for (let i = 0, len = replacements.length; i < len; i++) {
const replaceStartLeafIndex = replacements[i].startLeafIndex;
const replaceEndLeafIndex = replacements[i].endLeafIndex;
const innerLeafs = replacements[i].replacements;
// add leafs to the left of this replace op.
while (leafIndex < replaceStartLeafIndex) {
prevLeaf = this._appendLeaf(this._leafs[leafIndex], leafs, prevLeaf);
leafIndex++;
}
// delete leafs that get replaced.
while (leafIndex <= replaceEndLeafIndex) {
leafIndex++;
}
// add new leafs.
for (let j = 0, lenJ = innerLeafs.length; j < lenJ; j++) {
prevLeaf = this._appendLeaf(innerLeafs[j], leafs, prevLeaf);
}
}
// add remaining leafs to the right of the last replacement.
while (leafIndex < initialLeafLength) {
prevLeaf = this._appendLeaf(this._leafs[leafIndex], leafs, prevLeaf);
leafIndex++;
}
if (leafs.length === 0) {
// don't leave behind an empty leafs array
leafs.push(new BufferPiece(''));
}
this._leafs = leafs;
this._rebuildNodes();
}
public setEOL(newEOL: '\r\n' | '\n'): void {
let leafs: BufferPiece[] = [];
for (let i = 0, len = this._leafs.length; i < len; i++) {
leafs[i] = BufferPiece.normalizeEOL(this._leafs[i], newEOL);
}
this._leafs = leafs;
this._rebuildNodes();
this._eol = newEOL;
this._eolLength = this._eol.length;
}
//#endregion
private IS_NODE(i: number): boolean {
return (i < this._nodesCount);
}
private IS_LEAF(i: number): boolean {
return (i >= this._leafsStart && i < this._leafsEnd);
}
private NODE_TO_LEAF_INDEX(i: number): number {
return (i - this._leafsStart);
}
// private LEAF_TO_NODE_INDEX(i: number): number {
// return (i + this._leafsStart);
// }
}
function log2(n: number): number {
let v = 1;
for (let pow = 1; ; pow++) {
v = v << 1;
if (v >= n) {
return pow;
}
}
// return -1;
}
function LEFT_CHILD(i: number): number {
return (i << 1);
}
function RIGHT_CHILD(i: number): number {
return (i << 1) + 1;
}
/*---------------------------------------------------------------------------------------------
* 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 { ITextBufferBuilder, ITextBufferFactory, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model';
import { BufferPiece, createLineStarts } from 'vs/editor/common/model/chunksTextBuffer/bufferPiece';
import { ChunksTextBuffer } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBuffer';
import { CharCode } from 'vs/base/common/charCode';
export class TextBufferFactory implements ITextBufferFactory {
constructor(
private readonly _pieces: BufferPiece[],
private readonly _averageChunkSize: number,
private readonly _BOM: string,
private readonly _cr: number,
private readonly _lf: number,
private readonly _crlf: number,
private readonly _containsRTL: boolean,
private readonly _isBasicASCII: boolean,
) {
}
/**
* 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 _getEOL(defaultEOL: DefaultEndOfLine): '\r\n' | '\n' {
const totalEOLCount = this._cr + this._lf + this._crlf;
const totalCRCount = this._cr + this._crlf;
if (totalEOLCount === 0) {
// This is an empty file or a file with precisely one line
return (defaultEOL === DefaultEndOfLine.LF ? '\n' : '\r\n');
}
if (totalCRCount > totalEOLCount / 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 create(defaultEOL: DefaultEndOfLine): ITextBuffer {
const eol = this._getEOL(defaultEOL);
let pieces = this._pieces;
if (
(eol === '\r\n' && (this._cr > 0 || this._lf > 0))
|| (eol === '\n' && (this._cr > 0 || this._crlf > 0))
) {
// Normalize pieces
for (let i = 0, len = pieces.length; i < len; i++) {
pieces[i] = BufferPiece.normalizeEOL(pieces[i], eol);
}
}
return new ChunksTextBuffer(pieces, this._averageChunkSize, this._BOM, eol, this._containsRTL, this._isBasicASCII);
}
public getFirstLineText(lengthLimit: number): string {
const firstPiece = this._pieces[0];
if (firstPiece.newLineCount() === 0) {
return firstPiece.substr(0, lengthLimit);
}
const firstEOLOffset = firstPiece.lineStartFor(0);
return firstPiece.substr(0, Math.min(lengthLimit, firstEOLOffset));
}
}
export class ChunksTextBufferBuilder implements ITextBufferBuilder {
private _rawPieces: BufferPiece[];
private _hasPreviousChar: boolean;
private _previousChar: number;
private _averageChunkSize: number;
private _tmpLineStarts: number[];
private BOM: string;
private cr: number;
private lf: number;
private crlf: number;
private containsRTL: boolean;
private isBasicASCII: boolean;
constructor() {
this._rawPieces = [];
this._hasPreviousChar = false;
this._previousChar = 0;
this._averageChunkSize = 0;
this._tmpLineStarts = [];
this.BOM = '';
this.cr = 0;
this.lf = 0;
this.crlf = 0;
this.containsRTL = false;
this.isBasicASCII = true;
}
public acceptChunk(chunk: string): void {
if (chunk.length === 0) {
return;
}
if (this._rawPieces.length === 0) {
if (strings.startsWithUTF8BOM(chunk)) {
this.BOM = strings.UTF8_BOM_CHARACTER;
chunk = chunk.substr(1);
}
}
this._averageChunkSize = (this._averageChunkSize * this._rawPieces.length + chunk.length) / (this._rawPieces.length + 1);
const lastChar = chunk.charCodeAt(chunk.length - 1);
if (lastChar === CharCode.CarriageReturn || (lastChar >= 0xd800 && lastChar <= 0xdbff)) {
// last character is \r or a high surrogate => keep it back
this._acceptChunk1(chunk.substr(0, chunk.length - 1), false);
this._hasPreviousChar = true;
this._previousChar = lastChar;
} else {
this._acceptChunk1(chunk, false);
this._hasPreviousChar = false;
this._previousChar = lastChar;
}
}
private _acceptChunk1(chunk: string, allowEmptyStrings: boolean): void {
if (!allowEmptyStrings && chunk.length === 0) {
// Nothing to do
return;
}
if (this._hasPreviousChar) {
this._acceptChunk2(chunk + String.fromCharCode(this._previousChar));
} else {
this._acceptChunk2(chunk);
}
}
private _acceptChunk2(chunk: string): void {
const lineStarts = createLineStarts(this._tmpLineStarts, chunk);
this._rawPieces.push(new BufferPiece(chunk, lineStarts.lineStarts));
this.cr += lineStarts.cr;
this.lf += lineStarts.lf;
this.crlf += lineStarts.crlf;
if (this.isBasicASCII) {
this.isBasicASCII = lineStarts.isBasicASCII;
}
if (!this.isBasicASCII && !this.containsRTL) {
// No need to check if is basic ASCII
this.containsRTL = strings.containsRTL(chunk);
}
}
public finish(): TextBufferFactory {
this._finish();
return new TextBufferFactory(this._rawPieces, this._averageChunkSize, this.BOM, this.cr, this.lf, this.crlf, this.containsRTL, this.isBasicASCII);
}
private _finish(): void {
if (this._rawPieces.length === 0) {
// no chunks => forcefully go through accept chunk
this._acceptChunk1('', true);
return;
}
if (this._hasPreviousChar) {
this._hasPreviousChar = false;
// recreate last chunk
const lastPiece = this._rawPieces[this._rawPieces.length - 1];
const tmp = new BufferPiece(String.fromCharCode(this._previousChar));
const newLastPiece = BufferPiece.join(lastPiece, tmp);
this._rawPieces[this._rawPieces.length - 1] = newLastPiece;
if (this._previousChar === CharCode.CarriageReturn) {
this.cr++;
}
}
}
}
......@@ -35,12 +35,10 @@ 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';
import { ChunksTextBufferBuilder } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder';
export enum TextBufferType {
LinesArray,
PieceTree,
Chunks
PieceTree
}
// Here is the master switch for the text buffer implementation:
export const OPTIONS = {
......@@ -51,9 +49,6 @@ function createTextBufferBuilder() {
if (OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) {
return new PieceTreeTextBufferBuilder();
}
if (OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.Chunks) {
return new ChunksTextBufferBuilder();
}
return new LinesTextBufferBuilder();
}
......
......@@ -6,7 +6,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';
import { ChunksTextBufferBuilder } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder';
export function doBenchmark<T>(id: string, ts: T[], fn: (t: T) => void) {
let columns: string[] = [id];
......@@ -57,7 +56,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(), new ChunksTextBufferBuilder()].forEach((builder: ITextBufferBuilder) => {
[new LinesTextBufferBuilder(), new PieceTreeTextBufferBuilder()].forEach((builder: ITextBufferBuilder) => {
let timeDiffTotal = 0.0;
for (let j = 0; j < this.iterations; j++) {
let factory = benchmark.buildBuffer(builder);
......
/*---------------------------------------------------------------------------------------------
* 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 assert from 'assert';
import { BufferPiece } from 'vs/editor/common/model/chunksTextBuffer/bufferPiece';
suite('BufferPiece', () => {
test('findLineStartBeforeOffset', () => {
let piece = new BufferPiece([
'Line1\r\n',
'l2\n',
'another\r',
'and\r\n',
'finally\n',
'last'
].join(''));
assert.equal(piece.length(), 35);
assert.deepEqual(piece.findLineStartBeforeOffset(0), -1);
assert.deepEqual(piece.findLineStartBeforeOffset(1), -1);
assert.deepEqual(piece.findLineStartBeforeOffset(2), -1);
assert.deepEqual(piece.findLineStartBeforeOffset(3), -1);
assert.deepEqual(piece.findLineStartBeforeOffset(4), -1);
assert.deepEqual(piece.findLineStartBeforeOffset(5), -1);
assert.deepEqual(piece.findLineStartBeforeOffset(6), -1);
assert.deepEqual(piece.findLineStartBeforeOffset(7), 0);
assert.deepEqual(piece.findLineStartBeforeOffset(8), 0);
assert.deepEqual(piece.findLineStartBeforeOffset(9), 0);
assert.deepEqual(piece.findLineStartBeforeOffset(10), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(11), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(12), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(13), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(14), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(15), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(16), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(17), 1);
assert.deepEqual(piece.findLineStartBeforeOffset(18), 2);
assert.deepEqual(piece.findLineStartBeforeOffset(19), 2);
assert.deepEqual(piece.findLineStartBeforeOffset(20), 2);
assert.deepEqual(piece.findLineStartBeforeOffset(21), 2);
assert.deepEqual(piece.findLineStartBeforeOffset(22), 2);
assert.deepEqual(piece.findLineStartBeforeOffset(23), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(24), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(25), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(26), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(27), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(28), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(29), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(30), 3);
assert.deepEqual(piece.findLineStartBeforeOffset(31), 4);
assert.deepEqual(piece.findLineStartBeforeOffset(32), 4);
assert.deepEqual(piece.findLineStartBeforeOffset(33), 4);
assert.deepEqual(piece.findLineStartBeforeOffset(34), 4);
assert.deepEqual(piece.findLineStartBeforeOffset(35), 4);
assert.deepEqual(piece.findLineStartBeforeOffset(36), 4);
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册