Path: blob/main/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../base/common/event.js';6import * as strings from '../../../../base/common/strings.js';7import { Position } from '../../core/position.js';8import { Range } from '../../core/range.js';9import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContentChange, ISingleEditOperationIdentifier, ITextBuffer, ITextSnapshot, ValidAnnotatedEditOperation, IValidEditOperation, SearchData } from '../../model.js';10import { PieceTreeBase, StringBuffer } from './pieceTreeBase.js';11import { countEOL, StringEOL } from '../../core/misc/eolCounter.js';12import { TextChange } from '../../core/textChange.js';13import { Disposable } from '../../../../base/common/lifecycle.js';1415export interface IValidatedEditOperation {16sortIndex: number;17identifier: ISingleEditOperationIdentifier | null;18range: Range;19rangeOffset: number;20rangeLength: number;21text: string;22eolCount: number;23firstLineLength: number;24lastLineLength: number;25forceMoveMarkers: boolean;26isAutoWhitespaceEdit: boolean;27}2829interface IReverseSingleEditOperation extends IValidEditOperation {30sortIndex: number;31}3233export class PieceTreeTextBuffer extends Disposable implements ITextBuffer {34private _pieceTree: PieceTreeBase;35private readonly _BOM: string;36private _mightContainRTL: boolean;37private _mightContainUnusualLineTerminators: boolean;38private _mightContainNonBasicASCII: boolean;3940private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());41public get onDidChangeContent(): Event<void> { return this._onDidChangeContent.event; }4243constructor(chunks: StringBuffer[], BOM: string, eol: '\r\n' | '\n', containsRTL: boolean, containsUnusualLineTerminators: boolean, isBasicASCII: boolean, eolNormalized: boolean) {44super();45this._BOM = BOM;46this._mightContainNonBasicASCII = !isBasicASCII;47this._mightContainRTL = containsRTL;48this._mightContainUnusualLineTerminators = containsUnusualLineTerminators;49this._pieceTree = new PieceTreeBase(chunks, eol, eolNormalized);50}5152// #region TextBuffer53public equals(other: ITextBuffer): boolean {54if (!(other instanceof PieceTreeTextBuffer)) {55return false;56}57if (this._BOM !== other._BOM) {58return false;59}60if (this.getEOL() !== other.getEOL()) {61return false;62}63return this._pieceTree.equal(other._pieceTree);64}65public mightContainRTL(): boolean {66return this._mightContainRTL;67}68public mightContainUnusualLineTerminators(): boolean {69return this._mightContainUnusualLineTerminators;70}71public resetMightContainUnusualLineTerminators(): void {72this._mightContainUnusualLineTerminators = false;73}74public mightContainNonBasicASCII(): boolean {75return this._mightContainNonBasicASCII;76}77public getBOM(): string {78return this._BOM;79}80public getEOL(): '\r\n' | '\n' {81return this._pieceTree.getEOL();82}8384public createSnapshot(preserveBOM: boolean): ITextSnapshot {85return this._pieceTree.createSnapshot(preserveBOM ? this._BOM : '');86}8788public getOffsetAt(lineNumber: number, column: number): number {89return this._pieceTree.getOffsetAt(lineNumber, column);90}9192public getPositionAt(offset: number): Position {93return this._pieceTree.getPositionAt(offset);94}9596public getRangeAt(start: number, length: number): Range {97const end = start + length;98const startPosition = this.getPositionAt(start);99const endPosition = this.getPositionAt(end);100return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);101}102103public getValueInRange(range: Range, eol: EndOfLinePreference = EndOfLinePreference.TextDefined): string {104if (range.isEmpty()) {105return '';106}107108const lineEnding = this._getEndOfLine(eol);109return this._pieceTree.getValueInRange(range, lineEnding);110}111112public getValueLengthInRange(range: Range, eol: EndOfLinePreference = EndOfLinePreference.TextDefined): number {113if (range.isEmpty()) {114return 0;115}116117if (range.startLineNumber === range.endLineNumber) {118return (range.endColumn - range.startColumn);119}120121const startOffset = this.getOffsetAt(range.startLineNumber, range.startColumn);122const endOffset = this.getOffsetAt(range.endLineNumber, range.endColumn);123124// offsets use the text EOL, so we need to compensate for length differences125// if the requested EOL doesn't match the text EOL126let eolOffsetCompensation = 0;127const desiredEOL = this._getEndOfLine(eol);128const actualEOL = this.getEOL();129if (desiredEOL.length !== actualEOL.length) {130const delta = desiredEOL.length - actualEOL.length;131const eolCount = range.endLineNumber - range.startLineNumber;132eolOffsetCompensation = delta * eolCount;133}134135return endOffset - startOffset + eolOffsetCompensation;136}137138public getCharacterCountInRange(range: Range, eol: EndOfLinePreference = EndOfLinePreference.TextDefined): number {139if (this._mightContainNonBasicASCII) {140// we must count by iterating141142let result = 0;143144const fromLineNumber = range.startLineNumber;145const toLineNumber = range.endLineNumber;146for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {147const lineContent = this.getLineContent(lineNumber);148const fromOffset = (lineNumber === fromLineNumber ? range.startColumn - 1 : 0);149const toOffset = (lineNumber === toLineNumber ? range.endColumn - 1 : lineContent.length);150151for (let offset = fromOffset; offset < toOffset; offset++) {152if (strings.isHighSurrogate(lineContent.charCodeAt(offset))) {153result = result + 1;154offset = offset + 1;155} else {156result = result + 1;157}158}159}160161result += this._getEndOfLine(eol).length * (toLineNumber - fromLineNumber);162163return result;164}165166return this.getValueLengthInRange(range, eol);167}168169public getNearestChunk(offset: number): string {170return this._pieceTree.getNearestChunk(offset);171}172173public getLength(): number {174return this._pieceTree.getLength();175}176177public getLineCount(): number {178return this._pieceTree.getLineCount();179}180181public getLinesContent(): string[] {182return this._pieceTree.getLinesContent();183}184185public getLineContent(lineNumber: number): string {186return this._pieceTree.getLineContent(lineNumber);187}188189public getLineCharCode(lineNumber: number, index: number): number {190return this._pieceTree.getLineCharCode(lineNumber, index);191}192193public getCharCode(offset: number): number {194return this._pieceTree.getCharCode(offset);195}196197public getLineLength(lineNumber: number): number {198return this._pieceTree.getLineLength(lineNumber);199}200201public getLineMinColumn(lineNumber: number): number {202return 1;203}204205public getLineMaxColumn(lineNumber: number): number {206return this.getLineLength(lineNumber) + 1;207}208209public getLineFirstNonWhitespaceColumn(lineNumber: number): number {210const result = strings.firstNonWhitespaceIndex(this.getLineContent(lineNumber));211if (result === -1) {212return 0;213}214return result + 1;215}216217public getLineLastNonWhitespaceColumn(lineNumber: number): number {218const result = strings.lastNonWhitespaceIndex(this.getLineContent(lineNumber));219if (result === -1) {220return 0;221}222return result + 2;223}224225private _getEndOfLine(eol: EndOfLinePreference): string {226switch (eol) {227case EndOfLinePreference.LF:228return '\n';229case EndOfLinePreference.CRLF:230return '\r\n';231case EndOfLinePreference.TextDefined:232return this.getEOL();233default:234throw new Error('Unknown EOL preference');235}236}237238public setEOL(newEOL: '\r\n' | '\n'): void {239this._pieceTree.setEOL(newEOL);240}241242public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult {243let mightContainRTL = this._mightContainRTL;244let mightContainUnusualLineTerminators = this._mightContainUnusualLineTerminators;245let mightContainNonBasicASCII = this._mightContainNonBasicASCII;246let canReduceOperations = true;247248let operations: IValidatedEditOperation[] = [];249for (let i = 0; i < rawOperations.length; i++) {250const op = rawOperations[i];251if (canReduceOperations && op._isTracked) {252canReduceOperations = false;253}254const validatedRange = op.range;255if (op.text) {256let textMightContainNonBasicASCII = true;257if (!mightContainNonBasicASCII) {258textMightContainNonBasicASCII = !strings.isBasicASCII(op.text);259mightContainNonBasicASCII = textMightContainNonBasicASCII;260}261if (!mightContainRTL && textMightContainNonBasicASCII) {262// check if the new inserted text contains RTL263mightContainRTL = strings.containsRTL(op.text);264}265if (!mightContainUnusualLineTerminators && textMightContainNonBasicASCII) {266// check if the new inserted text contains unusual line terminators267mightContainUnusualLineTerminators = strings.containsUnusualLineTerminators(op.text);268}269}270271let validText = '';272let eolCount = 0;273let firstLineLength = 0;274let lastLineLength = 0;275if (op.text) {276let strEOL: StringEOL;277[eolCount, firstLineLength, lastLineLength, strEOL] = countEOL(op.text);278279const bufferEOL = this.getEOL();280const expectedStrEOL = (bufferEOL === '\r\n' ? StringEOL.CRLF : StringEOL.LF);281if (strEOL === StringEOL.Unknown || strEOL === expectedStrEOL) {282validText = op.text;283} else {284validText = op.text.replace(/\r\n|\r|\n/g, bufferEOL);285}286}287288operations[i] = {289sortIndex: i,290identifier: op.identifier || null,291range: validatedRange,292rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn),293rangeLength: this.getValueLengthInRange(validatedRange),294text: validText,295eolCount: eolCount,296firstLineLength: firstLineLength,297lastLineLength: lastLineLength,298forceMoveMarkers: Boolean(op.forceMoveMarkers),299isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false300};301}302303// Sort operations ascending304operations.sort(PieceTreeTextBuffer._sortOpsAscending);305306let hasTouchingRanges = false;307for (let i = 0, count = operations.length - 1; i < count; i++) {308const rangeEnd = operations[i].range.getEndPosition();309const nextRangeStart = operations[i + 1].range.getStartPosition();310311if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {312if (nextRangeStart.isBefore(rangeEnd)) {313// overlapping ranges314throw new Error('Overlapping ranges are not allowed!');315}316hasTouchingRanges = true;317}318}319320if (canReduceOperations) {321operations = this._reduceOperations(operations);322}323324// Delta encode operations325const reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);326const newTrimAutoWhitespaceCandidates: { lineNumber: number; oldContent: string }[] = [];327if (recordTrimAutoWhitespace) {328for (let i = 0; i < operations.length; i++) {329const op = operations[i];330const reverseRange = reverseRanges[i];331332if (op.isAutoWhitespaceEdit && op.range.isEmpty()) {333// Record already the future line numbers that might be auto whitespace removal candidates on next edit334for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {335let currentLineContent = '';336if (lineNumber === reverseRange.startLineNumber) {337currentLineContent = this.getLineContent(op.range.startLineNumber);338if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {339continue;340}341}342newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });343}344}345}346}347348let reverseOperations: IReverseSingleEditOperation[] | null = null;349if (computeUndoEdits) {350351let reverseRangeDeltaOffset = 0;352reverseOperations = [];353for (let i = 0; i < operations.length; i++) {354const op = operations[i];355const reverseRange = reverseRanges[i];356const bufferText = this.getValueInRange(op.range);357const reverseRangeOffset = op.rangeOffset + reverseRangeDeltaOffset;358reverseRangeDeltaOffset += (op.text.length - bufferText.length);359360reverseOperations[i] = {361sortIndex: op.sortIndex,362identifier: op.identifier,363range: reverseRange,364text: bufferText,365textChange: new TextChange(op.rangeOffset, bufferText, reverseRangeOffset, op.text)366};367}368369// Can only sort reverse operations when the order is not significant370if (!hasTouchingRanges) {371reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);372}373}374375376this._mightContainRTL = mightContainRTL;377this._mightContainUnusualLineTerminators = mightContainUnusualLineTerminators;378this._mightContainNonBasicASCII = mightContainNonBasicASCII;379380const contentChanges = this._doApplyEdits(operations);381382let trimAutoWhitespaceLineNumbers: number[] | null = null;383if (recordTrimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) {384// sort line numbers auto whitespace removal candidates for next edit descending385newTrimAutoWhitespaceCandidates.sort((a, b) => b.lineNumber - a.lineNumber);386387trimAutoWhitespaceLineNumbers = [];388for (let i = 0, len = newTrimAutoWhitespaceCandidates.length; i < len; i++) {389const lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber;390if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) {391// Do not have the same line number twice392continue;393}394395const prevContent = newTrimAutoWhitespaceCandidates[i].oldContent;396const lineContent = this.getLineContent(lineNumber);397398if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) {399continue;400}401402trimAutoWhitespaceLineNumbers.push(lineNumber);403}404}405406this._onDidChangeContent.fire();407408return new ApplyEditsResult(409reverseOperations,410contentChanges,411trimAutoWhitespaceLineNumbers412);413}414415/**416* Transform operations such that they represent the same logic edit,417* but that they also do not cause OOM crashes.418*/419private _reduceOperations(operations: IValidatedEditOperation[]): IValidatedEditOperation[] {420if (operations.length < 1000) {421// We know from empirical testing that a thousand edits work fine regardless of their shape.422return operations;423}424425// At one point, due to how events are emitted and how each operation is handled,426// some operations can trigger a high amount of temporary string allocations,427// that will immediately get edited again.428// e.g. a formatter inserting ridiculous ammounts of \n on a model with a single line429// Therefore, the strategy is to collapse all the operations into a huge single edit operation430return [this._toSingleEditOperation(operations)];431}432433_toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation {434let forceMoveMarkers = false;435const firstEditRange = operations[0].range;436const lastEditRange = operations[operations.length - 1].range;437const entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn);438let lastEndLineNumber = firstEditRange.startLineNumber;439let lastEndColumn = firstEditRange.startColumn;440const result: string[] = [];441442for (let i = 0, len = operations.length; i < len; i++) {443const operation = operations[i];444const range = operation.range;445446forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers;447448// (1) -- Push old text449result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn)));450451// (2) -- Push new text452if (operation.text.length > 0) {453result.push(operation.text);454}455456lastEndLineNumber = range.endLineNumber;457lastEndColumn = range.endColumn;458}459460const text = result.join('');461const [eolCount, firstLineLength, lastLineLength] = countEOL(text);462463return {464sortIndex: 0,465identifier: operations[0].identifier,466range: entireEditRange,467rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn),468rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined),469text: text,470eolCount: eolCount,471firstLineLength: firstLineLength,472lastLineLength: lastLineLength,473forceMoveMarkers: forceMoveMarkers,474isAutoWhitespaceEdit: false475};476}477478private _doApplyEdits(operations: IValidatedEditOperation[]): IInternalModelContentChange[] {479operations.sort(PieceTreeTextBuffer._sortOpsDescending);480481const contentChanges: IInternalModelContentChange[] = [];482483// operations are from bottom to top484for (let i = 0; i < operations.length; i++) {485const op = operations[i];486487const startLineNumber = op.range.startLineNumber;488const startColumn = op.range.startColumn;489const endLineNumber = op.range.endLineNumber;490const endColumn = op.range.endColumn;491492if (startLineNumber === endLineNumber && startColumn === endColumn && op.text.length === 0) {493// no-op494continue;495}496497if (op.text) {498// replacement499this._pieceTree.delete(op.rangeOffset, op.rangeLength);500this._pieceTree.insert(op.rangeOffset, op.text, true);501502} else {503// deletion504this._pieceTree.delete(op.rangeOffset, op.rangeLength);505}506507const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn);508contentChanges.push({509range: contentChangeRange,510rangeLength: op.rangeLength,511text: op.text,512rangeOffset: op.rangeOffset,513forceMoveMarkers: op.forceMoveMarkers514});515}516return contentChanges;517}518519findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] {520return this._pieceTree.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);521}522523// #endregion524525// #region helper526// testing purpose.527public getPieceTree(): PieceTreeBase {528return this._pieceTree;529}530531public static _getInverseEditRange(range: Range, text: string) {532const startLineNumber = range.startLineNumber;533const startColumn = range.startColumn;534const [eolCount, firstLineLength, lastLineLength] = countEOL(text);535let resultRange: Range;536537if (text.length > 0) {538// the operation inserts something539const lineCount = eolCount + 1;540541if (lineCount === 1) {542// single line insert543resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLineLength);544} else {545// multi line insert546resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLineLength + 1);547}548} else {549// There is nothing to insert550resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn);551}552553return resultRange;554}555556/**557* Assumes `operations` are validated and sorted ascending558*/559public static _getInverseEditRanges(operations: IValidatedEditOperation[]): Range[] {560const result: Range[] = [];561562let prevOpEndLineNumber: number = 0;563let prevOpEndColumn: number = 0;564let prevOp: IValidatedEditOperation | null = null;565for (let i = 0, len = operations.length; i < len; i++) {566const op = operations[i];567568let startLineNumber: number;569let startColumn: number;570571if (prevOp) {572if (prevOp.range.endLineNumber === op.range.startLineNumber) {573startLineNumber = prevOpEndLineNumber;574startColumn = prevOpEndColumn + (op.range.startColumn - prevOp.range.endColumn);575} else {576startLineNumber = prevOpEndLineNumber + (op.range.startLineNumber - prevOp.range.endLineNumber);577startColumn = op.range.startColumn;578}579} else {580startLineNumber = op.range.startLineNumber;581startColumn = op.range.startColumn;582}583584let resultRange: Range;585586if (op.text.length > 0) {587// the operation inserts something588const lineCount = op.eolCount + 1;589590if (lineCount === 1) {591// single line insert592resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + op.firstLineLength);593} else {594// multi line insert595resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, op.lastLineLength + 1);596}597} else {598// There is nothing to insert599resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn);600}601602prevOpEndLineNumber = resultRange.endLineNumber;603prevOpEndColumn = resultRange.endColumn;604605result.push(resultRange);606prevOp = op;607}608609return result;610}611612private static _sortOpsAscending(a: IValidatedEditOperation, b: IValidatedEditOperation): number {613const r = Range.compareRangesUsingEnds(a.range, b.range);614if (r === 0) {615return a.sortIndex - b.sortIndex;616}617return r;618}619620private static _sortOpsDescending(a: IValidatedEditOperation, b: IValidatedEditOperation): number {621const r = Range.compareRangesUsingEnds(a.range, b.range);622if (r === 0) {623return b.sortIndex - a.sortIndex;624}625return -r;626}627// #endregion628}629630631