Path: blob/main/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CharCode } from '../../../../base/common/charCode.js';6import { Position } from '../../core/position.js';7import { Range } from '../../core/range.js';8import { FindMatch, ITextSnapshot, SearchData } from '../../model.js';9import { NodeColor, SENTINEL, TreeNode, fixInsert, leftest, rbDelete, righttest, updateTreeMetadata } from './rbTreeBase.js';10import { Searcher, createFindMatch, isValidMatch } from '../textModelSearch.js';1112// const lfRegex = new RegExp(/\r\n|\r|\n/g);13const AverageBufferSize = 65535;1415function createUintArray(arr: number[]): Uint32Array | Uint16Array {16let r;17if (arr[arr.length - 1] < 65536) {18r = new Uint16Array(arr.length);19} else {20r = new Uint32Array(arr.length);21}22r.set(arr, 0);23return r;24}2526class LineStarts {27constructor(28public readonly lineStarts: Uint32Array | Uint16Array | number[],29public readonly cr: number,30public readonly lf: number,31public readonly crlf: number,32public readonly isBasicASCII: boolean33) { }34}3536export function createLineStartsFast(str: string, readonly: boolean = true): Uint32Array | Uint16Array | number[] {37const r: number[] = [0];38let rLength = 1;3940for (let i = 0, len = str.length; i < len; i++) {41const chr = str.charCodeAt(i);4243if (chr === CharCode.CarriageReturn) {44if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) {45// \r\n... case46r[rLength++] = i + 2;47i++; // skip \n48} else {49// \r... case50r[rLength++] = i + 1;51}52} else if (chr === CharCode.LineFeed) {53r[rLength++] = i + 1;54}55}56if (readonly) {57return createUintArray(r);58} else {59return r;60}61}6263export function createLineStarts(r: number[], str: string): LineStarts {64r.length = 0;65r[0] = 0;66let rLength = 1;67let cr = 0, lf = 0, crlf = 0;68let isBasicASCII = true;69for (let i = 0, len = str.length; i < len; i++) {70const chr = str.charCodeAt(i);7172if (chr === CharCode.CarriageReturn) {73if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) {74// \r\n... case75crlf++;76r[rLength++] = i + 2;77i++; // skip \n78} else {79cr++;80// \r... case81r[rLength++] = i + 1;82}83} else if (chr === CharCode.LineFeed) {84lf++;85r[rLength++] = i + 1;86} else {87if (isBasicASCII) {88if (chr !== CharCode.Tab && (chr < 32 || chr > 126)) {89isBasicASCII = false;90}91}92}93}94const result = new LineStarts(createUintArray(r), cr, lf, crlf, isBasicASCII);95r.length = 0;9697return result;98}99100interface NodePosition {101/**102* Piece Index103*/104node: TreeNode;105/**106* remainder in current piece.107*/108remainder: number;109/**110* node start offset in document.111*/112nodeStartOffset: number;113}114115interface BufferCursor {116/**117* Line number in current buffer118*/119line: number;120/**121* Column number in current buffer122*/123column: number;124}125126export class Piece {127readonly bufferIndex: number;128readonly start: BufferCursor;129readonly end: BufferCursor;130readonly length: number;131readonly lineFeedCnt: number;132133constructor(bufferIndex: number, start: BufferCursor, end: BufferCursor, lineFeedCnt: number, length: number) {134this.bufferIndex = bufferIndex;135this.start = start;136this.end = end;137this.lineFeedCnt = lineFeedCnt;138this.length = length;139}140}141142export class StringBuffer {143buffer: string;144lineStarts: Uint32Array | Uint16Array | number[];145146constructor(buffer: string, lineStarts: Uint32Array | Uint16Array | number[]) {147this.buffer = buffer;148this.lineStarts = lineStarts;149}150}151152/**153* Readonly snapshot for piece tree.154* In a real multiple thread environment, to make snapshot reading always work correctly, we need to155* 1. Make TreeNode.piece immutable, then reading and writing can run in parallel.156* 2. TreeNode/Buffers normalization should not happen during snapshot reading.157*/158class PieceTreeSnapshot implements ITextSnapshot {159private readonly _pieces: Piece[];160private _index: number;161private readonly _tree: PieceTreeBase;162private readonly _BOM: string;163164constructor(tree: PieceTreeBase, BOM: string) {165this._pieces = [];166this._tree = tree;167this._BOM = BOM;168this._index = 0;169if (tree.root !== SENTINEL) {170tree.iterate(tree.root, node => {171if (node !== SENTINEL) {172this._pieces.push(node.piece);173}174return true;175});176}177}178179read(): string | null {180if (this._pieces.length === 0) {181if (this._index === 0) {182this._index++;183return this._BOM;184} else {185return null;186}187}188189if (this._index > this._pieces.length - 1) {190return null;191}192193if (this._index === 0) {194return this._BOM + this._tree.getPieceContent(this._pieces[this._index++]);195}196return this._tree.getPieceContent(this._pieces[this._index++]);197}198}199200interface CacheEntry {201node: TreeNode;202nodeStartOffset: number;203nodeStartLineNumber?: number;204}205206class PieceTreeSearchCache {207private readonly _limit: number;208private _cache: CacheEntry[];209210constructor(limit: number) {211this._limit = limit;212this._cache = [];213}214215public get(offset: number): CacheEntry | null {216for (let i = this._cache.length - 1; i >= 0; i--) {217const nodePos = this._cache[i];218if (nodePos.nodeStartOffset <= offset && nodePos.nodeStartOffset + nodePos.node.piece.length >= offset) {219return nodePos;220}221}222return null;223}224225public get2(lineNumber: number): { node: TreeNode; nodeStartOffset: number; nodeStartLineNumber: number } | null {226for (let i = this._cache.length - 1; i >= 0; i--) {227const nodePos = this._cache[i];228if (nodePos.nodeStartLineNumber && nodePos.nodeStartLineNumber < lineNumber && nodePos.nodeStartLineNumber + nodePos.node.piece.lineFeedCnt >= lineNumber) {229return <{ node: TreeNode; nodeStartOffset: number; nodeStartLineNumber: number }>nodePos;230}231}232return null;233}234235public set(nodePosition: CacheEntry) {236if (this._cache.length >= this._limit) {237this._cache.shift();238}239this._cache.push(nodePosition);240}241242public validate(offset: number) {243let hasInvalidVal = false;244const tmp: Array<CacheEntry | null> = this._cache;245for (let i = 0; i < tmp.length; i++) {246const nodePos = tmp[i]!;247if (nodePos.node.parent === null || nodePos.nodeStartOffset >= offset) {248tmp[i] = null;249hasInvalidVal = true;250continue;251}252}253254if (hasInvalidVal) {255const newArr: CacheEntry[] = [];256for (const entry of tmp) {257if (entry !== null) {258newArr.push(entry);259}260}261262this._cache = newArr;263}264}265}266267export class PieceTreeBase {268root!: TreeNode;269protected _buffers!: StringBuffer[]; // 0 is change buffer, others are readonly original buffer.270protected _lineCnt!: number;271protected _length!: number;272protected _EOL!: '\r\n' | '\n';273protected _EOLLength!: number;274protected _EOLNormalized!: boolean;275private _lastChangeBufferPos!: BufferCursor;276private _searchCache!: PieceTreeSearchCache;277private _lastVisitedLine!: { lineNumber: number; value: string };278279constructor(chunks: StringBuffer[], eol: '\r\n' | '\n', eolNormalized: boolean) {280this.create(chunks, eol, eolNormalized);281}282283create(chunks: StringBuffer[], eol: '\r\n' | '\n', eolNormalized: boolean) {284this._buffers = [285new StringBuffer('', [0])286];287this._lastChangeBufferPos = { line: 0, column: 0 };288this.root = SENTINEL;289this._lineCnt = 1;290this._length = 0;291this._EOL = eol;292this._EOLLength = eol.length;293this._EOLNormalized = eolNormalized;294295let lastNode: TreeNode | null = null;296for (let i = 0, len = chunks.length; i < len; i++) {297if (chunks[i].buffer.length > 0) {298if (!chunks[i].lineStarts) {299chunks[i].lineStarts = createLineStartsFast(chunks[i].buffer);300}301302const piece = new Piece(303i + 1,304{ line: 0, column: 0 },305{ line: chunks[i].lineStarts.length - 1, column: chunks[i].buffer.length - chunks[i].lineStarts[chunks[i].lineStarts.length - 1] },306chunks[i].lineStarts.length - 1,307chunks[i].buffer.length308);309this._buffers.push(chunks[i]);310lastNode = this.rbInsertRight(lastNode, piece);311}312}313314this._searchCache = new PieceTreeSearchCache(1);315this._lastVisitedLine = { lineNumber: 0, value: '' };316this.computeBufferMetadata();317}318319normalizeEOL(eol: '\r\n' | '\n') {320const averageBufferSize = AverageBufferSize;321const min = averageBufferSize - Math.floor(averageBufferSize / 3);322const max = min * 2;323324let tempChunk = '';325let tempChunkLen = 0;326const chunks: StringBuffer[] = [];327328this.iterate(this.root, node => {329const str = this.getNodeContent(node);330const len = str.length;331if (tempChunkLen <= min || tempChunkLen + len < max) {332tempChunk += str;333tempChunkLen += len;334return true;335}336337// flush anyways338const text = tempChunk.replace(/\r\n|\r|\n/g, eol);339chunks.push(new StringBuffer(text, createLineStartsFast(text)));340tempChunk = str;341tempChunkLen = len;342return true;343});344345if (tempChunkLen > 0) {346const text = tempChunk.replace(/\r\n|\r|\n/g, eol);347chunks.push(new StringBuffer(text, createLineStartsFast(text)));348}349350this.create(chunks, eol, true);351}352353// #region Buffer API354public getEOL(): '\r\n' | '\n' {355return this._EOL;356}357358public setEOL(newEOL: '\r\n' | '\n'): void {359this._EOL = newEOL;360this._EOLLength = this._EOL.length;361this.normalizeEOL(newEOL);362}363364public createSnapshot(BOM: string): ITextSnapshot {365return new PieceTreeSnapshot(this, BOM);366}367368public equal(other: PieceTreeBase): boolean {369if (this.getLength() !== other.getLength()) {370return false;371}372if (this.getLineCount() !== other.getLineCount()) {373return false;374}375376let offset = 0;377const ret = this.iterate(this.root, node => {378if (node === SENTINEL) {379return true;380}381const str = this.getNodeContent(node);382const len = str.length;383const startPosition = other.nodeAt(offset);384const endPosition = other.nodeAt(offset + len);385const val = other.getValueInRange2(startPosition, endPosition);386387offset += len;388return str === val;389});390391return ret;392}393394public getOffsetAt(lineNumber: number, column: number): number {395let leftLen = 0; // inorder396397let x = this.root;398399while (x !== SENTINEL) {400if (x.left !== SENTINEL && x.lf_left + 1 >= lineNumber) {401x = x.left;402} else if (x.lf_left + x.piece.lineFeedCnt + 1 >= lineNumber) {403leftLen += x.size_left;404// lineNumber >= 2405const accumualtedValInCurrentIndex = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2);406return leftLen += accumualtedValInCurrentIndex + column - 1;407} else {408lineNumber -= x.lf_left + x.piece.lineFeedCnt;409leftLen += x.size_left + x.piece.length;410x = x.right;411}412}413414return leftLen;415}416417public getPositionAt(offset: number): Position {418offset = Math.floor(offset);419offset = Math.max(0, offset);420421let x = this.root;422let lfCnt = 0;423const originalOffset = offset;424425while (x !== SENTINEL) {426if (x.size_left !== 0 && x.size_left >= offset) {427x = x.left;428} else if (x.size_left + x.piece.length >= offset) {429const out = this.getIndexOf(x, offset - x.size_left);430431lfCnt += x.lf_left + out.index;432433if (out.index === 0) {434const lineStartOffset = this.getOffsetAt(lfCnt + 1, 1);435const column = originalOffset - lineStartOffset;436return new Position(lfCnt + 1, column + 1);437}438439return new Position(lfCnt + 1, out.remainder + 1);440} else {441offset -= x.size_left + x.piece.length;442lfCnt += x.lf_left + x.piece.lineFeedCnt;443444if (x.right === SENTINEL) {445// last node446const lineStartOffset = this.getOffsetAt(lfCnt + 1, 1);447const column = originalOffset - offset - lineStartOffset;448return new Position(lfCnt + 1, column + 1);449} else {450x = x.right;451}452}453}454455return new Position(1, 1);456}457458public getValueInRange(range: Range, eol?: string): string {459if (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn) {460return '';461}462463const startPosition = this.nodeAt2(range.startLineNumber, range.startColumn);464const endPosition = this.nodeAt2(range.endLineNumber, range.endColumn);465466const value = this.getValueInRange2(startPosition, endPosition);467if (eol) {468if (eol !== this._EOL || !this._EOLNormalized) {469return value.replace(/\r\n|\r|\n/g, eol);470}471472if (eol === this.getEOL() && this._EOLNormalized) {473if (eol === '\r\n') {474475}476return value;477}478return value.replace(/\r\n|\r|\n/g, eol);479}480return value;481}482483public getValueInRange2(startPosition: NodePosition, endPosition: NodePosition): string {484if (startPosition.node === endPosition.node) {485const node = startPosition.node;486const buffer = this._buffers[node.piece.bufferIndex].buffer;487const startOffset = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start);488return buffer.substring(startOffset + startPosition.remainder, startOffset + endPosition.remainder);489}490491let x = startPosition.node;492const buffer = this._buffers[x.piece.bufferIndex].buffer;493const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start);494let ret = buffer.substring(startOffset + startPosition.remainder, startOffset + x.piece.length);495496x = x.next();497while (x !== SENTINEL) {498const buffer = this._buffers[x.piece.bufferIndex].buffer;499const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start);500501if (x === endPosition.node) {502ret += buffer.substring(startOffset, startOffset + endPosition.remainder);503break;504} else {505ret += buffer.substr(startOffset, x.piece.length);506}507508x = x.next();509}510511return ret;512}513514public getLinesContent(): string[] {515const lines: string[] = [];516let linesLength = 0;517let currentLine = '';518let danglingCR = false;519520this.iterate(this.root, node => {521if (node === SENTINEL) {522return true;523}524525const piece = node.piece;526let pieceLength = piece.length;527if (pieceLength === 0) {528return true;529}530531const buffer = this._buffers[piece.bufferIndex].buffer;532const lineStarts = this._buffers[piece.bufferIndex].lineStarts;533534const pieceStartLine = piece.start.line;535const pieceEndLine = piece.end.line;536let pieceStartOffset = lineStarts[pieceStartLine] + piece.start.column;537538if (danglingCR) {539if (buffer.charCodeAt(pieceStartOffset) === CharCode.LineFeed) {540// pretend the \n was in the previous piece..541pieceStartOffset++;542pieceLength--;543}544lines[linesLength++] = currentLine;545currentLine = '';546danglingCR = false;547if (pieceLength === 0) {548return true;549}550}551552if (pieceStartLine === pieceEndLine) {553// this piece has no new lines554if (!this._EOLNormalized && buffer.charCodeAt(pieceStartOffset + pieceLength - 1) === CharCode.CarriageReturn) {555danglingCR = true;556currentLine += buffer.substr(pieceStartOffset, pieceLength - 1);557} else {558currentLine += buffer.substr(pieceStartOffset, pieceLength);559}560return true;561}562563// add the text before the first line start in this piece564currentLine += (565this._EOLNormalized566? buffer.substring(pieceStartOffset, Math.max(pieceStartOffset, lineStarts[pieceStartLine + 1] - this._EOLLength))567: buffer.substring(pieceStartOffset, lineStarts[pieceStartLine + 1]).replace(/(\r\n|\r|\n)$/, '')568);569lines[linesLength++] = currentLine;570571for (let line = pieceStartLine + 1; line < pieceEndLine; line++) {572currentLine = (573this._EOLNormalized574? buffer.substring(lineStarts[line], lineStarts[line + 1] - this._EOLLength)575: buffer.substring(lineStarts[line], lineStarts[line + 1]).replace(/(\r\n|\r|\n)$/, '')576);577lines[linesLength++] = currentLine;578}579580if (!this._EOLNormalized && buffer.charCodeAt(lineStarts[pieceEndLine] + piece.end.column - 1) === CharCode.CarriageReturn) {581danglingCR = true;582if (piece.end.column === 0) {583// The last line ended with a \r, let's undo the push, it will be pushed by next iteration584linesLength--;585} else {586currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column - 1);587}588} else {589currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column);590}591592return true;593});594595if (danglingCR) {596lines[linesLength++] = currentLine;597currentLine = '';598}599600lines[linesLength++] = currentLine;601return lines;602}603604public getLength(): number {605return this._length;606}607608public getLineCount(): number {609return this._lineCnt;610}611612public getLineContent(lineNumber: number): string {613if (this._lastVisitedLine.lineNumber === lineNumber) {614return this._lastVisitedLine.value;615}616617this._lastVisitedLine.lineNumber = lineNumber;618619if (lineNumber === this._lineCnt) {620this._lastVisitedLine.value = this.getLineRawContent(lineNumber);621} else if (this._EOLNormalized) {622this._lastVisitedLine.value = this.getLineRawContent(lineNumber, this._EOLLength);623} else {624this._lastVisitedLine.value = this.getLineRawContent(lineNumber).replace(/(\r\n|\r|\n)$/, '');625}626627return this._lastVisitedLine.value;628}629630private _getCharCode(nodePos: NodePosition): number {631if (nodePos.remainder === nodePos.node.piece.length) {632// the char we want to fetch is at the head of next node.633const matchingNode = nodePos.node.next();634if (!matchingNode) {635return 0;636}637638const buffer = this._buffers[matchingNode.piece.bufferIndex];639const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start);640return buffer.buffer.charCodeAt(startOffset);641} else {642const buffer = this._buffers[nodePos.node.piece.bufferIndex];643const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start);644const targetOffset = startOffset + nodePos.remainder;645646return buffer.buffer.charCodeAt(targetOffset);647}648}649650public getLineCharCode(lineNumber: number, index: number): number {651const nodePos = this.nodeAt2(lineNumber, index + 1);652return this._getCharCode(nodePos);653}654655public getLineLength(lineNumber: number): number {656if (lineNumber === this.getLineCount()) {657const startOffset = this.getOffsetAt(lineNumber, 1);658return this.getLength() - startOffset;659}660return this.getOffsetAt(lineNumber + 1, 1) - this.getOffsetAt(lineNumber, 1) - this._EOLLength;661}662663public getCharCode(offset: number): number {664const nodePos = this.nodeAt(offset);665return this._getCharCode(nodePos);666}667668public getNearestChunk(offset: number): string {669const nodePos = this.nodeAt(offset);670if (nodePos.remainder === nodePos.node.piece.length) {671// the offset is at the head of next node.672const matchingNode = nodePos.node.next();673if (!matchingNode || matchingNode === SENTINEL) {674return '';675}676677const buffer = this._buffers[matchingNode.piece.bufferIndex];678const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start);679return buffer.buffer.substring(startOffset, startOffset + matchingNode.piece.length);680} else {681const buffer = this._buffers[nodePos.node.piece.bufferIndex];682const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start);683const targetOffset = startOffset + nodePos.remainder;684const targetEnd = startOffset + nodePos.node.piece.length;685return buffer.buffer.substring(targetOffset, targetEnd);686}687}688689public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) {690const buffer = this._buffers[node.piece.bufferIndex];691const startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start);692const start = this.offsetInBuffer(node.piece.bufferIndex, startCursor);693const end = this.offsetInBuffer(node.piece.bufferIndex, endCursor);694695let m: RegExpExecArray | null;696// Reset regex to search from the beginning697const ret: BufferCursor = { line: 0, column: 0 };698let searchText: string;699let offsetInBuffer: (offset: number) => number;700701if (searcher._wordSeparators) {702searchText = buffer.buffer.substring(start, end);703offsetInBuffer = (offset: number) => offset + start;704searcher.reset(0);705} else {706searchText = buffer.buffer;707offsetInBuffer = (offset: number) => offset;708searcher.reset(start);709}710711do {712m = searcher.next(searchText);713714if (m) {715if (offsetInBuffer(m.index) >= end) {716return resultLen;717}718this.positionInBuffer(node, offsetInBuffer(m.index) - startOffsetInBuffer, ret);719const lineFeedCnt = this.getLineFeedCnt(node.piece.bufferIndex, startCursor, ret);720const retStartColumn = ret.line === startCursor.line ? ret.column - startCursor.column + startColumn : ret.column + 1;721const retEndColumn = retStartColumn + m[0].length;722result[resultLen++] = createFindMatch(new Range(startLineNumber + lineFeedCnt, retStartColumn, startLineNumber + lineFeedCnt, retEndColumn), m, captureMatches);723724if (offsetInBuffer(m.index) + m[0].length >= end) {725return resultLen;726}727if (resultLen >= limitResultCount) {728return resultLen;729}730}731732} while (m);733734return resultLen;735}736737public findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] {738const result: FindMatch[] = [];739let resultLen = 0;740const searcher = new Searcher(searchData.wordSeparators, searchData.regex);741742let startPosition = this.nodeAt2(searchRange.startLineNumber, searchRange.startColumn);743if (startPosition === null) {744return [];745}746const endPosition = this.nodeAt2(searchRange.endLineNumber, searchRange.endColumn);747if (endPosition === null) {748return [];749}750let start = this.positionInBuffer(startPosition.node, startPosition.remainder);751const end = this.positionInBuffer(endPosition.node, endPosition.remainder);752753if (startPosition.node === endPosition.node) {754this.findMatchesInNode(startPosition.node, searcher, searchRange.startLineNumber, searchRange.startColumn, start, end, searchData, captureMatches, limitResultCount, resultLen, result);755return result;756}757758let startLineNumber = searchRange.startLineNumber;759760let currentNode = startPosition.node;761while (currentNode !== endPosition.node) {762const lineBreakCnt = this.getLineFeedCnt(currentNode.piece.bufferIndex, start, currentNode.piece.end);763764if (lineBreakCnt >= 1) {765// last line break position766const lineStarts = this._buffers[currentNode.piece.bufferIndex].lineStarts;767const startOffsetInBuffer = this.offsetInBuffer(currentNode.piece.bufferIndex, currentNode.piece.start);768const nextLineStartOffset = lineStarts[start.line + lineBreakCnt];769const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1;770resultLen = this.findMatchesInNode(currentNode, searcher, startLineNumber, startColumn, start, this.positionInBuffer(currentNode, nextLineStartOffset - startOffsetInBuffer), searchData, captureMatches, limitResultCount, resultLen, result);771772if (resultLen >= limitResultCount) {773return result;774}775776startLineNumber += lineBreakCnt;777}778779const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0;780// search for the remaining content781if (startLineNumber === searchRange.endLineNumber) {782const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1);783resultLen = this._findMatchesInLine(searchData, searcher, text, searchRange.endLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount);784return result;785}786787resultLen = this._findMatchesInLine(searchData, searcher, this.getLineContent(startLineNumber).substr(startColumn), startLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount);788789if (resultLen >= limitResultCount) {790return result;791}792793startLineNumber++;794startPosition = this.nodeAt2(startLineNumber, 1);795currentNode = startPosition.node;796start = this.positionInBuffer(startPosition.node, startPosition.remainder);797}798799if (startLineNumber === searchRange.endLineNumber) {800const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0;801const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1);802resultLen = this._findMatchesInLine(searchData, searcher, text, searchRange.endLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount);803return result;804}805806const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1;807resultLen = this.findMatchesInNode(endPosition.node, searcher, startLineNumber, startColumn, start, end, searchData, captureMatches, limitResultCount, resultLen, result);808return result;809}810811private _findMatchesInLine(searchData: SearchData, searcher: Searcher, text: string, lineNumber: number, deltaOffset: number, resultLen: number, result: FindMatch[], captureMatches: boolean, limitResultCount: number): number {812const wordSeparators = searchData.wordSeparators;813if (!captureMatches && searchData.simpleSearch) {814const searchString = searchData.simpleSearch;815const searchStringLen = searchString.length;816const textLength = text.length;817818let lastMatchIndex = -searchStringLen;819while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) {820if (!wordSeparators || isValidMatch(wordSeparators, text, textLength, lastMatchIndex, searchStringLen)) {821result[resultLen++] = new FindMatch(new Range(lineNumber, lastMatchIndex + 1 + deltaOffset, lineNumber, lastMatchIndex + 1 + searchStringLen + deltaOffset), null);822if (resultLen >= limitResultCount) {823return resultLen;824}825}826}827return resultLen;828}829830let m: RegExpExecArray | null;831// Reset regex to search from the beginning832searcher.reset(0);833do {834m = searcher.next(text);835if (m) {836result[resultLen++] = createFindMatch(new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset), m, captureMatches);837if (resultLen >= limitResultCount) {838return resultLen;839}840}841} while (m);842return resultLen;843}844845// #endregion846847// #region Piece Table848public insert(offset: number, value: string, eolNormalized: boolean = false): void {849this._EOLNormalized = this._EOLNormalized && eolNormalized;850this._lastVisitedLine.lineNumber = 0;851this._lastVisitedLine.value = '';852853if (this.root !== SENTINEL) {854const { node, remainder, nodeStartOffset } = this.nodeAt(offset);855const piece = node.piece;856const bufferIndex = piece.bufferIndex;857const insertPosInBuffer = this.positionInBuffer(node, remainder);858if (node.piece.bufferIndex === 0 &&859piece.end.line === this._lastChangeBufferPos.line &&860piece.end.column === this._lastChangeBufferPos.column &&861(nodeStartOffset + piece.length === offset) &&862value.length < AverageBufferSize863) {864// changed buffer865this.appendToNode(node, value);866this.computeBufferMetadata();867return;868}869870if (nodeStartOffset === offset) {871this.insertContentToNodeLeft(value, node);872this._searchCache.validate(offset);873} else if (nodeStartOffset + node.piece.length > offset) {874// we are inserting into the middle of a node.875const nodesToDel: TreeNode[] = [];876let newRightPiece = new Piece(877piece.bufferIndex,878insertPosInBuffer,879piece.end,880this.getLineFeedCnt(piece.bufferIndex, insertPosInBuffer, piece.end),881this.offsetInBuffer(bufferIndex, piece.end) - this.offsetInBuffer(bufferIndex, insertPosInBuffer)882);883884if (this.shouldCheckCRLF() && this.endWithCR(value)) {885const headOfRight = this.nodeCharCodeAt(node, remainder);886887if (headOfRight === 10 /** \n */) {888const newStart: BufferCursor = { line: newRightPiece.start.line + 1, column: 0 };889newRightPiece = new Piece(890newRightPiece.bufferIndex,891newStart,892newRightPiece.end,893this.getLineFeedCnt(newRightPiece.bufferIndex, newStart, newRightPiece.end),894newRightPiece.length - 1895);896897value += '\n';898}899}900901// reuse node for content before insertion point.902if (this.shouldCheckCRLF() && this.startWithLF(value)) {903const tailOfLeft = this.nodeCharCodeAt(node, remainder - 1);904if (tailOfLeft === 13 /** \r */) {905const previousPos = this.positionInBuffer(node, remainder - 1);906this.deleteNodeTail(node, previousPos);907value = '\r' + value;908909if (node.piece.length === 0) {910nodesToDel.push(node);911}912} else {913this.deleteNodeTail(node, insertPosInBuffer);914}915} else {916this.deleteNodeTail(node, insertPosInBuffer);917}918919const newPieces = this.createNewPieces(value);920if (newRightPiece.length > 0) {921this.rbInsertRight(node, newRightPiece);922}923924let tmpNode = node;925for (let k = 0; k < newPieces.length; k++) {926tmpNode = this.rbInsertRight(tmpNode, newPieces[k]);927}928this.deleteNodes(nodesToDel);929} else {930this.insertContentToNodeRight(value, node);931}932} else {933// insert new node934const pieces = this.createNewPieces(value);935let node = this.rbInsertLeft(null, pieces[0]);936937for (let k = 1; k < pieces.length; k++) {938node = this.rbInsertRight(node, pieces[k]);939}940}941942// todo, this is too brutal. Total line feed count should be updated the same way as lf_left.943this.computeBufferMetadata();944}945946public delete(offset: number, cnt: number): void {947this._lastVisitedLine.lineNumber = 0;948this._lastVisitedLine.value = '';949950if (cnt <= 0 || this.root === SENTINEL) {951return;952}953954const startPosition = this.nodeAt(offset);955const endPosition = this.nodeAt(offset + cnt);956const startNode = startPosition.node;957const endNode = endPosition.node;958959if (startNode === endNode) {960const startSplitPosInBuffer = this.positionInBuffer(startNode, startPosition.remainder);961const endSplitPosInBuffer = this.positionInBuffer(startNode, endPosition.remainder);962963if (startPosition.nodeStartOffset === offset) {964if (cnt === startNode.piece.length) { // delete node965const next = startNode.next();966rbDelete(this, startNode);967this.validateCRLFWithPrevNode(next);968this.computeBufferMetadata();969return;970}971this.deleteNodeHead(startNode, endSplitPosInBuffer);972this._searchCache.validate(offset);973this.validateCRLFWithPrevNode(startNode);974this.computeBufferMetadata();975return;976}977978if (startPosition.nodeStartOffset + startNode.piece.length === offset + cnt) {979this.deleteNodeTail(startNode, startSplitPosInBuffer);980this.validateCRLFWithNextNode(startNode);981this.computeBufferMetadata();982return;983}984985// delete content in the middle, this node will be splitted to nodes986this.shrinkNode(startNode, startSplitPosInBuffer, endSplitPosInBuffer);987this.computeBufferMetadata();988return;989}990991const nodesToDel: TreeNode[] = [];992993const startSplitPosInBuffer = this.positionInBuffer(startNode, startPosition.remainder);994this.deleteNodeTail(startNode, startSplitPosInBuffer);995this._searchCache.validate(offset);996if (startNode.piece.length === 0) {997nodesToDel.push(startNode);998}9991000// update last touched node1001const endSplitPosInBuffer = this.positionInBuffer(endNode, endPosition.remainder);1002this.deleteNodeHead(endNode, endSplitPosInBuffer);1003if (endNode.piece.length === 0) {1004nodesToDel.push(endNode);1005}10061007// delete nodes in between1008const secondNode = startNode.next();1009for (let node = secondNode; node !== SENTINEL && node !== endNode; node = node.next()) {1010nodesToDel.push(node);1011}10121013const prev = startNode.piece.length === 0 ? startNode.prev() : startNode;1014this.deleteNodes(nodesToDel);1015this.validateCRLFWithNextNode(prev);1016this.computeBufferMetadata();1017}10181019private insertContentToNodeLeft(value: string, node: TreeNode) {1020// we are inserting content to the beginning of node1021const nodesToDel: TreeNode[] = [];1022if (this.shouldCheckCRLF() && this.endWithCR(value) && this.startWithLF(node)) {1023// move `\n` to new node.10241025const piece = node.piece;1026const newStart: BufferCursor = { line: piece.start.line + 1, column: 0 };1027const nPiece = new Piece(1028piece.bufferIndex,1029newStart,1030piece.end,1031this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end),1032piece.length - 11033);10341035node.piece = nPiece;10361037value += '\n';1038updateTreeMetadata(this, node, -1, -1);10391040if (node.piece.length === 0) {1041nodesToDel.push(node);1042}1043}10441045const newPieces = this.createNewPieces(value);1046let newNode = this.rbInsertLeft(node, newPieces[newPieces.length - 1]);1047for (let k = newPieces.length - 2; k >= 0; k--) {1048newNode = this.rbInsertLeft(newNode, newPieces[k]);1049}1050this.validateCRLFWithPrevNode(newNode);1051this.deleteNodes(nodesToDel);1052}10531054private insertContentToNodeRight(value: string, node: TreeNode) {1055// we are inserting to the right of this node.1056if (this.adjustCarriageReturnFromNext(value, node)) {1057// move \n to the new node.1058value += '\n';1059}10601061const newPieces = this.createNewPieces(value);1062const newNode = this.rbInsertRight(node, newPieces[0]);1063let tmpNode = newNode;10641065for (let k = 1; k < newPieces.length; k++) {1066tmpNode = this.rbInsertRight(tmpNode, newPieces[k]);1067}10681069this.validateCRLFWithPrevNode(newNode);1070}10711072private positionInBuffer(node: TreeNode, remainder: number): BufferCursor;1073private positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null;1074private positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null {1075const piece = node.piece;1076const bufferIndex = node.piece.bufferIndex;1077const lineStarts = this._buffers[bufferIndex].lineStarts;10781079const startOffset = lineStarts[piece.start.line] + piece.start.column;10801081const offset = startOffset + remainder;10821083// binary search offset between startOffset and endOffset1084let low = piece.start.line;1085let high = piece.end.line;10861087let mid: number = 0;1088let midStop: number = 0;1089let midStart: number = 0;10901091while (low <= high) {1092mid = low + ((high - low) / 2) | 0;1093midStart = lineStarts[mid];10941095if (mid === high) {1096break;1097}10981099midStop = lineStarts[mid + 1];11001101if (offset < midStart) {1102high = mid - 1;1103} else if (offset >= midStop) {1104low = mid + 1;1105} else {1106break;1107}1108}11091110if (ret) {1111ret.line = mid;1112ret.column = offset - midStart;1113return null;1114}11151116return {1117line: mid,1118column: offset - midStart1119};1120}11211122private getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number {1123// we don't need to worry about start: abc\r|\n, or abc|\r, or abc|\n, or abc|\r\n doesn't change the fact that, there is one line break after start.1124// now let's take care of end: abc\r|\n, if end is in between \r and \n, we need to add line feed count by 11125if (end.column === 0) {1126return end.line - start.line;1127}11281129const lineStarts = this._buffers[bufferIndex].lineStarts;1130if (end.line === lineStarts.length - 1) { // it means, there is no \n after end, otherwise, there will be one more lineStart.1131return end.line - start.line;1132}11331134const nextLineStartOffset = lineStarts[end.line + 1];1135const endOffset = lineStarts[end.line] + end.column;1136if (nextLineStartOffset > endOffset + 1) { // there are more than 1 character after end, which means it can't be \n1137return end.line - start.line;1138}1139// endOffset + 1 === nextLineStartOffset1140// character at endOffset is \n, so we check the character before first1141// if character at endOffset is \r, end.column is 0 and we can't get here.1142const previousCharOffset = endOffset - 1; // end.column > 0 so it's okay.1143const buffer = this._buffers[bufferIndex].buffer;11441145if (buffer.charCodeAt(previousCharOffset) === 13) {1146return end.line - start.line + 1;1147} else {1148return end.line - start.line;1149}1150}11511152private offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number {1153const lineStarts = this._buffers[bufferIndex].lineStarts;1154return lineStarts[cursor.line] + cursor.column;1155}11561157private deleteNodes(nodes: TreeNode[]): void {1158for (let i = 0; i < nodes.length; i++) {1159rbDelete(this, nodes[i]);1160}1161}11621163private createNewPieces(text: string): Piece[] {1164if (text.length > AverageBufferSize) {1165// the content is large, operations like substring, charCode becomes slow1166// so here we split it into smaller chunks, just like what we did for CR/LF normalization1167const newPieces: Piece[] = [];1168while (text.length > AverageBufferSize) {1169const lastChar = text.charCodeAt(AverageBufferSize - 1);1170let splitText;1171if (lastChar === CharCode.CarriageReturn || (lastChar >= 0xD800 && lastChar <= 0xDBFF)) {1172// last character is \r or a high surrogate => keep it back1173splitText = text.substring(0, AverageBufferSize - 1);1174text = text.substring(AverageBufferSize - 1);1175} else {1176splitText = text.substring(0, AverageBufferSize);1177text = text.substring(AverageBufferSize);1178}11791180const lineStarts = createLineStartsFast(splitText);1181newPieces.push(new Piece(1182this._buffers.length, /* buffer index */1183{ line: 0, column: 0 },1184{ line: lineStarts.length - 1, column: splitText.length - lineStarts[lineStarts.length - 1] },1185lineStarts.length - 1,1186splitText.length1187));1188this._buffers.push(new StringBuffer(splitText, lineStarts));1189}11901191const lineStarts = createLineStartsFast(text);1192newPieces.push(new Piece(1193this._buffers.length, /* buffer index */1194{ line: 0, column: 0 },1195{ line: lineStarts.length - 1, column: text.length - lineStarts[lineStarts.length - 1] },1196lineStarts.length - 1,1197text.length1198));1199this._buffers.push(new StringBuffer(text, lineStarts));12001201return newPieces;1202}12031204let startOffset = this._buffers[0].buffer.length;1205const lineStarts = createLineStartsFast(text, false);12061207let start = this._lastChangeBufferPos;1208if (this._buffers[0].lineStarts[this._buffers[0].lineStarts.length - 1] === startOffset1209&& startOffset !== 01210&& this.startWithLF(text)1211&& this.endWithCR(this._buffers[0].buffer) // todo, we can check this._lastChangeBufferPos's column as it's the last one1212) {1213this._lastChangeBufferPos = { line: this._lastChangeBufferPos.line, column: this._lastChangeBufferPos.column + 1 };1214start = this._lastChangeBufferPos;12151216for (let i = 0; i < lineStarts.length; i++) {1217lineStarts[i] += startOffset + 1;1218}12191220this._buffers[0].lineStarts = (<number[]>this._buffers[0].lineStarts).concat(<number[]>lineStarts.slice(1));1221this._buffers[0].buffer += '_' + text;1222startOffset += 1;1223} else {1224if (startOffset !== 0) {1225for (let i = 0; i < lineStarts.length; i++) {1226lineStarts[i] += startOffset;1227}1228}1229this._buffers[0].lineStarts = (<number[]>this._buffers[0].lineStarts).concat(<number[]>lineStarts.slice(1));1230this._buffers[0].buffer += text;1231}12321233const endOffset = this._buffers[0].buffer.length;1234const endIndex = this._buffers[0].lineStarts.length - 1;1235const endColumn = endOffset - this._buffers[0].lineStarts[endIndex];1236const endPos = { line: endIndex, column: endColumn };1237const newPiece = new Piece(12380, /** todo@peng */1239start,1240endPos,1241this.getLineFeedCnt(0, start, endPos),1242endOffset - startOffset1243);1244this._lastChangeBufferPos = endPos;1245return [newPiece];1246}12471248public getLinesRawContent(): string {1249return this.getContentOfSubTree(this.root);1250}12511252public getLineRawContent(lineNumber: number, endOffset: number = 0): string {1253let x = this.root;12541255let ret = '';1256const cache = this._searchCache.get2(lineNumber);1257if (cache) {1258x = cache.node;1259const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - cache.nodeStartLineNumber - 1);1260const buffer = this._buffers[x.piece.bufferIndex].buffer;1261const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start);1262if (cache.nodeStartLineNumber + x.piece.lineFeedCnt === lineNumber) {1263ret = buffer.substring(startOffset + prevAccumulatedValue, startOffset + x.piece.length);1264} else {1265const accumulatedValue = this.getAccumulatedValue(x, lineNumber - cache.nodeStartLineNumber);1266return buffer.substring(startOffset + prevAccumulatedValue, startOffset + accumulatedValue - endOffset);1267}1268} else {1269let nodeStartOffset = 0;1270const originalLineNumber = lineNumber;1271while (x !== SENTINEL) {1272if (x.left !== SENTINEL && x.lf_left >= lineNumber - 1) {1273x = x.left;1274} else if (x.lf_left + x.piece.lineFeedCnt > lineNumber - 1) {1275const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2);1276const accumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 1);1277const buffer = this._buffers[x.piece.bufferIndex].buffer;1278const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start);1279nodeStartOffset += x.size_left;1280this._searchCache.set({1281node: x,1282nodeStartOffset,1283nodeStartLineNumber: originalLineNumber - (lineNumber - 1 - x.lf_left)1284});12851286return buffer.substring(startOffset + prevAccumulatedValue, startOffset + accumulatedValue - endOffset);1287} else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) {1288const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2);1289const buffer = this._buffers[x.piece.bufferIndex].buffer;1290const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start);12911292ret = buffer.substring(startOffset + prevAccumulatedValue, startOffset + x.piece.length);1293break;1294} else {1295lineNumber -= x.lf_left + x.piece.lineFeedCnt;1296nodeStartOffset += x.size_left + x.piece.length;1297x = x.right;1298}1299}1300}13011302// search in order, to find the node contains end column1303x = x.next();1304while (x !== SENTINEL) {1305const buffer = this._buffers[x.piece.bufferIndex].buffer;13061307if (x.piece.lineFeedCnt > 0) {1308const accumulatedValue = this.getAccumulatedValue(x, 0);1309const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start);13101311ret += buffer.substring(startOffset, startOffset + accumulatedValue - endOffset);1312return ret;1313} else {1314const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start);1315ret += buffer.substr(startOffset, x.piece.length);1316}13171318x = x.next();1319}13201321return ret;1322}13231324private computeBufferMetadata() {1325let x = this.root;13261327let lfCnt = 1;1328let len = 0;13291330while (x !== SENTINEL) {1331lfCnt += x.lf_left + x.piece.lineFeedCnt;1332len += x.size_left + x.piece.length;1333x = x.right;1334}13351336this._lineCnt = lfCnt;1337this._length = len;1338this._searchCache.validate(this._length);1339}13401341// #region node operations1342private getIndexOf(node: TreeNode, accumulatedValue: number): { index: number; remainder: number } {1343const piece = node.piece;1344const pos = this.positionInBuffer(node, accumulatedValue);1345const lineCnt = pos.line - piece.start.line;13461347if (this.offsetInBuffer(piece.bufferIndex, piece.end) - this.offsetInBuffer(piece.bufferIndex, piece.start) === accumulatedValue) {1348// we are checking the end of this node, so a CRLF check is necessary.1349const realLineCnt = this.getLineFeedCnt(node.piece.bufferIndex, piece.start, pos);1350if (realLineCnt !== lineCnt) {1351// aha yes, CRLF1352return { index: realLineCnt, remainder: 0 };1353}1354}13551356return { index: lineCnt, remainder: pos.column };1357}13581359private getAccumulatedValue(node: TreeNode, index: number) {1360if (index < 0) {1361return 0;1362}1363const piece = node.piece;1364const lineStarts = this._buffers[piece.bufferIndex].lineStarts;1365const expectedLineStartIndex = piece.start.line + index + 1;1366if (expectedLineStartIndex > piece.end.line) {1367return lineStarts[piece.end.line] + piece.end.column - lineStarts[piece.start.line] - piece.start.column;1368} else {1369return lineStarts[expectedLineStartIndex] - lineStarts[piece.start.line] - piece.start.column;1370}1371}13721373private deleteNodeTail(node: TreeNode, pos: BufferCursor) {1374const piece = node.piece;1375const originalLFCnt = piece.lineFeedCnt;1376const originalEndOffset = this.offsetInBuffer(piece.bufferIndex, piece.end);13771378const newEnd = pos;1379const newEndOffset = this.offsetInBuffer(piece.bufferIndex, newEnd);1380const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, newEnd);13811382const lf_delta = newLineFeedCnt - originalLFCnt;1383const size_delta = newEndOffset - originalEndOffset;1384const newLength = piece.length + size_delta;13851386node.piece = new Piece(1387piece.bufferIndex,1388piece.start,1389newEnd,1390newLineFeedCnt,1391newLength1392);13931394updateTreeMetadata(this, node, size_delta, lf_delta);1395}13961397private deleteNodeHead(node: TreeNode, pos: BufferCursor) {1398const piece = node.piece;1399const originalLFCnt = piece.lineFeedCnt;1400const originalStartOffset = this.offsetInBuffer(piece.bufferIndex, piece.start);14011402const newStart = pos;1403const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end);1404const newStartOffset = this.offsetInBuffer(piece.bufferIndex, newStart);1405const lf_delta = newLineFeedCnt - originalLFCnt;1406const size_delta = originalStartOffset - newStartOffset;1407const newLength = piece.length + size_delta;1408node.piece = new Piece(1409piece.bufferIndex,1410newStart,1411piece.end,1412newLineFeedCnt,1413newLength1414);14151416updateTreeMetadata(this, node, size_delta, lf_delta);1417}14181419private shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) {1420const piece = node.piece;1421const originalStartPos = piece.start;1422const originalEndPos = piece.end;14231424// old piece, originalStartPos, start1425const oldLength = piece.length;1426const oldLFCnt = piece.lineFeedCnt;1427const newEnd = start;1428const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, newEnd);1429const newLength = this.offsetInBuffer(piece.bufferIndex, start) - this.offsetInBuffer(piece.bufferIndex, originalStartPos);14301431node.piece = new Piece(1432piece.bufferIndex,1433piece.start,1434newEnd,1435newLineFeedCnt,1436newLength1437);14381439updateTreeMetadata(this, node, newLength - oldLength, newLineFeedCnt - oldLFCnt);14401441// new right piece, end, originalEndPos1442const newPiece = new Piece(1443piece.bufferIndex,1444end,1445originalEndPos,1446this.getLineFeedCnt(piece.bufferIndex, end, originalEndPos),1447this.offsetInBuffer(piece.bufferIndex, originalEndPos) - this.offsetInBuffer(piece.bufferIndex, end)1448);14491450const newNode = this.rbInsertRight(node, newPiece);1451this.validateCRLFWithPrevNode(newNode);1452}14531454private appendToNode(node: TreeNode, value: string): void {1455if (this.adjustCarriageReturnFromNext(value, node)) {1456value += '\n';1457}14581459const hitCRLF = this.shouldCheckCRLF() && this.startWithLF(value) && this.endWithCR(node);1460const startOffset = this._buffers[0].buffer.length;1461this._buffers[0].buffer += value;1462const lineStarts = createLineStartsFast(value, false);1463for (let i = 0; i < lineStarts.length; i++) {1464lineStarts[i] += startOffset;1465}1466if (hitCRLF) {1467const prevStartOffset = this._buffers[0].lineStarts[this._buffers[0].lineStarts.length - 2];1468(<number[]>this._buffers[0].lineStarts).pop();1469// _lastChangeBufferPos is already wrong1470this._lastChangeBufferPos = { line: this._lastChangeBufferPos.line - 1, column: startOffset - prevStartOffset };1471}14721473this._buffers[0].lineStarts = (<number[]>this._buffers[0].lineStarts).concat(<number[]>lineStarts.slice(1));1474const endIndex = this._buffers[0].lineStarts.length - 1;1475const endColumn = this._buffers[0].buffer.length - this._buffers[0].lineStarts[endIndex];1476const newEnd = { line: endIndex, column: endColumn };1477const newLength = node.piece.length + value.length;1478const oldLineFeedCnt = node.piece.lineFeedCnt;1479const newLineFeedCnt = this.getLineFeedCnt(0, node.piece.start, newEnd);1480const lf_delta = newLineFeedCnt - oldLineFeedCnt;14811482node.piece = new Piece(1483node.piece.bufferIndex,1484node.piece.start,1485newEnd,1486newLineFeedCnt,1487newLength1488);14891490this._lastChangeBufferPos = newEnd;1491updateTreeMetadata(this, node, value.length, lf_delta);1492}14931494private nodeAt(offset: number): NodePosition {1495let x = this.root;1496const cache = this._searchCache.get(offset);1497if (cache) {1498return {1499node: cache.node,1500nodeStartOffset: cache.nodeStartOffset,1501remainder: offset - cache.nodeStartOffset1502};1503}15041505let nodeStartOffset = 0;15061507while (x !== SENTINEL) {1508if (x.size_left > offset) {1509x = x.left;1510} else if (x.size_left + x.piece.length >= offset) {1511nodeStartOffset += x.size_left;1512const ret = {1513node: x,1514remainder: offset - x.size_left,1515nodeStartOffset1516};1517this._searchCache.set(ret);1518return ret;1519} else {1520offset -= x.size_left + x.piece.length;1521nodeStartOffset += x.size_left + x.piece.length;1522x = x.right;1523}1524}15251526return null!;1527}15281529private nodeAt2(lineNumber: number, column: number): NodePosition {1530let x = this.root;1531let nodeStartOffset = 0;15321533while (x !== SENTINEL) {1534if (x.left !== SENTINEL && x.lf_left >= lineNumber - 1) {1535x = x.left;1536} else if (x.lf_left + x.piece.lineFeedCnt > lineNumber - 1) {1537const prevAccumualtedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2);1538const accumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 1);1539nodeStartOffset += x.size_left;15401541return {1542node: x,1543remainder: Math.min(prevAccumualtedValue + column - 1, accumulatedValue),1544nodeStartOffset1545};1546} else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) {1547const prevAccumualtedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2);1548if (prevAccumualtedValue + column - 1 <= x.piece.length) {1549return {1550node: x,1551remainder: prevAccumualtedValue + column - 1,1552nodeStartOffset1553};1554} else {1555column -= x.piece.length - prevAccumualtedValue;1556break;1557}1558} else {1559lineNumber -= x.lf_left + x.piece.lineFeedCnt;1560nodeStartOffset += x.size_left + x.piece.length;1561x = x.right;1562}1563}15641565// search in order, to find the node contains position.column1566x = x.next();1567while (x !== SENTINEL) {15681569if (x.piece.lineFeedCnt > 0) {1570const accumulatedValue = this.getAccumulatedValue(x, 0);1571const nodeStartOffset = this.offsetOfNode(x);1572return {1573node: x,1574remainder: Math.min(column - 1, accumulatedValue),1575nodeStartOffset1576};1577} else {1578if (x.piece.length >= column - 1) {1579const nodeStartOffset = this.offsetOfNode(x);1580return {1581node: x,1582remainder: column - 1,1583nodeStartOffset1584};1585} else {1586column -= x.piece.length;1587}1588}15891590x = x.next();1591}15921593return null!;1594}15951596private nodeCharCodeAt(node: TreeNode, offset: number): number {1597if (node.piece.lineFeedCnt < 1) {1598return -1;1599}1600const buffer = this._buffers[node.piece.bufferIndex];1601const newOffset = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start) + offset;1602return buffer.buffer.charCodeAt(newOffset);1603}16041605private offsetOfNode(node: TreeNode): number {1606if (!node) {1607return 0;1608}1609let pos = node.size_left;1610while (node !== this.root) {1611if (node.parent.right === node) {1612pos += node.parent.size_left + node.parent.piece.length;1613}16141615node = node.parent;1616}16171618return pos;1619}16201621// #endregion16221623// #region CRLF1624private shouldCheckCRLF() {1625return !(this._EOLNormalized && this._EOL === '\n');1626}16271628private startWithLF(val: string | TreeNode): boolean {1629if (typeof val === 'string') {1630return val.charCodeAt(0) === 10;1631}16321633if (val === SENTINEL || val.piece.lineFeedCnt === 0) {1634return false;1635}16361637const piece = val.piece;1638const lineStarts = this._buffers[piece.bufferIndex].lineStarts;1639const line = piece.start.line;1640const startOffset = lineStarts[line] + piece.start.column;1641if (line === lineStarts.length - 1) {1642// last line, so there is no line feed at the end of this line1643return false;1644}1645const nextLineOffset = lineStarts[line + 1];1646if (nextLineOffset > startOffset + 1) {1647return false;1648}1649return this._buffers[piece.bufferIndex].buffer.charCodeAt(startOffset) === 10;1650}16511652private endWithCR(val: string | TreeNode): boolean {1653if (typeof val === 'string') {1654return val.charCodeAt(val.length - 1) === 13;1655}16561657if (val === SENTINEL || val.piece.lineFeedCnt === 0) {1658return false;1659}16601661return this.nodeCharCodeAt(val, val.piece.length - 1) === 13;1662}16631664private validateCRLFWithPrevNode(nextNode: TreeNode) {1665if (this.shouldCheckCRLF() && this.startWithLF(nextNode)) {1666const node = nextNode.prev();1667if (this.endWithCR(node)) {1668this.fixCRLF(node, nextNode);1669}1670}1671}16721673private validateCRLFWithNextNode(node: TreeNode) {1674if (this.shouldCheckCRLF() && this.endWithCR(node)) {1675const nextNode = node.next();1676if (this.startWithLF(nextNode)) {1677this.fixCRLF(node, nextNode);1678}1679}1680}16811682private fixCRLF(prev: TreeNode, next: TreeNode) {1683const nodesToDel: TreeNode[] = [];1684// update node1685const lineStarts = this._buffers[prev.piece.bufferIndex].lineStarts;1686let newEnd: BufferCursor;1687if (prev.piece.end.column === 0) {1688// it means, last line ends with \r, not \r\n1689newEnd = { line: prev.piece.end.line - 1, column: lineStarts[prev.piece.end.line] - lineStarts[prev.piece.end.line - 1] - 1 };1690} else {1691// \r\n1692newEnd = { line: prev.piece.end.line, column: prev.piece.end.column - 1 };1693}16941695const prevNewLength = prev.piece.length - 1;1696const prevNewLFCnt = prev.piece.lineFeedCnt - 1;1697prev.piece = new Piece(1698prev.piece.bufferIndex,1699prev.piece.start,1700newEnd,1701prevNewLFCnt,1702prevNewLength1703);17041705updateTreeMetadata(this, prev, - 1, -1);1706if (prev.piece.length === 0) {1707nodesToDel.push(prev);1708}17091710// update nextNode1711const newStart: BufferCursor = { line: next.piece.start.line + 1, column: 0 };1712const newLength = next.piece.length - 1;1713const newLineFeedCnt = this.getLineFeedCnt(next.piece.bufferIndex, newStart, next.piece.end);1714next.piece = new Piece(1715next.piece.bufferIndex,1716newStart,1717next.piece.end,1718newLineFeedCnt,1719newLength1720);17211722updateTreeMetadata(this, next, - 1, -1);1723if (next.piece.length === 0) {1724nodesToDel.push(next);1725}17261727// create new piece which contains \r\n1728const pieces = this.createNewPieces('\r\n');1729this.rbInsertRight(prev, pieces[0]);1730// delete empty nodes17311732for (let i = 0; i < nodesToDel.length; i++) {1733rbDelete(this, nodesToDel[i]);1734}1735}17361737private adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean {1738if (this.shouldCheckCRLF() && this.endWithCR(value)) {1739const nextNode = node.next();1740if (this.startWithLF(nextNode)) {1741// move `\n` forward1742value += '\n';17431744if (nextNode.piece.length === 1) {1745rbDelete(this, nextNode);1746} else {17471748const piece = nextNode.piece;1749const newStart: BufferCursor = { line: piece.start.line + 1, column: 0 };1750const newLength = piece.length - 1;1751const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end);1752nextNode.piece = new Piece(1753piece.bufferIndex,1754newStart,1755piece.end,1756newLineFeedCnt,1757newLength1758);17591760updateTreeMetadata(this, nextNode, -1, -1);1761}1762return true;1763}1764}17651766return false;1767}17681769// #endregion17701771// #endregion17721773// #region Tree operations1774iterate(node: TreeNode, callback: (node: TreeNode) => boolean): boolean {1775if (node === SENTINEL) {1776return callback(SENTINEL);1777}17781779const leftRet = this.iterate(node.left, callback);1780if (!leftRet) {1781return leftRet;1782}17831784return callback(node) && this.iterate(node.right, callback);1785}17861787private getNodeContent(node: TreeNode) {1788if (node === SENTINEL) {1789return '';1790}1791const buffer = this._buffers[node.piece.bufferIndex];1792const piece = node.piece;1793const startOffset = this.offsetInBuffer(piece.bufferIndex, piece.start);1794const endOffset = this.offsetInBuffer(piece.bufferIndex, piece.end);1795const currentContent = buffer.buffer.substring(startOffset, endOffset);1796return currentContent;1797}17981799getPieceContent(piece: Piece) {1800const buffer = this._buffers[piece.bufferIndex];1801const startOffset = this.offsetInBuffer(piece.bufferIndex, piece.start);1802const endOffset = this.offsetInBuffer(piece.bufferIndex, piece.end);1803const currentContent = buffer.buffer.substring(startOffset, endOffset);1804return currentContent;1805}18061807/**1808* node node1809* / \ / \1810* a b <---- a b1811* /1812* z1813*/1814private rbInsertRight(node: TreeNode | null, p: Piece): TreeNode {1815const z = new TreeNode(p, NodeColor.Red);1816z.left = SENTINEL;1817z.right = SENTINEL;1818z.parent = SENTINEL;1819z.size_left = 0;1820z.lf_left = 0;18211822const x = this.root;1823if (x === SENTINEL) {1824this.root = z;1825z.color = NodeColor.Black;1826} else if (node!.right === SENTINEL) {1827node!.right = z;1828z.parent = node!;1829} else {1830const nextNode = leftest(node!.right);1831nextNode.left = z;1832z.parent = nextNode;1833}18341835fixInsert(this, z);1836return z;1837}18381839/**1840* node node1841* / \ / \1842* a b ----> a b1843* \1844* z1845*/1846private rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode {1847const z = new TreeNode(p, NodeColor.Red);1848z.left = SENTINEL;1849z.right = SENTINEL;1850z.parent = SENTINEL;1851z.size_left = 0;1852z.lf_left = 0;18531854if (this.root === SENTINEL) {1855this.root = z;1856z.color = NodeColor.Black;1857} else if (node!.left === SENTINEL) {1858node!.left = z;1859z.parent = node!;1860} else {1861const prevNode = righttest(node!.left); // a1862prevNode.right = z;1863z.parent = prevNode;1864}18651866fixInsert(this, z);1867return z;1868}18691870private getContentOfSubTree(node: TreeNode): string {1871let str = '';18721873this.iterate(node, node => {1874str += this.getNodeContent(node);1875return true;1876});18771878return str;1879}1880// #endregion1881}188218831884