diff --git a/src/vs/editor/common/model/chunksTextBuffer/bufferPiece.ts b/src/vs/editor/common/model/chunksTextBuffer/bufferPiece.ts new file mode 100644 index 00000000000..53063409aea --- /dev/null +++ b/src/vs/editor/common/model/chunksTextBuffer/bufferPiece.ts @@ -0,0 +1,323 @@ +/*--------------------------------------------------------------------------------------------- + * 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(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; +} diff --git a/src/vs/editor/common/model/chunksTextBuffer/chunksTextBuffer.ts b/src/vs/editor/common/model/chunksTextBuffer/chunksTextBuffer.ts new file mode 100644 index 00000000000..d089cd00af0 --- /dev/null +++ b/src/vs/editor/common/model/chunksTextBuffer/chunksTextBuffer.ts @@ -0,0 +1,1527 @@ +/*--------------------------------------------------------------------------------------------- + * 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, + lines: op.lines, + 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(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; +} diff --git a/src/vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder.ts b/src/vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder.ts new file mode 100644 index 00000000000..12eaf167b28 --- /dev/null +++ b/src/vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * 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++; + } + } + } +} diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index d0d5cd7d1fd..6506c95e3b0 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -34,9 +34,14 @@ import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelS 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 { ChunksTextBufferBuilder } from 'vs/editor/common/model/chunksTextBuffer/chunksTextBufferBuilder'; // Here is the master switch for the text buffer implementation: +const USE_CHUNKS_TEXT_BUFFER = false; function createTextBufferBuilder() { + if (USE_CHUNKS_TEXT_BUFFER) { + return new ChunksTextBufferBuilder(); + } return new LinesTextBufferBuilder(); } diff --git a/src/vs/editor/test/common/model/chunksTextBuffer/bufferPiece.test.ts b/src/vs/editor/test/common/model/chunksTextBuffer/bufferPiece.test.ts new file mode 100644 index 00000000000..a6cc8f6f22d --- /dev/null +++ b/src/vs/editor/test/common/model/chunksTextBuffer/bufferPiece.test.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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); + }); +}); diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index be2fd30bd13..a99bde0f438 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -891,16 +891,18 @@ suite('TextModel.createSnapshot', () => { let snapshot = model.createSnapshot(); let actual = ''; - // 70999 length => 2 read calls are necessary + // 70999 length => at most 2 read calls are necessary let tmp1 = snapshot.read(); assert.ok(tmp1); actual += tmp1; let tmp2 = snapshot.read(); - assert.ok(tmp2); - actual += tmp2; - - assert.equal(snapshot.read(), null); + if (tmp2 === null) { + // all good + } else { + actual += tmp2; + assert.equal(snapshot.read(), null); + } assert.equal(actual, text);