/** reference from https://github.com/microsoft/vscode */ import { CharCode } from '../common/charCode'; import { Position } from '../common/position'; import { Range } from '../common/range'; import { FindMatch, ITextSnapshot, SearchData } from '../common/model'; import { NodeColor, SENTINEL, TreeNode, fixInsert, leftest, rbDelete, righttest, updateTreeMetadata, } from './rbTreeBase'; import { Searcher, createFindMatch, isValidMatch } from '../model/textModelSearch'; // const lfRegex = new RegExp(/\r\n|\r|\n/g); const AverageBufferSize = 65535; function createUintArray(arr: number[]): Uint32Array | Uint16Array { let r; if (arr[arr.length - 1] < 65536) { r = new Uint16Array(arr.length); } else { r = new Uint32Array(arr.length); } r.set(arr, 0); return r; } class LineStarts { constructor( public readonly lineStarts: Uint32Array | Uint16Array | number[], public readonly cr: number, public readonly lf: number, public readonly crlf: number, public readonly isBasicASCII: boolean ) {} } export function createLineStartsFast(str: string, readonly: boolean = true): Uint32Array | Uint16Array | number[] { const r: number[] = [0]; let rLength = 1; 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; } } if (readonly) { return createUintArray(r); } else { return r; } } export function createLineStarts(r: number[], str: string): LineStarts { r.length = 0; r[0] = 0; let rLength = 1; 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(createUintArray(r), cr, lf, crlf, isBasicASCII); r.length = 0; return result; } interface NodePosition { /** * Piece Index */ node: TreeNode; /** * remainder in current piece. */ remainder: number; /** * node start offset in document. */ nodeStartOffset: number } export interface BufferCursor { /** * Line number in current buffer */ line: number; /** * Column number in current buffer */ column: number } export class Piece { readonly bufferIndex: number; readonly start: BufferCursor; readonly end: BufferCursor; readonly length: number; readonly lineFeedCnt: number; constructor(bufferIndex: number, start: BufferCursor, end: BufferCursor, lineFeedCnt: number, length: number) { this.bufferIndex = bufferIndex; this.start = start; this.end = end; this.lineFeedCnt = lineFeedCnt; this.length = length; } } export class StringBuffer { buffer: string; lineStarts: Uint32Array | Uint16Array | number[]; constructor(buffer: string, lineStarts: Uint32Array | Uint16Array | number[]) { this.buffer = buffer; this.lineStarts = lineStarts; } } /** * Readonly snapshot for piece tree. * In a real multiple thread environment, to make snapshot reading always work correctly, we need to * 1. Make TreeNode.piece immutable, then reading and writing can run in parallel. * 2. TreeNode/Buffers normalization should not happen during snapshot reading. */ class PieceTreeSnapshot implements ITextSnapshot { private readonly _pieces: Piece[]; private _index: number; private readonly _tree: PieceTreeBase; private readonly _BOM: string; constructor(tree: PieceTreeBase, BOM: string) { this._pieces = []; this._tree = tree; this._BOM = BOM; this._index = 0; if (tree.root !== SENTINEL) { tree.iterate(tree.root, node => { if (node !== SENTINEL) { this._pieces.push(node.piece); } return true; }); } } read(): string | null { if (this._pieces.length === 0) { if (this._index === 0) { this._index++; return this._BOM; } else { return null; } } if (this._index > this._pieces.length - 1) { return null; } if (this._index === 0) { return this._BOM + this._tree.getPieceContent(this._pieces[this._index++]); } return this._tree.getPieceContent(this._pieces[this._index++]); } } interface CacheEntry { node: TreeNode; nodeStartOffset: number; nodeStartLineNumber?: number } class PieceTreeSearchCache { private readonly _limit: number; private _cache: CacheEntry[]; constructor(limit: number) { this._limit = limit; this._cache = []; } public get(offset: number): CacheEntry | null { for (let i = this._cache.length - 1; i >= 0; i--) { const nodePos = this._cache[i]; if (nodePos.nodeStartOffset <= offset && nodePos.nodeStartOffset + nodePos.node.piece.length >= offset) { return nodePos; } } return null; } public get2( lineNumber: number ): { node: TreeNode; nodeStartOffset: number; nodeStartLineNumber: number } | null { for (let i = this._cache.length - 1; i >= 0; i--) { const nodePos = this._cache[i]; if ( nodePos.nodeStartLineNumber && nodePos.nodeStartLineNumber < lineNumber && nodePos.nodeStartLineNumber + nodePos.node.piece.lineFeedCnt >= lineNumber ) { return < { node: TreeNode; nodeStartOffset: number; nodeStartLineNumber: number } >nodePos; } } return null; } public set(nodePosition: CacheEntry) { if (this._cache.length >= this._limit) { this._cache.shift(); } this._cache.push(nodePosition); } public validate(offset: number) { let hasInvalidVal = false; const tmp: Array = this._cache; for (let i = 0; i < tmp.length; i++) { const nodePos = tmp[i]!; if (nodePos.node.parent === null || nodePos.nodeStartOffset >= offset) { tmp[i] = null; hasInvalidVal = true; continue; } } if (hasInvalidVal) { const newArr: CacheEntry[] = []; for (const entry of tmp) { if (entry !== null) { newArr.push(entry); } } this._cache = newArr; } } } export class PieceTreeBase { root!: TreeNode; protected _buffers!: StringBuffer[]; // 0 is change buffer, others are readonly original buffer. protected _lineCnt!: number; protected _length!: number; protected _EOL!: '\r\n' | '\n'; protected _EOLLength!: number; protected _EOLNormalized!: boolean; private _lastChangeBufferPos!: BufferCursor; private _searchCache!: PieceTreeSearchCache; private _lastVisitedLine!: { lineNumber: number; value: string }; constructor(chunks: StringBuffer[], eol: '\r\n' | '\n', eolNormalized: boolean) { this.create(chunks, eol, eolNormalized); } create(chunks: StringBuffer[], eol: '\r\n' | '\n', eolNormalized: boolean) { this._buffers = [new StringBuffer('', [0])]; this._lastChangeBufferPos = { line: 0, column: 0 }; this.root = SENTINEL; this._lineCnt = 1; this._length = 0; this._EOL = eol; this._EOLLength = eol.length; this._EOLNormalized = eolNormalized; let lastNode: TreeNode | null = null; for (let i = 0, len = chunks.length; i < len; i++) { if (chunks[i].buffer.length > 0) { if (!chunks[i].lineStarts) { chunks[i].lineStarts = createLineStartsFast(chunks[i].buffer); } const piece = new Piece( i + 1, { line: 0, column: 0 }, { line: chunks[i].lineStarts.length - 1, column: chunks[i].buffer.length - chunks[i].lineStarts[chunks[i].lineStarts.length - 1], }, chunks[i].lineStarts.length - 1, chunks[i].buffer.length ); this._buffers.push(chunks[i]); lastNode = this.rbInsertRight(lastNode, piece); } } this._searchCache = new PieceTreeSearchCache(1); this._lastVisitedLine = { lineNumber: 0, value: '' }; this.computeBufferMetadata(); } normalizeEOL(eol: '\r\n' | '\n') { const averageBufferSize = AverageBufferSize; const min = averageBufferSize - Math.floor(averageBufferSize / 3); const max = min * 2; let tempChunk = ''; let tempChunkLen = 0; const chunks: StringBuffer[] = []; this.iterate(this.root, node => { const str = this.getNodeContent(node); const len = str.length; if (tempChunkLen <= min || tempChunkLen + len < max) { tempChunk += str; tempChunkLen += len; return true; } // flush anyways const text = tempChunk.replace(/\r\n|\r|\n/g, eol); chunks.push(new StringBuffer(text, createLineStartsFast(text))); tempChunk = str; tempChunkLen = len; return true; }); if (tempChunkLen > 0) { const text = tempChunk.replace(/\r\n|\r|\n/g, eol); chunks.push(new StringBuffer(text, createLineStartsFast(text))); } this.create(chunks, eol, true); } // #region Buffer API public getEOL(): '\r\n' | '\n' { return this._EOL; } public setEOL(newEOL: '\r\n' | '\n'): void { this._EOL = newEOL; this._EOLLength = this._EOL.length; this.normalizeEOL(newEOL); } public createSnapshot(BOM: string): ITextSnapshot { return new PieceTreeSnapshot(this, BOM); } public equal(other: PieceTreeBase): boolean { if (this.getLength() !== other.getLength()) { return false; } if (this.getLineCount() !== other.getLineCount()) { return false; } let offset = 0; const ret = this.iterate(this.root, node => { if (node === SENTINEL) { return true; } const str = this.getNodeContent(node); const len = str.length; const startPosition = other.nodeAt(offset); const endPosition = other.nodeAt(offset + len); const val = other.getValueInRange2(startPosition, endPosition); offset += len; return str === val; }); return ret; } public getOffsetAt(lineNumber: number, column: number): number { let leftLen = 0; // inorder let x = this.root; while (x !== SENTINEL) { if (x.left !== SENTINEL && x.lf_left + 1 >= lineNumber) { x = x.left; } else if (x.lf_left + x.piece.lineFeedCnt + 1 >= lineNumber) { leftLen += x.size_left; // lineNumber >= 2 const accumualtedValInCurrentIndex = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); return (leftLen += accumualtedValInCurrentIndex + column - 1); } else { lineNumber -= x.lf_left + x.piece.lineFeedCnt; leftLen += x.size_left + x.piece.length; x = x.right; } } return leftLen; } public getPositionAt(offset: number): Position { offset = Math.floor(offset); offset = Math.max(0, offset); let x = this.root; let lfCnt = 0; const originalOffset = offset; while (x !== SENTINEL) { if (x.size_left !== 0 && x.size_left >= offset) { x = x.left; } else if (x.size_left + x.piece.length >= offset) { const out = this.getIndexOf(x, offset - x.size_left); lfCnt += x.lf_left + out.index; if (out.index === 0) { const lineStartOffset = this.getOffsetAt(lfCnt + 1, 1); const column = originalOffset - lineStartOffset; return new Position(lfCnt + 1, column + 1); } return new Position(lfCnt + 1, out.remainder + 1); } else { offset -= x.size_left + x.piece.length; lfCnt += x.lf_left + x.piece.lineFeedCnt; if (x.right === SENTINEL) { // last node const lineStartOffset = this.getOffsetAt(lfCnt + 1, 1); const column = originalOffset - offset - lineStartOffset; return new Position(lfCnt + 1, column + 1); } else { x = x.right; } } } return new Position(1, 1); } public getValueInRange(range: Range, eol?: string): string { if (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn) { return ''; } const startPosition = this.nodeAt2(range.startLineNumber, range.startColumn); const endPosition = this.nodeAt2(range.endLineNumber, range.endColumn); const value = this.getValueInRange2(startPosition, endPosition); if (eol) { if (eol !== this._EOL || !this._EOLNormalized) { return value.replace(/\r\n|\r|\n/g, eol); } if (eol === this.getEOL() && this._EOLNormalized) { // if (eol === '\r\n') {} return value; } return value.replace(/\r\n|\r|\n/g, eol); } return value; } public getValueInRange2(startPosition: NodePosition | null, endPosition: NodePosition | null): string { if (!startPosition || !endPosition) { return ""; } if (startPosition.node === endPosition.node) { const node = startPosition.node; const buffer = this._buffers[node.piece.bufferIndex].buffer; const startOffset = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); return buffer.substring(startOffset + startPosition.remainder, startOffset + endPosition.remainder); } let x = startPosition.node; const buffer = this._buffers[x.piece.bufferIndex].buffer; const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); let ret = buffer.substring(startOffset + startPosition.remainder, startOffset + x.piece.length); x = x.next(); while (x !== SENTINEL) { const buffer = this._buffers[x.piece.bufferIndex].buffer; const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); if (x === endPosition.node) { ret += buffer.substring(startOffset, startOffset + endPosition.remainder); break; } else { ret += buffer.substr(startOffset, x.piece.length); } x = x.next(); } return ret; } public getLinesContent(): string[] { const lines: string[] = []; let linesLength = 0; let currentLine = ''; let danglingCR = false; this.iterate(this.root, node => { if (node === SENTINEL) { return true; } const piece = node.piece; let pieceLength = piece.length; if (pieceLength === 0) { return true; } const buffer = this._buffers[piece.bufferIndex].buffer; const lineStarts = this._buffers[piece.bufferIndex].lineStarts; const pieceStartLine = piece.start.line; const pieceEndLine = piece.end.line; let pieceStartOffset = lineStarts[pieceStartLine] + piece.start.column; if (danglingCR) { if (buffer.charCodeAt(pieceStartOffset) === CharCode.LineFeed) { // pretend the \n was in the previous piece.. pieceStartOffset++; pieceLength--; } lines[linesLength++] = currentLine; currentLine = ''; danglingCR = false; if (pieceLength === 0) { return true; } } if (pieceStartLine === pieceEndLine) { // this piece has no new lines if ( !this._EOLNormalized && buffer.charCodeAt(pieceStartOffset + pieceLength - 1) === CharCode.CarriageReturn ) { danglingCR = true; currentLine += buffer.substr(pieceStartOffset, pieceLength - 1); } else { currentLine += buffer.substr(pieceStartOffset, pieceLength); } return true; } // add the text before the first line start in this piece currentLine += this._EOLNormalized ? buffer.substring( pieceStartOffset, Math.max(pieceStartOffset, lineStarts[pieceStartLine + 1] - this._EOLLength) ) : buffer.substring(pieceStartOffset, lineStarts[pieceStartLine + 1]).replace(/(\r\n|\r|\n)$/, ''); lines[linesLength++] = currentLine; for (let line = pieceStartLine + 1; line < pieceEndLine; line++) { currentLine = this._EOLNormalized ? buffer.substring(lineStarts[line], lineStarts[line + 1] - this._EOLLength) : buffer.substring(lineStarts[line], lineStarts[line + 1]).replace(/(\r\n|\r|\n)$/, ''); lines[linesLength++] = currentLine; } if ( !this._EOLNormalized && buffer.charCodeAt(lineStarts[pieceEndLine] + piece.end.column - 1) === CharCode.CarriageReturn ) { danglingCR = true; if (piece.end.column === 0) { // The last line ended with a \r, let's undo the push, it will be pushed by next iteration linesLength--; } else { currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column - 1); } } else { currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column); } return true; }); if (danglingCR) { lines[linesLength++] = currentLine; currentLine = ''; } lines[linesLength++] = currentLine; return lines; } public getLength(): number { return this._length; } public getLineCount(): number { return this._lineCnt; } public getLineContent(lineNumber: number): string { if (this._lastVisitedLine.lineNumber === lineNumber) { return this._lastVisitedLine.value; } this._lastVisitedLine.lineNumber = lineNumber; if (lineNumber === this._lineCnt) { this._lastVisitedLine.value = this.getLineRawContent(lineNumber); } else if (this._EOLNormalized) { this._lastVisitedLine.value = this.getLineRawContent(lineNumber, this._EOLLength); } else { this._lastVisitedLine.value = this.getLineRawContent(lineNumber).replace(/(\r\n|\r|\n)$/, ''); } return this._lastVisitedLine.value; } private _getCharCode(nodePos: NodePosition): number { if (nodePos.remainder === nodePos.node.piece.length) { // the char we want to fetch is at the head of next node. const matchingNode = nodePos.node.next(); if (!matchingNode) { return 0; } const buffer = this._buffers[matchingNode.piece.bufferIndex]; const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); return buffer.buffer.charCodeAt(startOffset); } else { const buffer = this._buffers[nodePos.node.piece.bufferIndex]; const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); const targetOffset = startOffset + nodePos.remainder; return buffer.buffer.charCodeAt(targetOffset); } } public getLineCharCode(lineNumber: number, index: number): number { const nodePos = this.nodeAt2(lineNumber, index + 1); return this._getCharCode(nodePos); } public getLineLength(lineNumber: number): number { if (lineNumber === this.getLineCount()) { const startOffset = this.getOffsetAt(lineNumber, 1); return this.getLength() - startOffset; } return this.getOffsetAt(lineNumber + 1, 1) - this.getOffsetAt(lineNumber, 1) - this._EOLLength; } public getCharCode(offset: number): number { const nodePos = this.nodeAt(offset); return this._getCharCode(nodePos); } public getNearestChunk(offset: number): string { const nodePos = this.nodeAt(offset); if (nodePos.remainder === nodePos.node.piece.length) { // the offset is at the head of next node. const matchingNode = nodePos.node.next(); if (!matchingNode || matchingNode === SENTINEL) { return ''; } const buffer = this._buffers[matchingNode.piece.bufferIndex]; const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); return buffer.buffer.substring(startOffset, startOffset + matchingNode.piece.length); } else { const buffer = this._buffers[nodePos.node.piece.bufferIndex]; const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); const targetOffset = startOffset + nodePos.remainder; const targetEnd = startOffset + nodePos.node.piece.length; return buffer.buffer.substring(targetOffset, targetEnd); } } public findMatchesInNode( node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[] ) { const buffer = this._buffers[node.piece.bufferIndex]; const startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); const start = this.offsetInBuffer(node.piece.bufferIndex, startCursor); const end = this.offsetInBuffer(node.piece.bufferIndex, endCursor); let m: RegExpExecArray | null; // Reset regex to search from the beginning const ret: BufferCursor = { line: 0, column: 0 }; let searchText: string; let offsetInBuffer: (offset: number) => number; if (searcher._wordSeparators) { searchText = buffer.buffer.substring(start, end); offsetInBuffer = (offset: number) => offset + start; searcher.reset(0); } else { searchText = buffer.buffer; offsetInBuffer = (offset: number) => offset; searcher.reset(start); } do { m = searcher.next(searchText); if (m) { if (offsetInBuffer(m.index) >= end) { return resultLen; } this.positionInBuffer(node, offsetInBuffer(m.index) - startOffsetInBuffer, ret); const lineFeedCnt = this.getLineFeedCnt(node.piece.bufferIndex, startCursor, ret); const retStartColumn = ret.line === startCursor.line ? ret.column - startCursor.column + startColumn : ret.column + 1; const retEndColumn = retStartColumn + m[0].length; result[resultLen++] = createFindMatch( new Range( startLineNumber + lineFeedCnt, retStartColumn, startLineNumber + lineFeedCnt, retEndColumn ), m, captureMatches ); if (offsetInBuffer(m.index) + m[0].length >= end) { return resultLen; } if (resultLen >= limitResultCount) { return resultLen; } } } while (m); return resultLen; } public findMatchesLineByLine( searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number ): FindMatch[] { const result: FindMatch[] = []; let resultLen = 0; const searcher = new Searcher(searchData.wordSeparators, searchData.regex); let startPosition = this.nodeAt2(searchRange.startLineNumber, searchRange.startColumn); if (startPosition === null) { return []; } const endPosition = this.nodeAt2(searchRange.endLineNumber, searchRange.endColumn); if (endPosition === null) { return []; } let start = this.positionInBuffer(startPosition.node, startPosition.remainder); const end = this.positionInBuffer(endPosition.node, endPosition.remainder); if (startPosition.node === endPosition.node) { this.findMatchesInNode( startPosition.node, searcher, searchRange.startLineNumber, searchRange.startColumn, start, end, searchData, captureMatches, limitResultCount, resultLen, result ); return result; } let startLineNumber = searchRange.startLineNumber; let currentNode = startPosition.node; while (currentNode !== endPosition.node) { const lineBreakCnt = this.getLineFeedCnt(currentNode.piece.bufferIndex, start, currentNode.piece.end); if (lineBreakCnt >= 1) { // last line break position const lineStarts = this._buffers[currentNode.piece.bufferIndex].lineStarts; const startOffsetInBuffer = this.offsetInBuffer(currentNode.piece.bufferIndex, currentNode.piece.start); const nextLineStartOffset = lineStarts[start.line + lineBreakCnt]; const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1; resultLen = this.findMatchesInNode( currentNode, searcher, startLineNumber, startColumn, start, this.positionInBuffer(currentNode, nextLineStartOffset - startOffsetInBuffer), searchData, captureMatches, limitResultCount, resultLen, result ); if (resultLen >= limitResultCount) { return result; } startLineNumber += lineBreakCnt; } const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0; // search for the remaining content if (startLineNumber === searchRange.endLineNumber) { const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1); resultLen = this._findMatchesInLine( searchData, searcher, text, searchRange.endLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount ); return result; } resultLen = this._findMatchesInLine( searchData, searcher, this.getLineContent(startLineNumber).substr(startColumn), startLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount ); if (resultLen >= limitResultCount) { return result; } startLineNumber++; startPosition = this.nodeAt2(startLineNumber, 1); currentNode = startPosition.node; start = this.positionInBuffer(startPosition.node, startPosition.remainder); } if (startLineNumber === searchRange.endLineNumber) { const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0; const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1); resultLen = this._findMatchesInLine( searchData, searcher, text, searchRange.endLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount ); return result; } const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1; resultLen = this.findMatchesInNode( endPosition.node, searcher, startLineNumber, startColumn, start, end, searchData, captureMatches, limitResultCount, resultLen, result ); return result; } private _findMatchesInLine( searchData: SearchData, searcher: Searcher, text: string, lineNumber: number, deltaOffset: number, resultLen: number, result: FindMatch[], captureMatches: boolean, limitResultCount: number ): number { const wordSeparators = searchData.wordSeparators; if (!captureMatches && searchData.simpleSearch) { const searchString = searchData.simpleSearch; const searchStringLen = searchString.length; const textLength = text.length; let lastMatchIndex = -searchStringLen; while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) { if ( !wordSeparators || isValidMatch(wordSeparators, text, textLength, lastMatchIndex, searchStringLen) ) { result[resultLen++] = new FindMatch( new Range( lineNumber, lastMatchIndex + 1 + deltaOffset, lineNumber, lastMatchIndex + 1 + searchStringLen + deltaOffset ), null ); if (resultLen >= limitResultCount) { return resultLen; } } } return resultLen; } let m: RegExpExecArray | null; // Reset regex to search from the beginning searcher.reset(0); do { m = searcher.next(text); if (m) { result[resultLen++] = createFindMatch( new Range( lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset ), m, captureMatches ); if (resultLen >= limitResultCount) { return resultLen; } } } while (m); return resultLen; } // #endregion // #region Piece Table public insert(offset: number, value: string, eolNormalized: boolean = false): void { this._EOLNormalized = this._EOLNormalized && eolNormalized; this._lastVisitedLine.lineNumber = 0; this._lastVisitedLine.value = ''; if (this.root !== SENTINEL) { const { node, remainder, nodeStartOffset } = this.nodeAt(offset); const piece = node.piece; const bufferIndex = piece.bufferIndex; const insertPosInBuffer = this.positionInBuffer(node, remainder); if ( node.piece.bufferIndex === 0 && piece.end.line === this._lastChangeBufferPos.line && piece.end.column === this._lastChangeBufferPos.column && nodeStartOffset + piece.length === offset && value.length < AverageBufferSize ) { // changed buffer this.appendToNode(node, value); this.computeBufferMetadata(); return; } if (nodeStartOffset === offset) { this.insertContentToNodeLeft(value, node); this._searchCache.validate(offset); } else if (nodeStartOffset + node.piece.length > offset) { // we are inserting into the middle of a node. const nodesToDel: TreeNode[] = []; let newRightPiece = new Piece( piece.bufferIndex, insertPosInBuffer, piece.end, this.getLineFeedCnt(piece.bufferIndex, insertPosInBuffer, piece.end), this.offsetInBuffer(bufferIndex, piece.end) - this.offsetInBuffer(bufferIndex, insertPosInBuffer) ); if (this.shouldCheckCRLF() && this.endWithCR(value)) { const headOfRight = this.nodeCharCodeAt(node, remainder); if (headOfRight === 10 /** \n */) { const newStart: BufferCursor = { line: newRightPiece.start.line + 1, column: 0, }; newRightPiece = new Piece( newRightPiece.bufferIndex, newStart, newRightPiece.end, this.getLineFeedCnt(newRightPiece.bufferIndex, newStart, newRightPiece.end), newRightPiece.length - 1 ); value += '\n'; } } // reuse node for content before insertion point. if (this.shouldCheckCRLF() && this.startWithLF(value)) { const tailOfLeft = this.nodeCharCodeAt(node, remainder - 1); if (tailOfLeft === 13 /** \r */) { const previousPos = this.positionInBuffer(node, remainder - 1); this.deleteNodeTail(node, previousPos); value = '\r' + value; if (node.piece.length === 0) { nodesToDel.push(node); } } else { this.deleteNodeTail(node, insertPosInBuffer); } } else { this.deleteNodeTail(node, insertPosInBuffer); } const newPieces = this.createNewPieces(value); if (newRightPiece.length > 0) { this.rbInsertRight(node, newRightPiece); } let tmpNode = node; for (let k = 0; k < newPieces.length; k++) { tmpNode = this.rbInsertRight(tmpNode, newPieces[k]); } this.deleteNodes(nodesToDel); } else { this.insertContentToNodeRight(value, node); } } else { // insert new node const pieces = this.createNewPieces(value); let node = this.rbInsertLeft(null, pieces[0]); for (let k = 1; k < pieces.length; k++) { node = this.rbInsertRight(node, pieces[k]); } } // todo, this is too brutal. Total line feed count should be updated the same way as lf_left. this.computeBufferMetadata(); } public delete(offset: number, cnt: number): void { this._lastVisitedLine.lineNumber = 0; this._lastVisitedLine.value = ''; if (cnt <= 0 || this.root === SENTINEL) { return; } const startPosition = this.nodeAt(offset); const endPosition = this.nodeAt(offset + cnt); const startNode = startPosition.node; const endNode = endPosition.node; if (startNode === endNode) { const startSplitPosInBuffer = this.positionInBuffer(startNode, startPosition.remainder); const endSplitPosInBuffer = this.positionInBuffer(startNode, endPosition.remainder); if (startPosition.nodeStartOffset === offset) { if (cnt === startNode.piece.length) { // delete node const next = startNode.next(); rbDelete(this, startNode); this.validateCRLFWithPrevNode(next); this.computeBufferMetadata(); return; } this.deleteNodeHead(startNode, endSplitPosInBuffer); this._searchCache.validate(offset); this.validateCRLFWithPrevNode(startNode); this.computeBufferMetadata(); return; } if (startPosition.nodeStartOffset + startNode.piece.length === offset + cnt) { this.deleteNodeTail(startNode, startSplitPosInBuffer); this.validateCRLFWithNextNode(startNode); this.computeBufferMetadata(); return; } // delete content in the middle, this node will be splitted to nodes this.shrinkNode(startNode, startSplitPosInBuffer, endSplitPosInBuffer); this.computeBufferMetadata(); return; } const nodesToDel: TreeNode[] = []; const startSplitPosInBuffer = this.positionInBuffer(startNode, startPosition.remainder); this.deleteNodeTail(startNode, startSplitPosInBuffer); this._searchCache.validate(offset); if (startNode.piece.length === 0) { nodesToDel.push(startNode); } // update last touched node const endSplitPosInBuffer = this.positionInBuffer(endNode, endPosition.remainder); this.deleteNodeHead(endNode, endSplitPosInBuffer); if (endNode.piece.length === 0) { nodesToDel.push(endNode); } // delete nodes in between const secondNode = startNode.next(); for (let node = secondNode; node !== SENTINEL && node !== endNode; node = node.next()) { nodesToDel.push(node); } const prev = startNode.piece.length === 0 ? startNode.prev() : startNode; this.deleteNodes(nodesToDel); this.validateCRLFWithNextNode(prev); this.computeBufferMetadata(); } private insertContentToNodeLeft(value: string, node: TreeNode) { // we are inserting content to the beginning of node const nodesToDel: TreeNode[] = []; if (this.shouldCheckCRLF() && this.endWithCR(value) && this.startWithLF(node)) { // move `\n` to new node. const piece = node.piece; const newStart: BufferCursor = { line: piece.start.line + 1, column: 0 }; const nPiece = new Piece( piece.bufferIndex, newStart, piece.end, this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end), piece.length - 1 ); node.piece = nPiece; value += '\n'; updateTreeMetadata(this, node, -1, -1); if (node.piece.length === 0) { nodesToDel.push(node); } } const newPieces = this.createNewPieces(value); let newNode = this.rbInsertLeft(node, newPieces[newPieces.length - 1]); for (let k = newPieces.length - 2; k >= 0; k--) { newNode = this.rbInsertLeft(newNode, newPieces[k]); } this.validateCRLFWithPrevNode(newNode); this.deleteNodes(nodesToDel); } private insertContentToNodeRight(value: string, node: TreeNode) { // we are inserting to the right of this node. if (this.adjustCarriageReturnFromNext(value, node)) { // move \n to the new node. value += '\n'; } const newPieces = this.createNewPieces(value); const newNode = this.rbInsertRight(node, newPieces[0]); let tmpNode = newNode; for (let k = 1; k < newPieces.length; k++) { tmpNode = this.rbInsertRight(tmpNode, newPieces[k]); } this.validateCRLFWithPrevNode(newNode); } private positionInBuffer(node: TreeNode, remainder: number): BufferCursor; private positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null; private positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null { const piece = node.piece; const bufferIndex = node.piece.bufferIndex; const lineStarts = this._buffers[bufferIndex].lineStarts; const startOffset = lineStarts[piece.start.line] + piece.start.column; const offset = startOffset + remainder; // binary search offset between startOffset and endOffset let low = piece.start.line; let high = piece.end.line; let mid: number = 0; let midStop: number = 0; let midStart: number = 0; while (low <= high) { mid = (low + (high - low) / 2) | 0; midStart = lineStarts[mid]; if (mid === high) { break; } midStop = lineStarts[mid + 1]; if (offset < midStart) { high = mid - 1; } else if (offset >= midStop) { low = mid + 1; } else { break; } } if (ret) { ret.line = mid; ret.column = offset - midStart; return null; } return { line: mid, column: offset - midStart, }; } private getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number { // 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. // 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 1 if (end.column === 0) { return end.line - start.line; } const lineStarts = this._buffers[bufferIndex].lineStarts; if (end.line === lineStarts.length - 1) { // it means, there is no \n after end, otherwise, there will be one more lineStart. return end.line - start.line; } const nextLineStartOffset = lineStarts[end.line + 1]; const endOffset = lineStarts[end.line] + end.column; if (nextLineStartOffset > endOffset + 1) { // there are more than 1 character after end, which means it can't be \n return end.line - start.line; } // endOffset + 1 === nextLineStartOffset // character at endOffset is \n, so we check the character before first // if character at endOffset is \r, end.column is 0 and we can't get here. const previousCharOffset = endOffset - 1; // end.column > 0 so it's okay. const buffer = this._buffers[bufferIndex].buffer; if (buffer.charCodeAt(previousCharOffset) === 13) { return end.line - start.line + 1; } else { return end.line - start.line; } } private offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number { const lineStarts = this._buffers[bufferIndex].lineStarts; return lineStarts[cursor.line] + cursor.column; } private deleteNodes(nodes: TreeNode[]): void { for (let i = 0; i < nodes.length; i++) { rbDelete(this, nodes[i]); } } private createNewPieces(text: string): Piece[] { if (text.length > AverageBufferSize) { // the content is large, operations like substring, charCode becomes slow // so here we split it into smaller chunks, just like what we did for CR/LF normalization const newPieces: Piece[] = []; while (text.length > AverageBufferSize) { const lastChar = text.charCodeAt(AverageBufferSize - 1); let splitText; if (lastChar === CharCode.CarriageReturn || (lastChar >= 0xd800 && lastChar <= 0xdbff)) { // last character is \r or a high surrogate => keep it back splitText = text.substring(0, AverageBufferSize - 1); text = text.substring(AverageBufferSize - 1); } else { splitText = text.substring(0, AverageBufferSize); text = text.substring(AverageBufferSize); } const lineStarts = createLineStartsFast(splitText); newPieces.push( new Piece( this._buffers.length /* buffer index */, { line: 0, column: 0 }, { line: lineStarts.length - 1, column: splitText.length - lineStarts[lineStarts.length - 1], }, lineStarts.length - 1, splitText.length ) ); this._buffers.push(new StringBuffer(splitText, lineStarts)); } const lineStarts = createLineStartsFast(text); newPieces.push( new Piece( this._buffers.length /* buffer index */, { line: 0, column: 0 }, { line: lineStarts.length - 1, column: text.length - lineStarts[lineStarts.length - 1], }, lineStarts.length - 1, text.length ) ); this._buffers.push(new StringBuffer(text, lineStarts)); return newPieces; } let startOffset = this._buffers[0].buffer.length; const lineStarts = createLineStartsFast(text, false); let start = this._lastChangeBufferPos; if ( this._buffers[0].lineStarts[this._buffers[0].lineStarts.length - 1] === startOffset && startOffset !== 0 && this.startWithLF(text) && this.endWithCR(this._buffers[0].buffer) // todo, we can check this._lastChangeBufferPos's column as it's the last one ) { this._lastChangeBufferPos = { line: this._lastChangeBufferPos.line, column: this._lastChangeBufferPos.column + 1, }; start = this._lastChangeBufferPos; for (let i = 0; i < lineStarts.length; i++) { lineStarts[i] += startOffset + 1; } this._buffers[0].lineStarts = ( this._buffers[0].lineStarts).concat(lineStarts.slice(1)); this._buffers[0].buffer += '_' + text; startOffset += 1; } else { if (startOffset !== 0) { for (let i = 0; i < lineStarts.length; i++) { lineStarts[i] += startOffset; } } this._buffers[0].lineStarts = ( this._buffers[0].lineStarts).concat(lineStarts.slice(1)); this._buffers[0].buffer += text; } const endOffset = this._buffers[0].buffer.length; const endIndex = this._buffers[0].lineStarts.length - 1; const endColumn = endOffset - this._buffers[0].lineStarts[endIndex]; const endPos = { line: endIndex, column: endColumn }; const newPiece = new Piece( 0 /** todo@peng */, start, endPos, this.getLineFeedCnt(0, start, endPos), endOffset - startOffset ); this._lastChangeBufferPos = endPos; return [newPiece]; } public getLinesRawContent(): string { return this.getContentOfSubTree(this.root); } public getLineRawContent(lineNumber: number, endOffset: number = 0): string { let x = this.root; let ret = ''; const cache = this._searchCache.get2(lineNumber); if (cache) { x = cache.node; const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - cache.nodeStartLineNumber - 1); const buffer = this._buffers[x.piece.bufferIndex].buffer; const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); if (cache.nodeStartLineNumber + x.piece.lineFeedCnt === lineNumber) { ret = buffer.substring(startOffset + prevAccumulatedValue, startOffset + x.piece.length); } else { const accumulatedValue = this.getAccumulatedValue(x, lineNumber - cache.nodeStartLineNumber); return buffer.substring(startOffset + prevAccumulatedValue, startOffset + accumulatedValue - endOffset); } } else { let nodeStartOffset = 0; const originalLineNumber = lineNumber; while (x !== SENTINEL) { if (x.left !== SENTINEL && x.lf_left >= lineNumber - 1) { x = x.left; } else if (x.lf_left + x.piece.lineFeedCnt > lineNumber - 1) { const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); const accumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 1); const buffer = this._buffers[x.piece.bufferIndex].buffer; const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); nodeStartOffset += x.size_left; this._searchCache.set({ node: x, nodeStartOffset, nodeStartLineNumber: originalLineNumber - (lineNumber - 1 - x.lf_left), }); return buffer.substring( startOffset + prevAccumulatedValue, startOffset + accumulatedValue - endOffset ); } else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) { const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); const buffer = this._buffers[x.piece.bufferIndex].buffer; const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); ret = buffer.substring(startOffset + prevAccumulatedValue, startOffset + x.piece.length); break; } else { lineNumber -= x.lf_left + x.piece.lineFeedCnt; nodeStartOffset += x.size_left + x.piece.length; x = x.right; } } } // search in order, to find the node contains end column x = x.next(); while (x !== SENTINEL) { const buffer = this._buffers[x.piece.bufferIndex].buffer; if (x.piece.lineFeedCnt > 0) { const accumulatedValue = this.getAccumulatedValue(x, 0); const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); ret += buffer.substring(startOffset, startOffset + accumulatedValue - endOffset); return ret; } else { const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); ret += buffer.substr(startOffset, x.piece.length); } x = x.next(); } return ret; } private computeBufferMetadata() { let x = this.root; let lfCnt = 1; let len = 0; while (x !== SENTINEL) { lfCnt += x.lf_left + x.piece.lineFeedCnt; len += x.size_left + x.piece.length; x = x.right; } this._lineCnt = lfCnt; this._length = len; this._searchCache.validate(this._length); } // #region node operations private getIndexOf(node: TreeNode, accumulatedValue: number): { index: number; remainder: number } { const piece = node.piece; const pos = this.positionInBuffer(node, accumulatedValue); const lineCnt = pos.line - piece.start.line; if ( this.offsetInBuffer(piece.bufferIndex, piece.end) - this.offsetInBuffer(piece.bufferIndex, piece.start) === accumulatedValue ) { // we are checking the end of this node, so a CRLF check is necessary. const realLineCnt = this.getLineFeedCnt(node.piece.bufferIndex, piece.start, pos); if (realLineCnt !== lineCnt) { // aha yes, CRLF return { index: realLineCnt, remainder: 0 }; } } return { index: lineCnt, remainder: pos.column }; } private getAccumulatedValue(node: TreeNode, index: number) { if (index < 0) { return 0; } const piece = node.piece; const lineStarts = this._buffers[piece.bufferIndex].lineStarts; const expectedLineStartIndex = piece.start.line + index + 1; if (expectedLineStartIndex > piece.end.line) { return lineStarts[piece.end.line] + piece.end.column - lineStarts[piece.start.line] - piece.start.column; } else { return lineStarts[expectedLineStartIndex] - lineStarts[piece.start.line] - piece.start.column; } } private deleteNodeTail(node: TreeNode, pos: BufferCursor) { const piece = node.piece; const originalLFCnt = piece.lineFeedCnt; const originalEndOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); const newEnd = pos; const newEndOffset = this.offsetInBuffer(piece.bufferIndex, newEnd); const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, newEnd); const lf_delta = newLineFeedCnt - originalLFCnt; const size_delta = newEndOffset - originalEndOffset; const newLength = piece.length + size_delta; node.piece = new Piece(piece.bufferIndex, piece.start, newEnd, newLineFeedCnt, newLength); updateTreeMetadata(this, node, size_delta, lf_delta); } private deleteNodeHead(node: TreeNode, pos: BufferCursor) { const piece = node.piece; const originalLFCnt = piece.lineFeedCnt; const originalStartOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); const newStart = pos; const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end); const newStartOffset = this.offsetInBuffer(piece.bufferIndex, newStart); const lf_delta = newLineFeedCnt - originalLFCnt; const size_delta = originalStartOffset - newStartOffset; const newLength = piece.length + size_delta; node.piece = new Piece(piece.bufferIndex, newStart, piece.end, newLineFeedCnt, newLength); updateTreeMetadata(this, node, size_delta, lf_delta); } private shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { const piece = node.piece; const originalStartPos = piece.start; const originalEndPos = piece.end; // old piece, originalStartPos, start const oldLength = piece.length; const oldLFCnt = piece.lineFeedCnt; const newEnd = start; const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, newEnd); const newLength = this.offsetInBuffer(piece.bufferIndex, start) - this.offsetInBuffer(piece.bufferIndex, originalStartPos); node.piece = new Piece(piece.bufferIndex, piece.start, newEnd, newLineFeedCnt, newLength); updateTreeMetadata(this, node, newLength - oldLength, newLineFeedCnt - oldLFCnt); // new right piece, end, originalEndPos const newPiece = new Piece( piece.bufferIndex, end, originalEndPos, this.getLineFeedCnt(piece.bufferIndex, end, originalEndPos), this.offsetInBuffer(piece.bufferIndex, originalEndPos) - this.offsetInBuffer(piece.bufferIndex, end) ); const newNode = this.rbInsertRight(node, newPiece); this.validateCRLFWithPrevNode(newNode); } private appendToNode(node: TreeNode, value: string): void { if (this.adjustCarriageReturnFromNext(value, node)) { value += '\n'; } const hitCRLF = this.shouldCheckCRLF() && this.startWithLF(value) && this.endWithCR(node); const startOffset = this._buffers[0].buffer.length; this._buffers[0].buffer += value; const lineStarts = createLineStartsFast(value, false); for (let i = 0; i < lineStarts.length; i++) { lineStarts[i] += startOffset; } if (hitCRLF) { const prevStartOffset = this._buffers[0].lineStarts[this._buffers[0].lineStarts.length - 2]; ( this._buffers[0].lineStarts).pop(); // _lastChangeBufferPos is already wrong this._lastChangeBufferPos = { line: this._lastChangeBufferPos.line - 1, column: startOffset - prevStartOffset, }; } this._buffers[0].lineStarts = ( this._buffers[0].lineStarts).concat(lineStarts.slice(1)); const endIndex = this._buffers[0].lineStarts.length - 1; const endColumn = this._buffers[0].buffer.length - this._buffers[0].lineStarts[endIndex]; const newEnd = { line: endIndex, column: endColumn }; const newLength = node.piece.length + value.length; const oldLineFeedCnt = node.piece.lineFeedCnt; const newLineFeedCnt = this.getLineFeedCnt(0, node.piece.start, newEnd); const lf_delta = newLineFeedCnt - oldLineFeedCnt; node.piece = new Piece(node.piece.bufferIndex, node.piece.start, newEnd, newLineFeedCnt, newLength); this._lastChangeBufferPos = newEnd; updateTreeMetadata(this, node, value.length, lf_delta); } private nodeAt(offset: number): NodePosition { let x = this.root; const cache = this._searchCache.get(offset); if (cache) { return { node: cache.node, nodeStartOffset: cache.nodeStartOffset, remainder: offset - cache.nodeStartOffset, }; } let nodeStartOffset = 0; while (x !== SENTINEL) { if (x.size_left > offset) { x = x.left; } else if (x.size_left + x.piece.length >= offset) { nodeStartOffset += x.size_left; const ret = { node: x, remainder: offset - x.size_left, nodeStartOffset, }; this._searchCache.set(ret); return ret; } else { offset -= x.size_left + x.piece.length; nodeStartOffset += x.size_left + x.piece.length; x = x.right; } } return null!; } private nodeAt2(lineNumber: number, column: number): NodePosition { let x = this.root; let nodeStartOffset = 0; while (x !== SENTINEL) { if (x.left !== SENTINEL && x.lf_left >= lineNumber - 1) { x = x.left; } else if (x.lf_left + x.piece.lineFeedCnt > lineNumber - 1) { const prevAccumualtedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); const accumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 1); nodeStartOffset += x.size_left; return { node: x, remainder: Math.min(prevAccumualtedValue + column - 1, accumulatedValue), nodeStartOffset, }; } else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) { const prevAccumualtedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); if (prevAccumualtedValue + column - 1 <= x.piece.length) { return { node: x, remainder: prevAccumualtedValue + column - 1, nodeStartOffset, }; } else { column -= x.piece.length - prevAccumualtedValue; break; } } else { lineNumber -= x.lf_left + x.piece.lineFeedCnt; nodeStartOffset += x.size_left + x.piece.length; x = x.right; } } // search in order, to find the node contains position.column x = x.next(); while (x !== SENTINEL) { if (x.piece.lineFeedCnt > 0) { const accumulatedValue = this.getAccumulatedValue(x, 0); const nodeStartOffset = this.offsetOfNode(x); return { node: x, remainder: Math.min(column - 1, accumulatedValue), nodeStartOffset, }; } else { if (x.piece.length >= column - 1) { const nodeStartOffset = this.offsetOfNode(x); return { node: x, remainder: column - 1, nodeStartOffset, }; } else { column -= x.piece.length; } } x = x.next(); } return null!; } private nodeCharCodeAt(node: TreeNode, offset: number): number { if (node.piece.lineFeedCnt < 1) { return -1; } const buffer = this._buffers[node.piece.bufferIndex]; const newOffset = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start) + offset; return buffer.buffer.charCodeAt(newOffset); } private offsetOfNode(node: TreeNode): number { if (!node) { return 0; } let pos = node.size_left; while (node !== this.root) { if (node.parent.right === node) { pos += node.parent.size_left + node.parent.piece.length; } node = node.parent; } return pos; } // #endregion // #region CRLF private shouldCheckCRLF() { return !(this._EOLNormalized && this._EOL === '\n'); } private startWithLF(val: string | TreeNode): boolean { if (typeof val === 'string') { return val.charCodeAt(0) === 10; } if (val === SENTINEL || val.piece.lineFeedCnt === 0) { return false; } const piece = val.piece; const lineStarts = this._buffers[piece.bufferIndex].lineStarts; const line = piece.start.line; const startOffset = lineStarts[line] + piece.start.column; if (line === lineStarts.length - 1) { // last line, so there is no line feed at the end of this line return false; } const nextLineOffset = lineStarts[line + 1]; if (nextLineOffset > startOffset + 1) { return false; } return this._buffers[piece.bufferIndex].buffer.charCodeAt(startOffset) === 10; } private endWithCR(val: string | TreeNode): boolean { if (typeof val === 'string') { return val.charCodeAt(val.length - 1) === 13; } if (val === SENTINEL || val.piece.lineFeedCnt === 0) { return false; } return this.nodeCharCodeAt(val, val.piece.length - 1) === 13; } private validateCRLFWithPrevNode(nextNode: TreeNode) { if (this.shouldCheckCRLF() && this.startWithLF(nextNode)) { const node = nextNode.prev(); if (this.endWithCR(node)) { this.fixCRLF(node, nextNode); } } } private validateCRLFWithNextNode(node: TreeNode) { if (this.shouldCheckCRLF() && this.endWithCR(node)) { const nextNode = node.next(); if (this.startWithLF(nextNode)) { this.fixCRLF(node, nextNode); } } } private fixCRLF(prev: TreeNode, next: TreeNode) { const nodesToDel: TreeNode[] = []; // update node const lineStarts = this._buffers[prev.piece.bufferIndex].lineStarts; let newEnd: BufferCursor; if (prev.piece.end.column === 0) { // it means, last line ends with \r, not \r\n newEnd = { line: prev.piece.end.line - 1, column: lineStarts[prev.piece.end.line] - lineStarts[prev.piece.end.line - 1] - 1, }; } else { // \r\n newEnd = { line: prev.piece.end.line, column: prev.piece.end.column - 1 }; } const prevNewLength = prev.piece.length - 1; const prevNewLFCnt = prev.piece.lineFeedCnt - 1; prev.piece = new Piece(prev.piece.bufferIndex, prev.piece.start, newEnd, prevNewLFCnt, prevNewLength); updateTreeMetadata(this, prev, -1, -1); if (prev.piece.length === 0) { nodesToDel.push(prev); } // update nextNode const newStart: BufferCursor = { line: next.piece.start.line + 1, column: 0, }; const newLength = next.piece.length - 1; const newLineFeedCnt = this.getLineFeedCnt(next.piece.bufferIndex, newStart, next.piece.end); next.piece = new Piece(next.piece.bufferIndex, newStart, next.piece.end, newLineFeedCnt, newLength); updateTreeMetadata(this, next, -1, -1); if (next.piece.length === 0) { nodesToDel.push(next); } // create new piece which contains \r\n const pieces = this.createNewPieces('\r\n'); this.rbInsertRight(prev, pieces[0]); // delete empty nodes for (let i = 0; i < nodesToDel.length; i++) { rbDelete(this, nodesToDel[i]); } } private adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean { if (this.shouldCheckCRLF() && this.endWithCR(value)) { const nextNode = node.next(); if (this.startWithLF(nextNode)) { // move `\n` forward value += '\n'; if (nextNode.piece.length === 1) { rbDelete(this, nextNode); } else { const piece = nextNode.piece; const newStart: BufferCursor = { line: piece.start.line + 1, column: 0, }; const newLength = piece.length - 1; const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end); nextNode.piece = new Piece(piece.bufferIndex, newStart, piece.end, newLineFeedCnt, newLength); updateTreeMetadata(this, nextNode, -1, -1); } return true; } } return false; } // #endregion // #endregion // #region Tree operations iterate(node: TreeNode, callback: (node: TreeNode) => boolean): boolean { if (node === SENTINEL) { return callback(SENTINEL); } const leftRet = this.iterate(node.left, callback); if (!leftRet) { return leftRet; } return callback(node) && this.iterate(node.right, callback); } private getNodeContent(node: TreeNode) { if (node === SENTINEL) { return ''; } const buffer = this._buffers[node.piece.bufferIndex]; const piece = node.piece; const startOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); const endOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); const currentContent = buffer.buffer.substring(startOffset, endOffset); return currentContent; } getPieceContent(piece: Piece) { const buffer = this._buffers[piece.bufferIndex]; const startOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); const endOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); const currentContent = buffer.buffer.substring(startOffset, endOffset); return currentContent; } /** * node node * / \ / \ * a b <---- a b * / * z */ private rbInsertRight(node: TreeNode | null, p: Piece): TreeNode { const z = new TreeNode(p, NodeColor.Red); z.left = SENTINEL; z.right = SENTINEL; z.parent = SENTINEL; z.size_left = 0; z.lf_left = 0; const x = this.root; if (x === SENTINEL) { this.root = z; z.color = NodeColor.Black; } else if (node!.right === SENTINEL) { node!.right = z; z.parent = node!; } else { const nextNode = leftest(node!.right); nextNode.left = z; z.parent = nextNode; } fixInsert(this, z); return z; } /** * node node * / \ / \ * a b ----> a b * \ * z */ private rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode { const z = new TreeNode(p, NodeColor.Red); z.left = SENTINEL; z.right = SENTINEL; z.parent = SENTINEL; z.size_left = 0; z.lf_left = 0; if (this.root === SENTINEL) { this.root = z; z.color = NodeColor.Black; } else if (node!.left === SENTINEL) { node!.left = z; z.parent = node!; } else { const prevNode = righttest(node!.left); // a prevNode.right = z; z.parent = prevNode; } fixInsert(this, z); return z; } private getContentOfSubTree(node: TreeNode): string { let str = ''; this.iterate(node, node => { str += this.getNodeContent(node); return true; }); return str; } // #endregion }