diff --git a/src/vs/editor/common/model/indentationGuesser.ts b/src/vs/editor/common/model/indentationGuesser.ts index 56c6683c54381aea9b2c39a0de76e0909c488bb9..aa1dab5901a8fa85165d361984232a0ede0c26d4 100644 --- a/src/vs/editor/common/model/indentationGuesser.ts +++ b/src/vs/editor/common/model/indentationGuesser.ts @@ -6,6 +6,8 @@ import { CharCode } from 'vs/base/common/charCode'; import { TextBuffer } from 'vs/editor/common/model/textBuffer'; +import { TextBuffer as TextBuffer2 } from 'vs/editor/common/model/textBuffer2'; +import { IRawPTBuffer } from './textSource'; export interface IIndentationGuesserTarget { getLineCount(): number; @@ -15,7 +17,7 @@ export interface IIndentationGuesserTarget { export class IndentationGuesserTextBufferTarget implements IIndentationGuesserTarget { constructor( - private readonly _buffer: TextBuffer + private readonly _buffer: TextBuffer | TextBuffer2 ) { } public getLineCount(): number { @@ -42,6 +44,27 @@ export class IndentationGuesserStringArrayTarget implements IIndentationGuesserT } } +export class IndentationGuesserRawTextBufferTarget implements IIndentationGuesserTarget { + + constructor( + private readonly _rawBuffer: IRawPTBuffer + ) { } + + public getLineCount(): number { + return this._rawBuffer.length; + } + + public getLineContent(lineNumber: number): string { + if (lineNumber === 1) { + return this._rawBuffer.text.substring(0, this._rawBuffer.lineStarts[0]); + } else if (lineNumber === this._rawBuffer.lineStarts.length + 1) { + return this._rawBuffer.text.substring(this._rawBuffer.lineStarts[this._rawBuffer.lineStarts.length - 1] + 1); + } + + return this._rawBuffer.text.substring(this._rawBuffer.lineStarts[lineNumber - 2] + 1, this._rawBuffer.lineStarts[lineNumber - 1]); + } +} + /** * Compute the diff in spaces between two line's indentation. */ diff --git a/src/vs/editor/common/model/modelLine.ts b/src/vs/editor/common/model/modelLine.ts index 68c616b7dec32cbb18a1b2e57d7d9e4bb2fe0622..80eaef912332f30b0991fe3ea8af7f206d2311d5 100644 --- a/src/vs/editor/common/model/modelLine.ts +++ b/src/vs/editor/common/model/modelLine.ts @@ -13,6 +13,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IModelTokensChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { onUnexpectedError } from 'vs/base/common/errors'; import { TextBuffer } from 'vs/editor/common/model/textBuffer'; +import { TextBuffer as TextBuffer2 } from 'vs/editor/common/model/textBuffer2'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { nullTokenize2 } from 'vs/editor/common/modes/nullMode'; @@ -253,7 +254,7 @@ export class ModelLinesTokens { return (firstInvalidLineNumber >= lineNumber); } - public hasLinesToTokenize(buffer: TextBuffer): boolean { + public hasLinesToTokenize(buffer: TextBuffer | TextBuffer2): boolean { return (this._invalidLineStartIndex < buffer.getLineCount()); } @@ -419,7 +420,7 @@ export class ModelLinesTokens { //#region Tokenization - public _tokenizeOneLine(buffer: TextBuffer, eventBuilder: ModelTokensChangedEventBuilder): number { + public _tokenizeOneLine(buffer: TextBuffer | TextBuffer2, eventBuilder: ModelTokensChangedEventBuilder): number { if (!this.hasLinesToTokenize(buffer)) { return buffer.getLineCount() + 1; } @@ -428,7 +429,7 @@ export class ModelLinesTokens { return lineNumber; } - public _updateTokensUntilLine(buffer: TextBuffer, eventBuilder: ModelTokensChangedEventBuilder, lineNumber: number): void { + public _updateTokensUntilLine(buffer: TextBuffer | TextBuffer2, eventBuilder: ModelTokensChangedEventBuilder, lineNumber: number): void { if (!this.tokenizationSupport) { this._invalidLineStartIndex = buffer.getLineCount(); return; diff --git a/src/vs/editor/common/model/textBuffer.ts b/src/vs/editor/common/model/textBuffer.ts index 87f19776d657336f27deaf7b4d253a857bdf186f..c8c70440b01e9d093ae5be4850acbd21af69bc31 100644 --- a/src/vs/editor/common/model/textBuffer.ts +++ b/src/vs/editor/common/model/textBuffer.ts @@ -55,7 +55,7 @@ export class TextBuffer { private _lineStarts: PrefixSumComputer; constructor(textSource: ITextSource) { - this._lines = textSource.lines.slice(0); + this._lines = (textSource.lines).slice(0); this._BOM = textSource.BOM; this._EOL = textSource.EOL; this._mightContainRTL = textSource.containsRTL; diff --git a/src/vs/editor/common/model/textBuffer2.ts b/src/vs/editor/common/model/textBuffer2.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b5a6da72a4a9801dc356ae45b43de2cef02eccb --- /dev/null +++ b/src/vs/editor/common/model/textBuffer2.ts @@ -0,0 +1,1711 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import * as strings from 'vs/base/common/strings'; +import { PrefixSumComputer, PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { ITextSource, IRawPTBuffer } from 'vs/editor/common/model/textSource'; +import { ApplyEditResult, IInternalModelContentChange } from 'vs/editor/common/model/textBuffer'; +import { IValidatedEditOperation } from 'vs/editor/common/model/editableTextModel'; +import { ModelRawChange, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/model/textModelEvents'; + +export const enum NodeColor { + Black = 0, + Red = 1, +} + +export let error = { + sizeLeft: false +}; + +function getNodeColor(node: TreeNode) { + return node.color; +} + +function setNodeColor(node: TreeNode, color: NodeColor) { + node.color = color; +} + +function leftest(node: TreeNode): TreeNode { + while (node.left !== SENTINEL) { + node = node.left; + } + return node; +} + +function righttest(node: TreeNode): TreeNode { + while (node.right !== SENTINEL) { + node = node.right; + } + return node; +} + +function calculateSize(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.size_left + node.piece.length + calculateSize(node.right); +} + +function calculateLF(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.lf_left + node.piece.lineFeedCnt + calculateLF(node.right); +} + +function resetSentinel(): void { + SENTINEL.parent = SENTINEL; +} + +export class TreeNode { + parent: TreeNode; + left: TreeNode; + right: TreeNode; + color: NodeColor; + + // Piece + piece: Piece; + size_left: number; // size of the left subtree (not inorder) + lf_left: number; // line feeds cnt in the left subtree (not in order) + + constructor(piece: Piece, color: NodeColor) { + this.piece = piece; + this.color = color; + this.size_left = 0; + this.lf_left = 0; + this.parent = null; + this.left = null; + this.right = null; + } + + public next(): TreeNode { + if (this.right !== SENTINEL) { + return leftest(this.right); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.left === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + // root + // if (node.right === SENTINEL) { + return SENTINEL; + // } + // return leftest(node.right); + } else { + return node.parent; + } + } + + public prev(): TreeNode { + if (this.left !== SENTINEL) { + return righttest(this.left); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.right === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + // root + // if (node.left === SENTINEL) { + return SENTINEL; + // } + // return righttest(node.left); + } else { + return node.parent; + } + } + + public detach(): void { + this.parent = null; + this.left = null; + this.right = null; + } +} + +export const SENTINEL: TreeNode = new TreeNode(null, NodeColor.Black); +SENTINEL.parent = SENTINEL; +SENTINEL.left = SENTINEL; +SENTINEL.right = SENTINEL; +setNodeColor(SENTINEL, NodeColor.Black); + +export interface BufferCursor { + /** + * Piece Index + */ + node: TreeNode; + /** + * remainer in current piece. + */ + remainder: number; +} + +export class Piece { + isOriginalBuffer: boolean; + offset: number; + length: number; // size of current piece + + lineFeedCnt: number; + lineStarts: PrefixSumComputer; + + constructor(isOriginalBuffer: boolean, offset: number, length: number, lineFeedCnt: number, lineLengthsVal: Uint32Array) { + this.isOriginalBuffer = isOriginalBuffer; + this.offset = offset; + this.length = length; + this.lineFeedCnt = lineFeedCnt; + this.lineStarts = null; + + if (lineLengthsVal) { + let newVal = new Uint32Array(lineLengthsVal.length); + newVal.set(lineLengthsVal); + this.lineStarts = new PrefixSumComputer(newVal); + } + } +} + +export class TextBuffer { + private _BOM: string; + private _EOL: string; + private _mightContainRTL: boolean; + private _mightContainNonBasicASCII: boolean; + + private _originalBuffer: string; + private _changeBuffer: string; + private _regex: RegExp; + root: TreeNode; + + constructor(textSource: ITextSource) { + let rawBuffer = textSource.lines; + this._originalBuffer = rawBuffer.text; + this._changeBuffer = ''; + this.root = SENTINEL; + this._BOM = textSource.BOM; + this._EOL = textSource.EOL; + this._mightContainNonBasicASCII = !textSource.isBasicASCII; + this._mightContainRTL = textSource.containsRTL; + this._regex = new RegExp(/\r\n|\r|\n/g); + + if (this._originalBuffer.length > 0) { + let { lineFeedCount, lineLengths } = this.calculateNewLineCount(this._originalBuffer); + let piece = new Piece(true, 0, this._originalBuffer.length, lineFeedCount, lineLengths); + this.rbInsertLeft(null, piece); + } + } + + // #region TextBuffer + public getLinesContent() { + return this.getContentOfSubTree(this.root); + } + + public getLineCount(): number { + let x = this.root; + + let ret = 1; + while (x !== SENTINEL) { + ret += x.lf_left + x.piece.lineFeedCnt; + x = x.right; + } + + return ret; + } + + public getValueInRange(range: Range, eol: editorCommon.EndOfLinePreference = editorCommon.EndOfLinePreference.TextDefined): string { + // todo, validate range. + if (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn) { + return ''; + } + + let startPosition = this.nodeAt2(new Position(range.startLineNumber, range.startColumn)); + let endPosition = this.nodeAt2(new Position(range.endLineNumber, range.endColumn)); + + if (startPosition.node === endPosition.node) { + let node = startPosition.node; + let buffer = node.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + return buffer.substring(node.piece.offset + startPosition.remainder, node.piece.offset + endPosition.remainder); + } + + + let x = startPosition.node; + let buffer = x.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + let ret = buffer.substring(x.piece.offset + startPosition.remainder, x.piece.offset + x.piece.length); + + x = x.next(); + while (x !== SENTINEL) { + let buffer = x.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + + if (x === endPosition.node) { + ret += buffer.substring(x.piece.offset, x.piece.offset + endPosition.remainder); + break; + } else { + ret += buffer.substr(x.piece.offset, x.piece.length); + } + + x = x.next(); + } + + return ret; + } + + public getLineContent(lineNumber: number): string { + let x = this.root; + + let ret = ''; + 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) { + let prevAccumualtedValue = x.piece.lineStarts.getAccumulatedValue(lineNumber - x.lf_left - 2); + let accumualtedValue = x.piece.lineStarts.getAccumulatedValue(lineNumber - x.lf_left - 1); + let buffer = x.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + + return buffer.substring(x.piece.offset + prevAccumualtedValue, x.piece.offset + accumualtedValue); + } else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) { + let prevAccumualtedValue = x.piece.lineStarts.getAccumulatedValue(lineNumber - x.lf_left - 2); + let buffer = x.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + + ret = buffer.substring(x.piece.offset + prevAccumualtedValue, x.piece.offset + x.piece.length); + break; + } else { + lineNumber -= x.lf_left + x.piece.lineFeedCnt; + x = x.right; + } + } + + // if (x === SENTINEL) { + // throw('not possible'); + // } + + // search in order, to find the node contains end column + x = x.next(); + while (x !== SENTINEL) { + let buffer = x.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + + if (x.piece.lineFeedCnt > 0) { + let accumualtedValue = x.piece.lineStarts.getAccumulatedValue(0); + + ret += buffer.substring(x.piece.offset, x.piece.offset + accumualtedValue); + return ret; + } else { + ret += buffer.substr(x.piece.offset, x.piece.length); + } + + x = x.next(); + } + + return ret; + + } + + public getOffsetAt(position: Position): number { + return this.getOffsetAt2(position.lineNumber, position.column); + } + + public getOffsetAt2(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 + let accumualtedValInCurrentIndex = x.piece.lineStarts.getAccumulatedValue(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 { + let x = this.root; + let lfCnt = 0; + + while (x !== SENTINEL) { + if (x.size_left !== 0 && x.size_left >= offset) { + x = x.left; + } else if (x.size_left + x.piece.length >= offset) { + let out = x.piece.lineStarts.getIndexOf(offset - x.size_left); + + let column = 0; + + if (out.index === 0) { + let prev = x.prev(); + + if (prev !== SENTINEL) { + let lineLens = prev.piece.lineStarts.values; + column += lineLens[lineLens.length - 1]; + } + } + + lfCnt += x.lf_left + out.index; + return new Position(lfCnt + 1, column + out.remainder + 1); + } else { + offset -= x.size_left + x.piece.length; + lfCnt += x.lf_left + x.piece.lineFeedCnt; + x = x.right; + } + } + + return null; + } + + public equals(other: ITextSource): boolean { + if (this._BOM !== other.BOM) { + return false; + } + if (this._EOL !== other.EOL) { + return false; + } + if (this.getLinesContent() !== (other.lines).text) { + return false; + } + return true; + } + + public mightContainRTL(): boolean { + return this._mightContainRTL; + } + + public mightContainNonBasicASCII(): boolean { + return this._mightContainNonBasicASCII; + } + + public getBOM(): string { + return this._BOM; + } + + public getEOL(): string { + return this._EOL; + } + + public setEOL(newEOL: string): void { + this._EOL = newEOL; + // this._constructLineStarts(); + } + + public _applyEdits(rawOperations: editorCommon.IIdentifiedSingleEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditResult { + let mightContainRTL = this._mightContainRTL; + let mightContainNonBasicASCII = this._mightContainNonBasicASCII; + let canReduceOperations = true; + + let operations: IValidatedEditOperation[] = []; + for (let i = 0; i < rawOperations.length; i++) { + let op = rawOperations[i]; + if (canReduceOperations && op._isTracked) { + canReduceOperations = false; + } + let validatedRange = op.range; + if (!mightContainRTL && op.text) { + // check if the new inserted text contains RTL + mightContainRTL = strings.containsRTL(op.text); + } + if (!mightContainNonBasicASCII && op.text) { + mightContainNonBasicASCII = !strings.isBasicASCII(op.text); + } + operations[i] = { + sortIndex: i, + identifier: op.identifier, + range: validatedRange, + rangeOffset: this.getOffsetAt(validatedRange.getStartPosition()), + rangeLength: this.getValueLengthInRange(validatedRange), + lines: op.text ? op.text.split(/\r\n|\r|\n/) : null, + forceMoveMarkers: op.forceMoveMarkers, + isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false + }; + } + + // Sort operations ascending + operations.sort(TextBuffer._sortOpsAscending); + + for (let i = 0, count = operations.length - 1; i < count; i++) { + let rangeEnd = operations[i].range.getEndPosition(); + let nextRangeStart = operations[i + 1].range.getStartPosition(); + + if (nextRangeStart.isBefore(rangeEnd)) { + // overlapping ranges + throw new Error('Overlapping ranges are not allowed!'); + } + } + + if (canReduceOperations) { + // operations = this._reduceOperations(operations); + } + + // Delta encode operations + let reverseRanges = TextBuffer._getInverseEditRanges(operations); + let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = []; + + for (let i = 0; i < operations.length; i++) { + let op = operations[i]; + let reverseRange = reverseRanges[i]; + + if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) { + // Record already the future line numbers that might be auto whitespace removal candidates on next edit + for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) { + let currentLineContent = ''; + if (lineNumber === reverseRange.startLineNumber) { + currentLineContent = this.getLineContent(op.range.startLineNumber); + if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) { + continue; + } + } + newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent }); + } + } + } + + let reverseOperations: editorCommon.IIdentifiedSingleEditOperation[] = []; + for (let i = 0; i < operations.length; i++) { + let op = operations[i]; + let reverseRange = reverseRanges[i]; + + reverseOperations[i] = { + identifier: op.identifier, + range: reverseRange, + text: this.getValueInRange(op.range), + forceMoveMarkers: op.forceMoveMarkers + }; + } + + this._mightContainRTL = mightContainRTL; + this._mightContainNonBasicASCII = mightContainNonBasicASCII; + + const [rawContentChanges, contentChanges] = this._doApplyEdits(operations); + + let trimAutoWhitespaceLineNumbers: number[] = null; + if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { + // sort line numbers auto whitespace removal candidates for next edit descending + newTrimAutoWhitespaceCandidates.sort((a, b) => b.lineNumber - a.lineNumber); + + trimAutoWhitespaceLineNumbers = []; + for (let i = 0, len = newTrimAutoWhitespaceCandidates.length; i < len; i++) { + let lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber; + if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) { + // Do not have the same line number twice + continue; + } + + let prevContent = newTrimAutoWhitespaceCandidates[i].oldContent; + let lineContent = this.getLineContent(lineNumber); + + if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) { + continue; + } + + trimAutoWhitespaceLineNumbers.push(lineNumber); + } + } + + return new ApplyEditResult( + reverseOperations, + rawContentChanges, + contentChanges, + trimAutoWhitespaceLineNumbers + ); + } + + private _doApplyEdits(operations: IValidatedEditOperation[]): [ModelRawChange[], IInternalModelContentChange[]] { + operations.sort(TextBuffer._sortOpsDescending); + + let rawContentChanges: ModelRawChange[] = []; + let contentChanges: IInternalModelContentChange[] = []; + + // operations are from bottom to top + for (let i = 0; i < operations.length; i++) { + let op = operations[i]; + + const startLineNumber = op.range.startLineNumber; + const startColumn = op.range.startColumn; + const endLineNumber = op.range.endLineNumber; + const endColumn = op.range.endColumn; + + if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.lines || op.lines.length === 0)) { + // no-op + continue; + } + + const deletingLinesCnt = endLineNumber - startLineNumber; + const insertingLinesCnt = (op.lines ? op.lines.length - 1 : 0); + const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt); + + const text = (op.lines ? op.lines.join(this.getEOL()) : ''); + + if (text) { + // replacement + this.delete(op.rangeOffset, op.rangeLength); + this.insert(text, op.rangeOffset); + + } else { + // deletion + this.delete(op.rangeOffset, op.rangeLength); + } + + for (let j = startLineNumber; j <= startLineNumber + editingLinesCnt; j++) { + rawContentChanges.push( + new ModelRawLineChanged(j, this.getLineContent(j)) + ); + } + + if (editingLinesCnt < deletingLinesCnt) { + rawContentChanges.push( + new ModelRawLinesDeleted(startLineNumber + editingLinesCnt + 1, endLineNumber) + ); + } + + if (editingLinesCnt < insertingLinesCnt) { + let newLinesContent: string[] = []; + for (let j = editingLinesCnt + 1; j <= insertingLinesCnt; j++) { + newLinesContent.push(op.lines[j]); + } + + newLinesContent[newLinesContent.length - 1] = this.getLineContent(startLineNumber + insertingLinesCnt - 1); + + rawContentChanges.push( + new ModelRawLinesInserted(startLineNumber + editingLinesCnt + 1, startLineNumber + insertingLinesCnt, newLinesContent.join('\n')) + ); + } + + const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn); + contentChanges.push({ + range: contentChangeRange, + rangeLength: op.rangeLength, + text: text, + lines: op.lines, + rangeOffset: op.rangeOffset, + forceMoveMarkers: op.forceMoveMarkers + }); + } + return [rawContentChanges, contentChanges]; + } + + public getValueLengthInRange(range: Range, eol: editorCommon.EndOfLinePreference = editorCommon.EndOfLinePreference.TextDefined): number { + if (range.isEmpty()) { + return 0; + } + + if (range.startLineNumber === range.endLineNumber) { + return (range.endColumn - range.startColumn); + } + + let startOffset = this.getOffsetAt(new Position(range.startLineNumber, range.startColumn)); + let endOffset = this.getOffsetAt(new Position(range.endLineNumber, range.endColumn)); + return endOffset - startOffset; + } + + public getLineCharCode(lineNumber: number, index: number): number { + return this.getLineContent(lineNumber).charCodeAt(index); + } + + public getLineLength(lineNumber: number): number { + return this.getLineContent(lineNumber).length; + } + + public getLineMinColumn(lineNumber: number): number { + return 1; + } + + public getLineMaxColumn(lineNumber: number): number { + return this.getLineLength(lineNumber) + 1; + } + + public getLineFirstNonWhitespaceColumn(lineNumber: number): number { + const result = strings.firstNonWhitespaceIndex(this.getLineContent(lineNumber)); + if (result === -1) { + return 0; + } + return result + 1; + } + + public getLineLastNonWhitespaceColumn(lineNumber: number): number { + const result = strings.lastNonWhitespaceIndex(this.getLineContent(lineNumber)); + if (result === -1) { + return 0; + } + return result + 2; + } + + public getRangeAt(start: number, end: number): Range { + const startPosition = this.getPositionAt(start); + const endPosition = this.getPositionAt(end); + return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); + } + + // #endregion + + // #region Piece Table + insert(value: string, offset: number): void { + // todo, validate value and offset. + if (this.root !== SENTINEL) { + let { node, remainder } = this.nodeAt(offset); + let insertPos = node.piece.lineStarts.getIndexOf(remainder); + let nodeOffsetInDocument = this.offsetOfNode(node); + const startOffset = this._changeBuffer.length; + + if (!node.piece.isOriginalBuffer && (node.piece.offset + node.piece.length === this._changeBuffer.length) && (nodeOffsetInDocument + node.piece.length === offset)) { + // append content to this node, we don't want to keep adding node when users simply type in sequence + // unless we want to make the structure immutable + this.appendToNode(node, value); + } else { + if (nodeOffsetInDocument === offset) { + // we are inserting content to the beginning of node + let nodesToDel = []; + if (value.charCodeAt(value.length - 1) === 13) { + // inserted content ends with \r + if (node !== SENTINEL) { + if (this.nodeCharCodeAt(node, 0) === 10) { + // move `\n` forward + value += '\n'; + node.piece.offset++; + node.piece.length--; + node.piece.lineFeedCnt--; + node.piece.lineStarts.removeValues(0, 1); // remove the first line, which is empty. + this.updateMetadata(node, -1, -1); + + if (node.piece.length === 0) { + nodesToDel.push(node); + } + } + } + } + + this._changeBuffer += value; + const { lineFeedCount, lineLengths } = this.calculateNewLineCount(value); + let newPiece: Piece = new Piece(false, startOffset, value.length, lineFeedCount, lineLengths); + let newNode = this.rbInsertLeft(node, newPiece); + this.fixCRLFWithPrev(newNode); + + for (let i = 0; i < nodesToDel.length; i++) { + this.rbDelete(nodesToDel[i]); + } + } else if (nodeOffsetInDocument + node.piece.length > offset) { + let nodesToDel = []; + + // we need to split node. Create the new piece first as we are reading current node info before modifying it. + let newRightPiece = new Piece( + node.piece.isOriginalBuffer, + node.piece.offset + offset - nodeOffsetInDocument, + nodeOffsetInDocument + node.piece.length - offset, + node.piece.lineFeedCnt - insertPos.index, + node.piece.lineStarts.values + ); + + if (value.charCodeAt(value.length - 1) === 13 /** \r */) { + let headOfRight = this.nodeCharCodeAt(node, offset - nodeOffsetInDocument); + + if (headOfRight === 10 /** \n */) { + newRightPiece.offset++; + newRightPiece.length--; + newRightPiece.lineFeedCnt--; + newRightPiece.lineStarts.removeValues(0, insertPos.index + 1); + value += '\n'; + } else { + this.deletePrefixSumHead(newRightPiece.lineStarts, insertPos); + } + } else { + this.deletePrefixSumHead(newRightPiece.lineStarts, insertPos); + } + + // reuse node + if (value.charCodeAt(0) === 10/** \n */) { + let tailOfLeft = this.nodeCharCodeAt(node, offset - nodeOffsetInDocument - 1); + if (tailOfLeft === 13 /** \r */) { + let previousPos = node.piece.lineStarts.getIndexOf(remainder - 1); + this.deleteNodeTail(node, previousPos); + value = '\r' + value; + + if (node.piece.length === 0) { + nodesToDel.push(node); + } + } else { + this.deleteNodeTail(node, insertPos); + } + } else { + this.deleteNodeTail(node, insertPos); + } + + this._changeBuffer += value; + const { lineFeedCount, lineLengths } = this.calculateNewLineCount(value); + let newPiece: Piece = new Piece(false, startOffset, value.length, lineFeedCount, lineLengths); + + if (newRightPiece.length > 0) { + this.rbInsertRight(node, newRightPiece); + } + this.rbInsertRight(node, newPiece); + for (let i = 0; i < nodesToDel.length; i++) { + this.rbDelete(nodesToDel[i]); + } + } else { + // we are inserting to the right of this node. + if (this.adjustCarriageReturnFromNext(value, node)) { + value += '\n'; + } + + this._changeBuffer += value; + const { lineFeedCount, lineLengths } = this.calculateNewLineCount(value); + let newPiece: Piece = new Piece(false, startOffset, value.length, lineFeedCount, lineLengths); + let newNode = this.rbInsertRight(node, newPiece); + this.fixCRLFWithPrev(newNode); + } + } + } else { + // insert new node + const startOffset = this._changeBuffer.length; + this._changeBuffer += value; + const { lineFeedCount, lineLengths } = this.calculateNewLineCount(value); + let piece = new Piece(false, startOffset, value.length, lineFeedCount, lineLengths); + + this.rbInsertLeft(null, piece); + } + } + + delete(offset: number, cnt: number): void { + if (cnt <= 0) { + return; + } + + if (this.root !== SENTINEL) { + let startPosition = this.nodeAt(offset); + let endPosition = this.nodeAt(offset + cnt); + let startNode = startPosition.node; + let endNode = endPosition.node; + + let length = startNode.piece.length; + let startNodeOffsetInDocument = this.offsetOfNode(startNode); + let splitPos = startNode.piece.lineStarts.getIndexOf(offset - startNodeOffsetInDocument); + + if (startNode === endNode) { + // deletion falls into one node. + let endSplitPos = startNode.piece.lineStarts.getIndexOf(offset - startNodeOffsetInDocument + cnt); + + if (startNodeOffsetInDocument === offset) { + if (cnt === length) { // delete node + let next = startNode.next(); + this.rbDelete(startNode); + this.fixCRLFWithPrev(next); + return; + } + this.deleteNodeHead(startNode, endSplitPos); + this.fixCRLFWithPrev(startNode); + return; + } + + if (startNodeOffsetInDocument + length === offset + cnt) { + this.deleteNodeTail(startNode, splitPos); + this.fixCRLFWithNext(startNode); + return; + } + + // delete content in the middle, this node will be splitted to nodes + return this.shrinkNode(startNode, splitPos, endSplitPos); + } + + // perform read operations before any write operation. + let endNodeOffsetInDocument = this.offsetOfNode(endNode); + + // update first touched node + this.deleteNodeTail(startNode, splitPos); + let nodesToDel = []; + if (startNode.piece.length === 0) { + nodesToDel.push(startNode); + } + + // update last touched node + let endSplitPos = endNode.piece.lineStarts.getIndexOf(offset - endNodeOffsetInDocument + cnt); + this.deleteNodeHead(endNode, endSplitPos); + + if (endNode.piece.length === 0) { + nodesToDel.push(endNode); + } + + let secondNode = startNode.next(); + for (let node = secondNode; node !== SENTINEL && node !== endNode; node = node.next()) { + nodesToDel.push(node); + } + + let prev = startNode.piece.length === 0 ? startNode.prev() : startNode; + + for (let i = 0; i < nodesToDel.length; i++) { + this.rbDelete(nodesToDel[i]); + } + + if (prev !== SENTINEL) { + this.fixCRLFWithNext(prev); + } + } + } + + // #region node operations + deleteNodeHead(node: TreeNode, pos?: PrefixSumIndexOfResult) { + // it's okay to delete CR in CRLF. + let cnt = node.piece.lineStarts.getAccumulatedValue(pos.index - 1) + pos.remainder; + node.piece.length -= cnt; + node.piece.offset += cnt; + node.piece.lineFeedCnt -= pos.index; + this.deletePrefixSumHead(node.piece.lineStarts, pos); + this.updateMetadata(node, -cnt, -pos.index); + } + + deleteNodeTail(node: TreeNode, start: PrefixSumIndexOfResult) { + let cnt = node.piece.length - node.piece.lineStarts.getAccumulatedValue(start.index - 1) - start.remainder; + let hitCRLF = this.hitTestCRLF(node, node.piece.lineStarts.getAccumulatedValue(start.index - 1) + start.remainder, start); + node.piece.length -= cnt; + let lf_delta = start.index - node.piece.lineFeedCnt; + node.piece.lineFeedCnt = start.index; + this.deletePrefixSumTail(node.piece.lineStarts, start); + + if (hitCRLF) { + node.piece.lineFeedCnt += 1; + lf_delta += 1; + node.piece.lineStarts.insertValues(node.piece.lineStarts.values.length, new Uint32Array(1) /*[0]*/); + } + + this.updateMetadata(node, -cnt, lf_delta); + } + + // remove start-end from node. + shrinkNode(node: TreeNode, start: PrefixSumIndexOfResult, end?: PrefixSumIndexOfResult) { + // read operation first + let oldLineLengthsVal = node.piece.lineStarts.values; + let offset = node.piece.lineStarts.getAccumulatedValue(start.index - 1) + start.remainder; + let endOffset = node.piece.lineStarts.getAccumulatedValue(end.index - 1) + end.remainder; + + // write. + let startHitCRLF = this.hitTestCRLF(node, offset, start); + let nodeOldLength = node.piece.length; + node.piece.length = offset; + let lf_delta = start.index - node.piece.lineFeedCnt; + node.piece.lineFeedCnt = start.index; + node.piece.lineStarts = new PrefixSumComputer(oldLineLengthsVal.slice(0, start.index + 1)); + node.piece.lineStarts.changeValue(start.index, start.remainder); + + if (startHitCRLF) { + node.piece.lineFeedCnt += 1; + lf_delta += 1; + node.piece.lineStarts.insertValues(node.piece.lineStarts.values.length, new Uint32Array(1) /*[0]*/); + } + this.updateMetadata(node, offset - nodeOldLength, lf_delta); + + let newPieceLength = nodeOldLength - endOffset; + if (newPieceLength <= 0) { + return; + } + + let newPiece: Piece = new Piece( + node.piece.isOriginalBuffer, + endOffset + node.piece.offset, + newPieceLength, + oldLineLengthsVal.length - end.index - 1, + oldLineLengthsVal.slice(end.index) + ); + newPiece.lineStarts.changeValue(0, newPiece.lineStarts.values[0] - end.remainder); + + let newNode = this.rbInsertRight(node, newPiece); + this.fixCRLFWithPrev(newNode); + } + + appendToNode(node: TreeNode, value: string): void { + if (this.adjustCarriageReturnFromNext(value, node)) { + value += '\n'; + } + + let hitCRLF = value.charCodeAt(0) === 10 && this.nodeCharCodeAt(node, node.piece.length - 1) === 13; + this._changeBuffer += value; + node.piece.length += value.length; + const { lineFeedCount, lineLengths } = this.calculateNewLineCount(value); + + let lf_delta = lineFeedCount; + if (hitCRLF) { + node.piece.lineFeedCnt += lineFeedCount - 1; + lf_delta--; + let lineStarts = node.piece.lineStarts; + lineStarts.removeValues(lineStarts.values.length - 1, 1); + lineStarts.changeValue(lineStarts.values.length - 1, lineStarts.values[lineStarts.values.length - 1] + 1); + lineStarts.insertValues(lineStarts.values.length, lineLengths.slice(1)); + } else { + node.piece.lineFeedCnt += lineFeedCount; + let lineStarts = node.piece.lineStarts; + lineStarts.changeValue(lineStarts.values.length - 1, lineStarts.values[lineStarts.values.length - 1] + lineLengths[0]); + lineStarts.insertValues(lineStarts.values.length, lineLengths.slice(1)); + } + + this.updateMetadata(node, value.length, lf_delta); + } + + nodeAt(offset: number): BufferCursor { + let x = this.root; + + while (x !== SENTINEL) { + if (x.size_left > offset) { + x = x.left; + } else if (x.size_left + x.piece.length >= offset) { + return { + node: x, + remainder: offset - x.size_left + }; + } else { + offset -= x.size_left + x.piece.length; + x = x.right; + } + } + + return null; + } + + nodeAt2(position: Position): BufferCursor { + let x = this.root; + let lineNumber = position.lineNumber; + let column = position.column; + + 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) { + let prevAccumualtedValue = x.piece.lineStarts.getAccumulatedValue(lineNumber - x.lf_left - 2); + let accumualtedValue = x.piece.lineStarts.getAccumulatedValue(lineNumber - x.lf_left - 1); + + return { + node: x, + remainder: Math.min(prevAccumualtedValue + column - 1, accumualtedValue) + }; + } else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) { + let prevAccumualtedValue = x.piece.lineStarts.getAccumulatedValue(lineNumber - x.lf_left - 2); + if (prevAccumualtedValue + column - 1 <= x.piece.length) { + return { + node: x, + remainder: prevAccumualtedValue + column - 1 + }; + } else { + column -= x.piece.length - prevAccumualtedValue; + break; + } + } else { + lineNumber -= x.lf_left + x.piece.lineFeedCnt; + x = x.right; + } + } + + // search in order, to find the node contains position.column + x = x.next(); + while (x !== SENTINEL) { + + if (x.piece.lineFeedCnt > 0) { + let accumualtedValue = x.piece.lineStarts.getAccumulatedValue(0); + return { + node: x, + remainder: Math.min(column - 1, accumualtedValue) + }; + } else { + if (x.piece.length >= column - 1) { + return { + node: x, + remainder: column - 1 + }; + } else { + column -= x.piece.length; + } + } + + x = x.next(); + } + + return null; + } + + 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; + } + + getNodeContent(node: TreeNode): string { + let buffer = node.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + let currentContent = buffer.substr(node.piece.offset, node.piece.length); + + return currentContent; + } + + deletePrefixSumTail(prefixSum: PrefixSumComputer, position: PrefixSumIndexOfResult): void { + prefixSum.removeValues(position.index + 1, prefixSum.values.length - position.index - 1); + prefixSum.changeValue(position.index, position.remainder); + } + + deletePrefixSumHead(prefixSum: PrefixSumComputer, position: PrefixSumIndexOfResult): void { + prefixSum.changeValue(position.index, prefixSum.values[position.index] - position.remainder); + if (position.index > 0) { + prefixSum.removeValues(0, position.index); + } + } + + // #endregion + + // #region CRLF + hitTestCRLF(node: TreeNode, offset: number, position: PrefixSumIndexOfResult) { + if (node.piece.lineFeedCnt < 1) { + return false; + } + + let currentLineLen = node.piece.lineStarts.getAccumulatedValue(position.index); + if (offset === currentLineLen - 1) { + // charCodeAt becomes slow (single or even two digits ms) when the changed buffer is long + return this.nodeCharCodeAt(node, offset - 1) === 13/* \r */ && this.nodeCharCodeAt(node, offset) === 10 /* \n */; + } + return false; + } + + fixCRLFWithPrev(nextNode: TreeNode) { + if (nextNode === SENTINEL || nextNode.piece.lineFeedCnt === 0) { + return; + } + + if (nextNode.piece.lineStarts.getAccumulatedValue(0) !== 1 /* if it's \n, the first line is 1 char */) { + return; + } + + if (this.nodeCharCodeAt(nextNode, 0) === 10 /* \n */) { + let node = nextNode.prev(); + + if (node === SENTINEL || node.piece.lineFeedCnt === 0) { + return; + } + + if (this.nodeCharCodeAt(node, node.piece.length - 1) === 13) { + this.fixCRLF(node, nextNode); + } + } + } + + fixCRLFWithNext(node: TreeNode) { + if (node === SENTINEL) { + return; + } + + if (this.nodeCharCodeAt(node, node.piece.length - 1) === 13 /* \r */) { + let nextNode = node.next(); + if (nextNode !== SENTINEL && this.nodeCharCodeAt(nextNode, 0) === 10 /* \n */) { + this.fixCRLF(node, nextNode); + } + } + } + + fixCRLF(prev: TreeNode, next: TreeNode) { + let nodesToDel = []; + // update node + prev.piece.length -= 1; + prev.piece.lineFeedCnt -= 1; + let lineStarts = prev.piece.lineStarts; + // lineStarts.values.length >= 2 due to a `\r` + lineStarts.removeValues(lineStarts.values.length - 1, 1); + lineStarts.changeValue(lineStarts.values.length - 1, lineStarts.values[lineStarts.values.length - 1] - 1); + this.updateMetadata(prev, - 1, -1); + + if (prev.piece.length === 0) { + nodesToDel.push(prev); + } + + // update nextNode + next.piece.length -= 1; + next.piece.offset += 1; + next.piece.lineFeedCnt -= 1; + lineStarts = next.piece.lineStarts; + lineStarts.removeValues(0, 1); + this.updateMetadata(next, - 1, -1); + if (next.piece.length === 0) { + nodesToDel.push(next); + } + + // create new piece which contains \r\n + let startOffset = this._changeBuffer.length; + this._changeBuffer += '\r\n'; + const { lineFeedCount, lineLengths } = this.calculateNewLineCount('\r\n'); + let piece = new Piece(false, startOffset, 2, lineFeedCount, lineLengths); + this.rbInsertRight(prev, piece); + // delete empty nodes + + for (let i = 0; i < nodesToDel.length; i++) { + this.rbDelete(nodesToDel[i]); + } + } + + adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean { + if (value.charCodeAt(value.length - 1) === 13) { + // inserted content ends with \r + let nextNode = node.next(); + if (nextNode !== SENTINEL) { + if (this.nodeCharCodeAt(nextNode, 0) === 10) { + // move `\n` forward + value += '\n'; + + if (nextNode.piece.length === 1) { + this.rbDelete(nextNode); + } else { + nextNode.piece.offset += 1; + nextNode.piece.length -= 1; + nextNode.piece.lineFeedCnt -= 1; + nextNode.piece.lineStarts.removeValues(0, 1); // remove the first line, which is empty. + this.updateMetadata(nextNode, -1, -1); + } + return true; + } + } + } + + return false; + } + + nodeCharCodeAt(node: TreeNode, offset: number): number { + if (node.piece.lineFeedCnt < 1) { + return -1; + } + let buffer = node.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + + return buffer.charCodeAt(node.piece.offset + offset); + } + // #endregion + + // #endregion + + // #region Red Black Tree + + leftRotate(x: TreeNode) { + let y = x.right; + + // fix size_left + y.size_left += x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left += x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + x.right = y.left; + + if (y.left !== SENTINEL) { + y.left.parent = x; + } + y.parent = x.parent; + if (x.parent === SENTINEL) { + this.root = y; + } else if (x.parent.left === x) { + x.parent.left = y; + } else { + x.parent.right = y; + } + y.left = x; + x.parent = y; + } + + rightRotate(y: TreeNode) { + let x = y.left; + y.left = x.right; + if (x.right !== SENTINEL) { + x.right.parent = y; + } + x.parent = y.parent; + + // fix size_left + y.size_left -= x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left -= x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + + if (y.parent === SENTINEL) { + this.root = x; + } else if (y === y.parent.right) { + y.parent.right = x; + } else { + y.parent.left = x; + } + + x.right = y; + y.parent = x; + } + + /** + * node node + * / \ / \ + * a b <---- a b + * / + * z + */ + rbInsertRight(node: TreeNode, p: Piece): TreeNode { + let z = new TreeNode(p, NodeColor.Red); + z.left = SENTINEL; + z.right = SENTINEL; + z.parent = SENTINEL; + z.size_left = 0; + z.lf_left = 0; + + let x = this.root; + if (x === SENTINEL) { + this.root = z; + setNodeColor(z, NodeColor.Black); + } else if (node.right === SENTINEL) { + node.right = z; + z.parent = node; + } else { + let nextNode = leftest(node.right); + nextNode.left = z; + z.parent = nextNode; + } + + this.fixInsert(z); + return z; + } + + /** + * node node + * / \ / \ + * a b ----> a b + * \ + * z + */ + rbInsertLeft(node: TreeNode, p: Piece): TreeNode { + let z = new TreeNode(p, NodeColor.Red); + z.left = SENTINEL; + z.right = SENTINEL; + z.parent = SENTINEL; + z.size_left = 0; + z.lf_left = 0; + + let x = this.root; + if (x === SENTINEL) { + this.root = z; + setNodeColor(z, NodeColor.Black); + } else if (node.left === SENTINEL) { + node.left = z; + z.parent = node; + } else { + let prevNode = righttest(node.left); // a + prevNode.right = z; + z.parent = prevNode; + } + + this.fixInsert(z); + return z; + } + + rbDelete(z: TreeNode) { + let x: TreeNode; + let y: TreeNode; + + if (z.left === SENTINEL) { + y = z; + x = y.right; + } else if (z.right === SENTINEL) { + y = z; + x = y.left; + } else { + y = leftest(z.right); + x = y.right; + } + + if (y === this.root) { + this.root = x; + + // if x is null, we are removing the only node + setNodeColor(x, NodeColor.Black); + + z.detach(); + resetSentinel(); + this.root.parent = SENTINEL; + + return; + } + + let yWasRed = (getNodeColor(y) === NodeColor.Red); + + if (y === y.parent.left) { + y.parent.left = x; + } else { + y.parent.right = x; + } + + if (y === z) { + x.parent = y.parent; + this.recomputeMetadata(x); + } else { + if (y.parent === z) { + x.parent = y; + } else { + x.parent = y.parent; + } + + // as we make changes to x's hierarchy, update size_left of subtree first + this.recomputeMetadata(x); + + y.left = z.left; + y.right = z.right; + y.parent = z.parent; + setNodeColor(y, getNodeColor(z)); + + if (z === this.root) { + this.root = y; + } else { + if (z === z.parent.left) { + z.parent.left = y; + } else { + z.parent.right = y; + } + } + + if (y.left !== SENTINEL) { + y.left.parent = y; + } + if (y.right !== SENTINEL) { + y.right.parent = y; + } + // update metadata + // we replace z with y, so in this sub tree, the length change is z.item.length + y.size_left = z.size_left; + y.lf_left = z.lf_left; + this.recomputeMetadata(y); + } + + z.detach(); + + if (x.parent.left === x) { + let newSizeLeft = calculateSize(x); + let newLFLeft = calculateLF(x); + if (newSizeLeft !== x.parent.size_left || newLFLeft !== x.parent.lf_left) { + let delta = newSizeLeft - x.parent.size_left; + let lf_delta = newLFLeft - x.parent.lf_left; + x.parent.size_left = newSizeLeft; + x.parent.lf_left = newLFLeft; + this.updateMetadata(x.parent, delta, lf_delta); + } + } + + this.recomputeMetadata(x.parent); + + if (yWasRed) { + resetSentinel(); + return; + } + + // RB-DELETE-FIXUP + let w: TreeNode; + while (x !== this.root && getNodeColor(x) === NodeColor.Black) { + if (x === x.parent.left) { + w = x.parent.right; + + if (getNodeColor(w) === NodeColor.Red) { + setNodeColor(w, NodeColor.Black); + setNodeColor(x.parent, NodeColor.Red); + this.leftRotate(x.parent); + w = x.parent.right; + } + + if (getNodeColor(w.left) === NodeColor.Black && getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w, NodeColor.Red); + x = x.parent; + } else { + if (getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w.left, NodeColor.Black); + setNodeColor(w, NodeColor.Red); + this.rightRotate(w); + w = x.parent.right; + } + + setNodeColor(w, getNodeColor(x.parent)); + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(w.right, NodeColor.Black); + this.leftRotate(x.parent); + x = this.root; + } + } else { + w = x.parent.left; + + if (getNodeColor(w) === NodeColor.Red) { + setNodeColor(w, NodeColor.Black); + setNodeColor(x.parent, NodeColor.Red); + this.rightRotate(x.parent); + w = x.parent.left; + } + + if (getNodeColor(w.left) === NodeColor.Black && getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w, NodeColor.Red); + x = x.parent; + + } else { + if (getNodeColor(w.left) === NodeColor.Black) { + setNodeColor(w.right, NodeColor.Black); + setNodeColor(w, NodeColor.Red); + this.leftRotate(w); + w = x.parent.left; + } + + setNodeColor(w, getNodeColor(x.parent)); + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(w.left, NodeColor.Black); + this.rightRotate(x.parent); + x = this.root; + } + } + } + setNodeColor(x, NodeColor.Black); + resetSentinel(); + } + + fixInsert(x: TreeNode) { + this.recomputeMetadata(x); + + while (x !== this.root && getNodeColor(x.parent) === NodeColor.Red) { + if (x.parent === x.parent.parent.left) { + const y = x.parent.parent.right; + + if (getNodeColor(y) === NodeColor.Red) { + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(y, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + x = x.parent.parent; + } else { + if (x === x.parent.right) { + x = x.parent; + this.leftRotate(x); + } + + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + this.rightRotate(x.parent.parent); + } + } else { + const y = x.parent.parent.left; + + if (getNodeColor(y) === NodeColor.Red) { + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(y, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + x = x.parent.parent; + } else { + if (x === x.parent.left) { + x = x.parent; + this.rightRotate(x); + } + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + this.leftRotate(x.parent.parent); + } + } + } + + setNodeColor(this.root, NodeColor.Black); + } + + updateMetadata(x: TreeNode, delta: number, lineFeedCntDelta: number): void { + // node length change, we need to update the roots of all subtrees containing this node. + while (x !== this.root && x !== SENTINEL) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lineFeedCntDelta; + } + + x = x.parent; + } + } + + recomputeMetadata(x: TreeNode) { + let delta = 0; + let lf_delta = 0; + if (x === this.root) { + return; + } + + if (delta === 0) { + // go upwards till the node whose left subtree is changed. + while (x !== this.root && x === x.parent.right) { + x = x.parent; + } + + if (x === this.root) { + // well, it means we add a node to the end (inorder) + return; + } + + // x is the node whose right subtree is changed. + x = x.parent; + + delta = calculateSize(x.left) - x.size_left; + lf_delta = calculateLF(x.left) - x.lf_left; + x.size_left += delta; + x.lf_left += lf_delta; + } + + // go upwards till root. O(logN) + while (x !== this.root && (delta !== 0 || lf_delta !== 0)) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lf_delta; + } + + x = x.parent; + } + } + + getContentOfSubTree(node: TreeNode): string { + if (node === SENTINEL) { + return ''; + } + + let buffer = node.piece.isOriginalBuffer ? this._originalBuffer : this._changeBuffer; + let currentContent = buffer.substr(node.piece.offset, node.piece.length); + + return this.getContentOfSubTree(node.left) + currentContent + this.getContentOfSubTree(node.right); + } + + calculateNewLineCount(chunk: string): { lineFeedCount: number, lineLengths: Uint32Array } { + let lineStarts = [0]; + + // Reset regex to search from the beginning + this._regex.lastIndex = 0; + let prevMatchStartIndex = -1; + let prevMatchLength = 0; + + let m: RegExpExecArray; + do { + if (prevMatchStartIndex + prevMatchLength === chunk.length) { + // Reached the end of the line + break; + } + + m = this._regex.exec(chunk); + if (!m) { + break; + } + + const matchStartIndex = m.index; + const matchLength = m[0].length; + + if (matchStartIndex === prevMatchStartIndex && matchLength === prevMatchLength) { + // Exit early if the regex matches the same range twice + break; + } + + prevMatchStartIndex = matchStartIndex; + prevMatchLength = matchLength; + + lineStarts.push(matchStartIndex + matchLength); + + } while (m); + + const lineLengths = new Uint32Array(lineStarts.length); + for (let i = 1; i < lineStarts.length; i++) { + lineLengths[i - 1] = lineStarts[i] - lineStarts[i - 1]; + } + + lineLengths[lineStarts.length - 1] = chunk.length - lineStarts[lineStarts.length - 1]; + + return { + lineFeedCount: lineLengths.length - 1, + lineLengths: lineLengths + }; + } + // #endregion + + // #region helper + /** + * Assumes `operations` are validated and sorted ascending + */ + public static _getInverseEditRanges(operations: IValidatedEditOperation[]): Range[] { + let result: Range[] = []; + + let prevOpEndLineNumber: number; + let prevOpEndColumn: number; + let prevOp: IValidatedEditOperation = null; + for (let i = 0, len = operations.length; i < len; i++) { + let op = operations[i]; + + let startLineNumber: number; + let startColumn: number; + + if (prevOp) { + if (prevOp.range.endLineNumber === op.range.startLineNumber) { + startLineNumber = prevOpEndLineNumber; + startColumn = prevOpEndColumn + (op.range.startColumn - prevOp.range.endColumn); + } else { + startLineNumber = prevOpEndLineNumber + (op.range.startLineNumber - prevOp.range.endLineNumber); + startColumn = op.range.startColumn; + } + } else { + startLineNumber = op.range.startLineNumber; + startColumn = op.range.startColumn; + } + + let resultRange: Range; + + if (op.lines && op.lines.length > 0) { + // the operation inserts something + let lineCount = op.lines.length; + let firstLine = op.lines[0]; + let lastLine = op.lines[lineCount - 1]; + + if (lineCount === 1) { + // single line insert + resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLine.length); + } else { + // multi line insert + resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLine.length + 1); + } + } else { + // There is nothing to insert + resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn); + } + + prevOpEndLineNumber = resultRange.endLineNumber; + prevOpEndColumn = resultRange.endColumn; + + result.push(resultRange); + prevOp = op; + } + + return result; + } + + private static _sortOpsAscending(a: IValidatedEditOperation, b: IValidatedEditOperation): number { + let r = Range.compareRangesUsingEnds(a.range, b.range); + if (r === 0) { + return a.sortIndex - b.sortIndex; + } + return r; + } + + private static _sortOpsDescending(a: IValidatedEditOperation, b: IValidatedEditOperation): number { + let r = Range.compareRangesUsingEnds(a.range, b.range); + if (r === 0) { + return b.sortIndex - a.sortIndex; + } + return -r; + } + // #endregion +} \ No newline at end of file diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 2fdc06ca5b4edfd7131eee9ee885dd4c3e0767a5..575d26bedca3bcc8b529bbbf584ea2725bca0da9 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -10,13 +10,14 @@ import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range, IRange } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { guessIndentation, IndentationGuesserTextBufferTarget, IndentationGuesserStringArrayTarget } from 'vs/editor/common/model/indentationGuesser'; +import { guessIndentation, IndentationGuesserTextBufferTarget, IndentationGuesserStringArrayTarget, IndentationGuesserRawTextBufferTarget } from 'vs/editor/common/model/indentationGuesser'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelSearch'; import { TextSource, ITextSource, IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource'; import { IModelContentChangedEvent, ModelRawContentChangedEvent, ModelRawFlush, ModelRawEOLChanged, IModelOptionsChangedEvent, InternalModelContentChangeEvent } from 'vs/editor/common/model/textModelEvents'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { TextBuffer } from 'vs/editor/common/model/textBuffer'; +// import { TextBuffer } from 'vs/editor/common/model/textBuffer'; +import { TextBuffer } from 'vs/editor/common/model/textBuffer2'; const LIMIT_FIND_COUNT = 999; export const LONG_LINE_BOUNDARY = 10000; @@ -48,7 +49,13 @@ export class TextModel extends Disposable implements editorCommon.ITextModel { let resolvedOpts: editorCommon.TextModelResolvedOptions; if (options.detectIndentation) { - const guessedIndentation = guessIndentation(new IndentationGuesserStringArrayTarget(textSource.lines), options.tabSize, options.insertSpaces); + const guessedIndentation = guessIndentation( + Array.isArray(textSource.lines) ? + new IndentationGuesserStringArrayTarget(textSource.lines) : + new IndentationGuesserRawTextBufferTarget(textSource.lines), + options.tabSize, + options.insertSpaces + ); resolvedOpts = new editorCommon.TextModelResolvedOptions({ tabSize: guessedIndentation.tabSize, insertSpaces: guessedIndentation.insertSpaces, @@ -412,7 +419,7 @@ export class TextModel extends Disposable implements editorCommon.ITextModel { public getLinesContent(): string[] { this._assertNotDisposed(); - return this._buffer.getLinesContent(); + return this._buffer.getLinesContent().split(/\r\n|\r|\n/); } public getEOL(): string { diff --git a/src/vs/editor/common/model/textSource.ts b/src/vs/editor/common/model/textSource.ts index 3eb9cf653e8a0dbfe15b81e93ac900b3b1d7a35f..0bba12b196ec5ca2082c4ae3e3af086b73eeebf2 100644 --- a/src/vs/editor/common/model/textSource.ts +++ b/src/vs/editor/common/model/textSource.ts @@ -7,6 +7,18 @@ import * as strings from 'vs/base/common/strings'; import { DefaultEndOfLine } from 'vs/editor/common/editorCommon'; +/** + * Raw text buffer for Piece Table. + */ +export interface IRawPTBuffer { + text: string; + lineStarts: number[]; + /** + * lines count + */ + length: number; +} + /** * A processed string ready to be turned into an editor model. */ @@ -18,7 +30,7 @@ export interface IRawTextSource { /** * The text split into lines. */ - readonly lines: string[]; + readonly lines: string[] | IRawPTBuffer; /** * The BOM (leading character sequence of the file). */ @@ -51,18 +63,28 @@ export class RawTextSource { const isBasicASCII = (containsRTL ? false : strings.isBasicASCII(rawText)); // Split the text into lines - const lines = rawText.split(/\r\n|\r|\n/); + // const lines = rawText.split(/\r\n|\r|\n/); // Remove the BOM (if present) let BOM = ''; - if (strings.startsWithUTF8BOM(lines[0])) { + if (strings.startsWithUTF8BOM(rawText)) { BOM = strings.UTF8_BOM_CHARACTER; - lines[0] = lines[0].substr(1); + rawText = rawText.substr(1); + } + + let lastLineFeed = -1; + var lineStarts = []; + while ((lastLineFeed = rawText.indexOf('\n', lastLineFeed + 1)) !== -1) { + lineStarts.push(lastLineFeed + 1); } return { BOM: BOM, - lines: lines, + lines: { + text: rawText, + lineStarts: lineStarts, + length: lineStarts.length + }, length: rawText.length, containsRTL: containsRTL, isBasicASCII: isBasicASCII, @@ -83,7 +105,7 @@ export interface ITextSource { /** * The text split into lines. */ - readonly lines: string[]; + readonly lines: string[] | IRawPTBuffer; /** * The BOM (leading character sequence of the file). */ diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index bda67f4f9b5d86d6fb5cd88a615674909384eb79..9faaf7880474c5370f5bb95fea489a23274d246b 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -409,10 +409,25 @@ export class ModelServiceImpl implements IModelService { }; const textSourceLineSequence = new class implements ISequence { public getLength(): number { - return textSource.lines.length; + return textSource.lines.length + 1; } public getElementHash(index: number): string { - return textSource.lines[index]; + if (Array.isArray(textSource.lines)) { + return textSource.lines[index]; + } else { + if (textSource.lines.length === 0) { + return textSource.lines.text; + } + if (index === 0) { + return textSource.lines.text.substring(0, textSource.lines.lineStarts[0]); + } + + if (index === textSource.lines.length) { + return textSource.lines.text.substring(textSource.lines.lineStarts[index - 1]); + } + + return textSource.lines.text.substring(textSource.lines.lineStarts[index - 1], textSource.lines.lineStarts[index]); + } } }; diff --git a/src/vs/editor/common/viewModel/prefixSumComputer.ts b/src/vs/editor/common/viewModel/prefixSumComputer.ts index 8730795093f8ec21bfd9dd1319ce7b9069c1e819..291ec21fa35ec0ad4c71aca9875f41c75d01b8eb 100644 --- a/src/vs/editor/common/viewModel/prefixSumComputer.ts +++ b/src/vs/editor/common/viewModel/prefixSumComputer.ts @@ -23,7 +23,7 @@ export class PrefixSumComputer { /** * values[i] is the value at index i */ - private values: Uint32Array; + values: Uint32Array; /** * prefixSum[i] = SUM(heights[j]), 0 <= j <= i diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 3a6006306898b61f806de43884f17a4426b3bcf0..c4b2db3f0822c2c8e172360e616273b5682896c0 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -106,8 +106,10 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd } return firstLineText.substr(0, Math.min(crIndex, lfIndex)); - } else { + } else if (Array.isArray(value.lines)) { return value.lines[0].substr(0, 100); + } else { + return value.lines.text.substr(0, 100); } } diff --git a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts index 5688f82055a4dadb1722440a5cb4c3f4414f5f84..c2bd91ae57487d604db961944b7a15c85a09bdab 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts @@ -81,7 +81,7 @@ export class WalkThroughSnippetContentProvider implements ITextModelContentProvi return ''; }; - const markdown = content.value.lines.join('\n'); + const markdown = Array.isArray(content.value.lines) ? content.value.lines.join('\n') : content.value.lines.text; marked(markdown, { renderer }); const modeId = this.modeService.getModeIdForLanguageName(languageName); diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts index 1a00d797b91ffa1e0f10a744f3649162aa38e572..dfa8348aaf7b6df2596b28d0681c41572af1d1d9 100644 --- a/src/vs/workbench/services/backup/node/backupFileService.ts +++ b/src/vs/workbench/services/backup/node/backupFileService.ts @@ -215,7 +215,7 @@ export class BackupFileService implements IBackupFileService { public parseBackupContent(rawTextSource: IRawTextSource): string { const textSource = TextSource.fromRawTextSource(rawTextSource, DefaultEndOfLine.LF); - return textSource.lines.slice(1).join(textSource.EOL); // The first line of a backup text file is the file name + return (textSource.lines).slice(1).join(textSource.EOL); // The first line of a backup text file is the file name } public toBackupResource(resource: Uri): Uri { diff --git a/src/vs/workbench/services/textfile/electron-browser/modelBuilder.ts b/src/vs/workbench/services/textfile/electron-browser/modelBuilder.ts index 2d5c9a2e694e7de9500b0c5aab493b6a93769e5b..ace7732de7885f24e525b54dc135069cc5f82900 100644 --- a/src/vs/workbench/services/textfile/electron-browser/modelBuilder.ts +++ b/src/vs/workbench/services/textfile/electron-browser/modelBuilder.ts @@ -56,6 +56,61 @@ function optimizeStringMemory(buff: Buffer, s: string): string { return s; } +class PTBasedBuilder { + private lineStarts: number[]; + private text: string; + private BOM: string; + private chunkIndex: number; + private totalLength: number; + + constructor() { + this.text = ''; + this.BOM = ''; + this.chunkIndex = 0; + this.totalLength = 0; + this.lineStarts = []; + } + + public acceptChunk(chunk: string): void { + if (this.chunkIndex === 0) { + if (strings.startsWithUTF8BOM(chunk)) { + this.BOM = strings.UTF8_BOM_CHARACTER; + chunk = chunk.substr(1); + } + } + + let lastLineFeed = -1; + while ((lastLineFeed = chunk.indexOf('\n', lastLineFeed + 1)) !== -1) { + this.lineStarts.push(this.totalLength + lastLineFeed + 1); + } + + this.text += chunk; + this.chunkIndex++; + } + + public finish(containsRTL: boolean, isBasicASCII: boolean): ModelBuilderResult { + if (this.lineStarts[this.lineStarts.length - 1] > this.totalLength) { + this.lineStarts.pop(); + } + + return { + hash: null, + value: { + length: this.totalLength, + lines: { + text: this.text, + lineStarts: this.lineStarts, + length: this.lineStarts.length + }, + BOM: this.BOM, + totalCRCount: 0, + containsRTL: containsRTL, + isBasicASCII: isBasicASCII + } + }; + } +} + class ModelLineBasedBuilder { private computeHash: boolean; @@ -126,13 +181,15 @@ export class ModelBuilder { private containsRTL: boolean; private isBasicASCII: boolean; + private ptBasedBuilder: PTBasedBuilder; + public static fromStringStream(stream: IStringStream): TPromise { return new TPromise((c, e, p) => { let done = false; let builder = new ModelBuilder(false); stream.on('data', (chunk) => { - builder.acceptChunk(chunk); + builder.acceptChunk2(chunk); }); stream.on('error', (error) => { @@ -145,7 +202,7 @@ export class ModelBuilder { stream.on('end', () => { if (!done) { done = true; - c(builder.finish()); + c(builder.finish2()); } }); }); @@ -159,6 +216,7 @@ export class ModelBuilder { this.totalLength = 0; this.containsRTL = false; this.isBasicASCII = true; + this.ptBasedBuilder = new PTBasedBuilder(); } private _updateCRCount(chunk: string): void { @@ -210,6 +268,22 @@ export class ModelBuilder { this.leftoverPrevChunk = lines[lines.length - 1]; } + public acceptChunk2(chunk: string): void { + if (chunk.length === 0) { + return; + } + + // update lineStart to offset mapping + if (!this.containsRTL) { + this.containsRTL = strings.containsRTL(chunk); + } + if (this.isBasicASCII) { + this.isBasicASCII = strings.isBasicASCII(chunk); + } + + this.ptBasedBuilder.acceptChunk(chunk); + } + public finish(): ModelBuilderResult { let finalLines = [this.leftoverPrevChunk]; if (this.leftoverEndsInCR) { @@ -218,4 +292,8 @@ export class ModelBuilder { this.lineBasedBuilder.acceptLines(finalLines); return this.lineBasedBuilder.finish(this.totalLength, this.totalCRCount, this.containsRTL, this.isBasicASCII); } + + public finish2(): ModelBuilderResult { + return this.ptBasedBuilder.finish(this.containsRTL, this.isBasicASCII); + } } diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 0dcf628ee66325f3c46bffcee3c6cbc209ce560b..b12327198a302e2e01b4f5542756317d980548b7 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -859,7 +859,7 @@ export class TestBackupFileService implements IBackupFileService { } public parseBackupContent(rawText: IRawTextSource): string { - return rawText.lines.join('\n'); + return Array.isArray(rawText.lines) ? rawText.lines.join('\n') : rawText.lines.text; } public discardResourceBackup(resource: URI): TPromise {