UNPKG

monaco-editor-core

Version:

A browser based code editor

456 lines (455 loc) • 21 kB
/*--------------------------------------------------------------------------------------------- * 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); // 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 delta = desiredEOL.length - actualEOL.length; const eolCount = range.endLineNumber - range.startLineNumber; eolOffsetCompensation = delta * eolCount; } return endOffset - startOffset + eolOffsetCompensation; } 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; } }