src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts
629 LOC · 509 covered · 120 uncovered · 145 ranges · 1708 concepts · 63 introducers · 861 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
textModel.ts ×190
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import * as strings from '../../../../base/common/strings.js';
import { Position } from '../../core/position.js';
import { Range } from '../../core/range.js';
import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContentChange, ISingleEditOperationIdentifier, ITextBuffer, ITextSnapshot, ValidAnnotatedEditOperation, IValidEditOperation, SearchData } from '../../model.js';
import { PieceTreeBase, StringBuffer } from './pieceTreeBase.js';
import { countEOL, StringEOL } from '../../core/misc/eolCounter.js';
import { TextChange } from '../../core/textChange.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
export interface IValidatedEditOperation {
sortIndex: number;
identifier: ISingleEditOperationIdentifier | null;
range: Range;
rangeOffset: number;
rangeLength: number;
text: string;
eolCount: number;
firstLineLength: number;
lastLineLength: number;
forceMoveMarkers: boolean;
isAutoWhitespaceEdit: boolean;
}
interface IReverseSingleEditOperation extends IValidEditOperation {
sortIndex: number;
}
export class PieceTreeTextBuffer extends Disposable implements ITextBuffer {
private _pieceTree: PieceTreeBase;
private readonly _BOM: string;
private _mightContainRTL: boolean;
private _mightContainUnusualLineTerminators: boolean;
private _mightContainNonBasicASCII: boolean;
private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
public get onDidChangeContent(): Event<void> { return this._onDidChangeContent.event; }
constructor(chunks: StringBuffer[], BOM: string, eol: '\r\n' | '\n', containsRTL: boolean, containsUnusualLineTerminators: boolean, isBasicASCII: boolean, eolNormalized: boolean) {
this._BOM = BOM;
this._mightContainNonBasicASCII = !isBasicASCII;
this._mightContainRTL = containsRTL;
this._mightContainUnusualLineTerminators = containsUnusualLineTerminators;
this._pieceTree = new PieceTreeBase(chunks, eol, eolNormalized);
}
// #region TextBuffer
public equals(other: ITextBuffer): boolean {
if (!(other instanceof PieceTreeTextBuffer)) {
return false;
}
if (this._BOM !== other._BOM) {
return false;
}
if (this.getEOL() !== other.getEOL()) {
return false;
}
return this._pieceTree.equal(other._pieceTree);
}
}
return this._mightContainUnusualLineTerminators;
}
this._mightContainUnusualLineTerminators = false;
}
}
return this._BOM;
}
}
public createSnapshot(preserveBOM: boolean): ITextSnapshot {
}
public getOffsetAt(lineNumber: number, column: number): number {
}
public getPositionAt(offset: number): Position {
}
public getRangeAt(start: number, length: number): Range {
const startPosition = this.getPositionAt(start);
const endPosition = this.getPositionAt(end);
return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
}
public getValueInRange(range: Range, eol: EndOfLinePreference = EndOfLinePreference.TextDefined): string {
}
const lineEnding = this._getEndOfLine(eol);
return this._pieceTree.getValueInRange(range, lineEnding);
public getValueLengthInRange(range: Range, eol: EndOfLinePreference = EndOfLinePreference.TextDefined): number {
}
if (range.startLineNumber === range.endLineNumber) {
}
const startOffset = this.getOffsetAt(range.startLineNumber, range.startColumn);
const endOffset = this.getOffsetAt(range.endLineNumber, range.endColumn);
// offsets use the text EOL, so we need to compensate for length differences
// if the requested EOL doesn't match the text EOL
let eolOffsetCompensation = 0;
const desiredEOL = this._getEndOfLine(eol);
const actualEOL = this.getEOL();
if (desiredEOL.length !== actualEOL.length) {
const eolCount = range.endLineNumber - range.startLineNumber;
eolOffsetCompensation = delta * eolCount;
}
return endOffset - startOffset + eolOffsetCompensation;
public getCharacterCountInRange(range: Range, eol: EndOfLinePreference = EndOfLinePreference.TextDefined): number {
if (this._mightContainNonBasicASCII) {
// we must count by iterating
let result = 0;
const fromLineNumber = range.startLineNumber;
const toLineNumber = range.endLineNumber;
for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
const lineContent = this.getLineContent(lineNumber);
const fromOffset = (lineNumber === fromLineNumber ? range.startColumn - 1 : 0);
const toOffset = (lineNumber === toLineNumber ? range.endColumn - 1 : lineContent.length);
for (let offset = fromOffset; offset < toOffset; offset++) {
if (strings.isHighSurrogate(lineContent.charCodeAt(offset))) {
result = result + 1;
offset = offset + 1;
} else {
result = result + 1;
}
}
}
result += this._getEndOfLine(eol).length * (toLineNumber - fromLineNumber);
return result;
}
return this.getValueLengthInRange(range, eol);
}
public getNearestChunk(offset: number): string {
return this._pieceTree.getNearestChunk(offset);
}
public getLength(): number {
}
public getLineCount(): number {
}
public getLinesContent(): string[] {
}
public getLineContent(lineNumber: number): string {
}
public getLineCharCode(lineNumber: number, index: number): number {
}
public getCharCode(offset: number): number {
return this._pieceTree.getCharCode(offset);
}
public getLineLength(lineNumber: number): number {
}
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));
textModel.ts ×2
if (result === -1) {
return 0;
}
return result + 1;
}
public getLineLastNonWhitespaceColumn(lineNumber: number): number {
const result = strings.lastNonWhitespaceIndex(this.getLineContent(lineNumber));
textModel.ts ×2
if (result === -1) {
return 0;
}
return result + 2;
}
private _getEndOfLine(eol: EndOfLinePreference): string {
case EndOfLinePreference.LF:
throw new Error('Unknown EOL preference');
}
public setEOL(newEOL: '\r\n' | '\n'): void {
}
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult {
let mightContainUnusualLineTerminators = this._mightContainUnusualLineTerminators;
let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
let canReduceOperations = true;
let operations: IValidatedEditOperation[] = [];
for (let i = 0; i < rawOperations.length; i++) {
const op = rawOperations[i];
if (canReduceOperations && op._isTracked) {
canReduceOperations = false;
}
if (op.text) {
if (!mightContainNonBasicASCII) {
mightContainNonBasicASCII = textMightContainNonBasicASCII;
}
mightContainRTL = strings.containsRTL(op.text);
}
if (!mightContainUnusualLineTerminators && textMightContainNonBasicASCII) {
pieceTreeTextBuffer.ts ×7
mightContainUnusualLineTerminators = strings.containsUnusualLineTerminators(op.text);
}
let validText = '';
let eolCount = 0;
let firstLineLength = 0;
let lastLineLength = 0;
if (op.text) {
[eolCount, firstLineLength, lastLineLength, strEOL] = countEOL(op.text);
const bufferEOL = this.getEOL();
const expectedStrEOL = (bufferEOL === '\r\n' ? StringEOL.CRLF : StringEOL.LF);
if (strEOL === StringEOL.Unknown || strEOL === expectedStrEOL) {
}
operations[i] = {
sortIndex: i,
identifier: op.identifier || null,
range: validatedRange,
rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn),
rangeLength: this.getValueLengthInRange(validatedRange),
text: validText,
eolCount: eolCount,
firstLineLength: firstLineLength,
lastLineLength: lastLineLength,
forceMoveMarkers: Boolean(op.forceMoveMarkers),
isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false
};
}
// Sort operations ascending
operations.sort(PieceTreeTextBuffer._sortOpsAscending);
let hasTouchingRanges = false;
for (let i = 0, count = operations.length - 1; i < count; i++) {
const nextRangeStart = operations[i + 1].range.getStartPosition();
if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {
throw new Error('Overlapping ranges are not allowed!');
}
}
if (canReduceOperations) {
operations = this._reduceOperations(operations);
}
// Delta encode operations
const reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);
textModel.ts ×10
const newTrimAutoWhitespaceCandidates: { lineNumber: number; oldContent: string }[] = [];
if (recordTrimAutoWhitespace) {
const op = operations[i];
const reverseRange = reverseRanges[i];
if (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: IReverseSingleEditOperation[] | null = null;
if (computeUndoEdits) {
let reverseRangeDeltaOffset = 0;
reverseOperations = [];
for (let i = 0; i < operations.length; i++) {
const op = operations[i];
const reverseRange = reverseRanges[i];
const bufferText = this.getValueInRange(op.range);
const reverseRangeOffset = op.rangeOffset + reverseRangeDeltaOffset;
reverseRangeDeltaOffset += (op.text.length - bufferText.length);
reverseOperations[i] = {
sortIndex: op.sortIndex,
identifier: op.identifier,
range: reverseRange,
text: bufferText,
textChange: new TextChange(op.rangeOffset, bufferText, reverseRangeOffset, op.text)
};
}
// Can only sort reverse operations when the order is not significant
if (!hasTouchingRanges) {
}
this._mightContainRTL = mightContainRTL;
this._mightContainUnusualLineTerminators = mightContainUnusualLineTerminators;
this._mightContainNonBasicASCII = mightContainNonBasicASCII;
const contentChanges = this._doApplyEdits(operations);
let trimAutoWhitespaceLineNumbers: number[] | null = null;
if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) {
textModel.ts ×10
// 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++) {
const lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber;
if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) {
// Do not have the same line number twice
continue;
}
const prevContent = newTrimAutoWhitespaceCandidates[i].oldContent;
const lineContent = this.getLineContent(lineNumber);
if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) {
continue;
}
trimAutoWhitespaceLineNumbers.push(lineNumber);
}
}
this._onDidChangeContent.fire();
return new ApplyEditsResult(
reverseOperations,
contentChanges,
trimAutoWhitespaceLineNumbers
);
/**
* Transform operations such that they represent the same logic edit,
* but that they also do not cause OOM crashes.
*/
private _reduceOperations(operations: IValidatedEditOperation[]): IValidatedEditOperation[] {
// We know from empirical testing that a thousand edits work fine regardless of their shape.
return operations;
}
// At one point, due to how events are emitted and how each operation is handled,
// some operations can trigger a high amount of temporary string allocations,
// that will immediately get edited again.
// e.g. a formatter inserting ridiculous ammounts of \n on a model with a single line
// Therefore, the strategy is to collapse all the operations into a huge single edit operation
return [this._toSingleEditOperation(operations)];
_toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation {
const firstEditRange = operations[0].range;
const lastEditRange = operations[operations.length - 1].range;
const entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn);
let lastEndLineNumber = firstEditRange.startLineNumber;
let lastEndColumn = firstEditRange.startColumn;
const result: string[] = [];
for (let i = 0, len = operations.length; i < len; i++) {
const operation = operations[i];
const range = operation.range;
forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers;
// (1) -- Push old text
result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn)));
// (2) -- Push new text
if (operation.text.length > 0) {
}
lastEndLineNumber = range.endLineNumber;
lastEndColumn = range.endColumn;
}
const text = result.join('');
const [eolCount, firstLineLength, lastLineLength] = countEOL(text);
return {
sortIndex: 0,
identifier: operations[0].identifier,
range: entireEditRange,
rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn),
rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined),
text: text,
eolCount: eolCount,
firstLineLength: firstLineLength,
lastLineLength: lastLineLength,
forceMoveMarkers: forceMoveMarkers,
isAutoWhitespaceEdit: false
};
}
private _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] {
const contentChanges: IInternalModelContentChange[] = [];
// operations are from bottom to top
for (let i = 0; i < operations.length; i++) {
const 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.text.length === 0) {
continue;
}
if (op.text) {
this._pieceTree.delete(op.rangeOffset, op.rangeLength);
this._pieceTree.insert(op.rangeOffset, op.text, true);
this._pieceTree.delete(op.rangeOffset, op.rangeLength);
}
const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
contentChanges.push({
range: contentChangeRange,
rangeLength: op.rangeLength,
text: op.text,
rangeOffset: op.rangeOffset,
forceMoveMarkers: op.forceMoveMarkers
});
}
}
findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] {
return this._pieceTree.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
pieceTreeBase.ts ×2
}
// #endregion
// #region helper
// testing purpose.
public getPieceTree(): PieceTreeBase {
}
public static _getInverseEditRange(range: Range, text: string) {
const startLineNumber = range.startLineNumber;
const startColumn = range.startColumn;
const [eolCount, firstLineLength, lastLineLength] = countEOL(text);
let resultRange: Range;
if (text.length > 0) {
// the operation inserts something
const lineCount = eolCount + 1;
if (lineCount === 1) {
// single line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLineLength);
} else {
// multi line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLineLength + 1);
}
} else {
// There is nothing to insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn);
}
return resultRange;
}
/**
* Assumes `operations` are validated and sorted ascending
*/
public static _getInverseEditRanges(operations: IValidatedEditOperation[]): Range[] {
let prevOpEndLineNumber: number = 0;
let prevOpEndColumn: number = 0;
let prevOp: IValidatedEditOperation | null = null;
for (let i = 0, len = operations.length; i < len; i++) {
const op = operations[i];
let startLineNumber: number;
let startColumn: number;
if (prevOp) {
startColumn = prevOpEndColumn + (op.range.startColumn - prevOp.range.endColumn);
startLineNumber = prevOpEndLineNumber + (op.range.startLineNumber - prevOp.range.endLineNumber);
pieceTreeTextBuffer.ts ×1
startColumn = op.range.startColumn;
}
startLineNumber = op.range.startLineNumber;
startColumn = op.range.startColumn;
}
let resultRange: Range;
if (op.text.length > 0) {
const lineCount = op.eolCount + 1;
if (lineCount === 1) {
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + op.firstLineLength);
resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, op.lastLineLength + 1);
}
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 {
if (r === 0) {
}
private static _sortOpsDescending(a: IValidatedEditOperation, b: IValidatedEditOperation): number {
if (r === 0) {
}
}