| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { Emitter } from '../../../../base/common/event.js';
- import * as strings from '../../../../base/common/strings.js';
- import { Range } from '../../core/range.js';
- import { ApplyEditsResult } from '../../model.js';
- import { PieceTreeBase } from './pieceTreeBase.js';
- import { countEOL } from '../../core/eolCounter.js';
- import { TextChange } from '../../core/textChange.js';
- import { Disposable } from '../../../../base/common/lifecycle.js';
- export class PieceTreeTextBuffer extends Disposable {
- constructor(chunks, BOM, eol, containsRTL, containsUnusualLineTerminators, isBasicASCII, eolNormalized) {
- super();
- this._onDidChangeContent = this._register(new Emitter());
- this._BOM = BOM;
- this._mightContainNonBasicASCII = !isBasicASCII;
- this._mightContainRTL = containsRTL;
- this._mightContainUnusualLineTerminators = containsUnusualLineTerminators;
- this._pieceTree = new PieceTreeBase(chunks, eol, eolNormalized);
- }
- mightContainRTL() {
- return this._mightContainRTL;
- }
- mightContainUnusualLineTerminators() {
- return this._mightContainUnusualLineTerminators;
- }
- resetMightContainUnusualLineTerminators() {
- this._mightContainUnusualLineTerminators = false;
- }
- mightContainNonBasicASCII() {
- return this._mightContainNonBasicASCII;
- }
- getBOM() {
- return this._BOM;
- }
- getEOL() {
- return this._pieceTree.getEOL();
- }
- createSnapshot(preserveBOM) {
- return this._pieceTree.createSnapshot(preserveBOM ? this._BOM : '');
- }
- getOffsetAt(lineNumber, column) {
- return this._pieceTree.getOffsetAt(lineNumber, column);
- }
- getPositionAt(offset) {
- return this._pieceTree.getPositionAt(offset);
- }
- getRangeAt(start, length) {
- const end = start + length;
- const startPosition = this.getPositionAt(start);
- const endPosition = this.getPositionAt(end);
- return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
- }
- getValueInRange(range, eol = 0 /* EndOfLinePreference.TextDefined */) {
- if (range.isEmpty()) {
- return '';
- }
- const lineEnding = this._getEndOfLine(eol);
- return this._pieceTree.getValueInRange(range, lineEnding);
- }
- getValueLengthInRange(range, eol = 0 /* EndOfLinePreference.TextDefined */) {
- if (range.isEmpty()) {
- return 0;
- }
- if (range.startLineNumber === range.endLineNumber) {
- return (range.endColumn - range.startColumn);
- }
- const startOffset = this.getOffsetAt(range.startLineNumber, range.startColumn);
- const endOffset = this.getOffsetAt(range.endLineNumber, range.endColumn);
- return endOffset - startOffset;
- }
- getCharacterCountInRange(range, eol = 0 /* EndOfLinePreference.TextDefined */) {
- 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);
- }
- getLength() {
- return this._pieceTree.getLength();
- }
- getLineCount() {
- return this._pieceTree.getLineCount();
- }
- getLinesContent() {
- return this._pieceTree.getLinesContent();
- }
- getLineContent(lineNumber) {
- return this._pieceTree.getLineContent(lineNumber);
- }
- getLineCharCode(lineNumber, index) {
- return this._pieceTree.getLineCharCode(lineNumber, index);
- }
- getLineLength(lineNumber) {
- return this._pieceTree.getLineLength(lineNumber);
- }
- getLineFirstNonWhitespaceColumn(lineNumber) {
- const result = strings.firstNonWhitespaceIndex(this.getLineContent(lineNumber));
- if (result === -1) {
- return 0;
- }
- return result + 1;
- }
- getLineLastNonWhitespaceColumn(lineNumber) {
- const result = strings.lastNonWhitespaceIndex(this.getLineContent(lineNumber));
- if (result === -1) {
- return 0;
- }
- return result + 2;
- }
- _getEndOfLine(eol) {
- switch (eol) {
- case 1 /* EndOfLinePreference.LF */:
- return '\n';
- case 2 /* EndOfLinePreference.CRLF */:
- return '\r\n';
- case 0 /* EndOfLinePreference.TextDefined */:
- return this.getEOL();
- default:
- throw new Error('Unknown EOL preference');
- }
- }
- setEOL(newEOL) {
- this._pieceTree.setEOL(newEOL);
- }
- applyEdits(rawOperations, recordTrimAutoWhitespace, computeUndoEdits) {
- let mightContainRTL = this._mightContainRTL;
- let mightContainUnusualLineTerminators = this._mightContainUnusualLineTerminators;
- let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
- let canReduceOperations = true;
- let operations = [];
- for (let i = 0; i < rawOperations.length; i++) {
- const op = rawOperations[i];
- if (canReduceOperations && op._isTracked) {
- canReduceOperations = false;
- }
- const validatedRange = op.range;
- if (op.text) {
- let textMightContainNonBasicASCII = true;
- if (!mightContainNonBasicASCII) {
- textMightContainNonBasicASCII = !strings.isBasicASCII(op.text);
- mightContainNonBasicASCII = textMightContainNonBasicASCII;
- }
- if (!mightContainRTL && textMightContainNonBasicASCII) {
- // check if the new inserted text contains RTL
- mightContainRTL = strings.containsRTL(op.text);
- }
- if (!mightContainUnusualLineTerminators && textMightContainNonBasicASCII) {
- // check if the new inserted text contains unusual line terminators
- mightContainUnusualLineTerminators = strings.containsUnusualLineTerminators(op.text);
- }
- }
- let validText = '';
- let eolCount = 0;
- let firstLineLength = 0;
- let lastLineLength = 0;
- if (op.text) {
- let strEOL;
- [eolCount, firstLineLength, lastLineLength, strEOL] = countEOL(op.text);
- const bufferEOL = this.getEOL();
- const expectedStrEOL = (bufferEOL === '\r\n' ? 2 /* StringEOL.CRLF */ : 1 /* StringEOL.LF */);
- if (strEOL === 0 /* StringEOL.Unknown */ || strEOL === expectedStrEOL) {
- validText = op.text;
- }
- else {
- validText = op.text.replace(/\r\n|\r|\n/g, bufferEOL);
- }
- }
- 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 rangeEnd = operations[i].range.getEndPosition();
- const nextRangeStart = operations[i + 1].range.getStartPosition();
- if (nextRangeStart.isBeforeOrEqual(rangeEnd)) {
- if (nextRangeStart.isBefore(rangeEnd)) {
- // overlapping ranges
- throw new Error('Overlapping ranges are not allowed!');
- }
- hasTouchingRanges = true;
- }
- }
- if (canReduceOperations) {
- operations = this._reduceOperations(operations);
- }
- // Delta encode operations
- const reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);
- const newTrimAutoWhitespaceCandidates = [];
- if (recordTrimAutoWhitespace) {
- for (let i = 0; i < operations.length; i++) {
- 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 = 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) {
- reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
- }
- }
- this._mightContainRTL = mightContainRTL;
- this._mightContainUnusualLineTerminators = mightContainUnusualLineTerminators;
- this._mightContainNonBasicASCII = mightContainNonBasicASCII;
- const contentChanges = this._doApplyEdits(operations);
- let trimAutoWhitespaceLineNumbers = 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++) {
- 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.
- */
- _reduceOperations(operations) {
- if (operations.length < 1000) {
- // 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) {
- let forceMoveMarkers = false;
- 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 = [];
- 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) {
- result.push(operation.text);
- }
- 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, 0 /* EndOfLinePreference.TextDefined */),
- text: text,
- eolCount: eolCount,
- firstLineLength: firstLineLength,
- lastLineLength: lastLineLength,
- forceMoveMarkers: forceMoveMarkers,
- isAutoWhitespaceEdit: false
- };
- }
- _doApplyEdits(operations) {
- operations.sort(PieceTreeTextBuffer._sortOpsDescending);
- const contentChanges = [];
- // 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) {
- // no-op
- continue;
- }
- if (op.text) {
- // replacement
- this._pieceTree.delete(op.rangeOffset, op.rangeLength);
- this._pieceTree.insert(op.rangeOffset, op.text, true);
- }
- else {
- // deletion
- 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
- });
- }
- return contentChanges;
- }
- findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount) {
- return this._pieceTree.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
- }
- /**
- * Assumes `operations` are validated and sorted ascending
- */
- static _getInverseEditRanges(operations) {
- const result = [];
- let prevOpEndLineNumber = 0;
- let prevOpEndColumn = 0;
- let prevOp = null;
- for (let i = 0, len = operations.length; i < len; i++) {
- const op = operations[i];
- let startLineNumber;
- let startColumn;
- 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;
- if (op.text.length > 0) {
- // the operation inserts something
- const lineCount = op.eolCount + 1;
- if (lineCount === 1) {
- // single line insert
- resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + op.firstLineLength);
- }
- else {
- // multi line insert
- resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, op.lastLineLength + 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;
- }
- static _sortOpsAscending(a, b) {
- const r = Range.compareRangesUsingEnds(a.range, b.range);
- if (r === 0) {
- return a.sortIndex - b.sortIndex;
- }
- return r;
- }
- static _sortOpsDescending(a, b) {
- const r = Range.compareRangesUsingEnds(a.range, b.range);
- if (r === 0) {
- return b.sortIndex - a.sortIndex;
- }
- return -r;
- }
- }
|