monaco-editor-core
Version:
A browser based code editor
246 lines (245 loc) • 9.66 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as buffer from '../../../base/common/buffer.js';
import { decodeUTF16LE } from './stringBuilder.js';
function escapeNewLine(str) {
return (str
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r'));
}
export class TextChange {
get oldLength() {
return this.oldText.length;
}
get oldEnd() {
return this.oldPosition + this.oldText.length;
}
get newLength() {
return this.newText.length;
}
get newEnd() {
return this.newPosition + this.newText.length;
}
constructor(oldPosition, oldText, newPosition, newText) {
this.oldPosition = oldPosition;
this.oldText = oldText;
this.newPosition = newPosition;
this.newText = newText;
}
toString() {
if (this.oldText.length === 0) {
return `(insert@${this.oldPosition} "${escapeNewLine(this.newText)}")`;
}
if (this.newText.length === 0) {
return `(delete@${this.oldPosition} "${escapeNewLine(this.oldText)}")`;
}
return `(replace@${this.oldPosition} "${escapeNewLine(this.oldText)}" with "${escapeNewLine(this.newText)}")`;
}
static _writeStringSize(str) {
return (4 + 2 * str.length);
}
static _writeString(b, str, offset) {
const len = str.length;
buffer.writeUInt32BE(b, len, offset);
offset += 4;
for (let i = 0; i < len; i++) {
buffer.writeUInt16LE(b, str.charCodeAt(i), offset);
offset += 2;
}
return offset;
}
static _readString(b, offset) {
const len = buffer.readUInt32BE(b, offset);
offset += 4;
return decodeUTF16LE(b, offset, len);
}
writeSize() {
return (+4 // oldPosition
+ 4 // newPosition
+ TextChange._writeStringSize(this.oldText)
+ TextChange._writeStringSize(this.newText));
}
write(b, offset) {
buffer.writeUInt32BE(b, this.oldPosition, offset);
offset += 4;
buffer.writeUInt32BE(b, this.newPosition, offset);
offset += 4;
offset = TextChange._writeString(b, this.oldText, offset);
offset = TextChange._writeString(b, this.newText, offset);
return offset;
}
static read(b, offset, dest) {
const oldPosition = buffer.readUInt32BE(b, offset);
offset += 4;
const newPosition = buffer.readUInt32BE(b, offset);
offset += 4;
const oldText = TextChange._readString(b, offset);
offset += TextChange._writeStringSize(oldText);
const newText = TextChange._readString(b, offset);
offset += TextChange._writeStringSize(newText);
dest.push(new TextChange(oldPosition, oldText, newPosition, newText));
return offset;
}
}
export function compressConsecutiveTextChanges(prevEdits, currEdits) {
if (prevEdits === null || prevEdits.length === 0) {
return currEdits;
}
const compressor = new TextChangeCompressor(prevEdits, currEdits);
return compressor.compress();
}
class TextChangeCompressor {
constructor(prevEdits, currEdits) {
this._prevEdits = prevEdits;
this._currEdits = currEdits;
this._result = [];
this._resultLen = 0;
this._prevLen = this._prevEdits.length;
this._prevDeltaOffset = 0;
this._currLen = this._currEdits.length;
this._currDeltaOffset = 0;
}
compress() {
let prevIndex = 0;
let currIndex = 0;
let prevEdit = this._getPrev(prevIndex);
let currEdit = this._getCurr(currIndex);
while (prevIndex < this._prevLen || currIndex < this._currLen) {
if (prevEdit === null) {
this._acceptCurr(currEdit);
currEdit = this._getCurr(++currIndex);
continue;
}
if (currEdit === null) {
this._acceptPrev(prevEdit);
prevEdit = this._getPrev(++prevIndex);
continue;
}
if (currEdit.oldEnd <= prevEdit.newPosition) {
this._acceptCurr(currEdit);
currEdit = this._getCurr(++currIndex);
continue;
}
if (prevEdit.newEnd <= currEdit.oldPosition) {
this._acceptPrev(prevEdit);
prevEdit = this._getPrev(++prevIndex);
continue;
}
if (currEdit.oldPosition < prevEdit.newPosition) {
const [e1, e2] = TextChangeCompressor._splitCurr(currEdit, prevEdit.newPosition - currEdit.oldPosition);
this._acceptCurr(e1);
currEdit = e2;
continue;
}
if (prevEdit.newPosition < currEdit.oldPosition) {
const [e1, e2] = TextChangeCompressor._splitPrev(prevEdit, currEdit.oldPosition - prevEdit.newPosition);
this._acceptPrev(e1);
prevEdit = e2;
continue;
}
// At this point, currEdit.oldPosition === prevEdit.newPosition
let mergePrev;
let mergeCurr;
if (currEdit.oldEnd === prevEdit.newEnd) {
mergePrev = prevEdit;
mergeCurr = currEdit;
prevEdit = this._getPrev(++prevIndex);
currEdit = this._getCurr(++currIndex);
}
else if (currEdit.oldEnd < prevEdit.newEnd) {
const [e1, e2] = TextChangeCompressor._splitPrev(prevEdit, currEdit.oldLength);
mergePrev = e1;
mergeCurr = currEdit;
prevEdit = e2;
currEdit = this._getCurr(++currIndex);
}
else {
const [e1, e2] = TextChangeCompressor._splitCurr(currEdit, prevEdit.newLength);
mergePrev = prevEdit;
mergeCurr = e1;
prevEdit = this._getPrev(++prevIndex);
currEdit = e2;
}
this._result[this._resultLen++] = new TextChange(mergePrev.oldPosition, mergePrev.oldText, mergeCurr.newPosition, mergeCurr.newText);
this._prevDeltaOffset += mergePrev.newLength - mergePrev.oldLength;
this._currDeltaOffset += mergeCurr.newLength - mergeCurr.oldLength;
}
const merged = TextChangeCompressor._merge(this._result);
const cleaned = TextChangeCompressor._removeNoOps(merged);
return cleaned;
}
_acceptCurr(currEdit) {
this._result[this._resultLen++] = TextChangeCompressor._rebaseCurr(this._prevDeltaOffset, currEdit);
this._currDeltaOffset += currEdit.newLength - currEdit.oldLength;
}
_getCurr(currIndex) {
return (currIndex < this._currLen ? this._currEdits[currIndex] : null);
}
_acceptPrev(prevEdit) {
this._result[this._resultLen++] = TextChangeCompressor._rebasePrev(this._currDeltaOffset, prevEdit);
this._prevDeltaOffset += prevEdit.newLength - prevEdit.oldLength;
}
_getPrev(prevIndex) {
return (prevIndex < this._prevLen ? this._prevEdits[prevIndex] : null);
}
static _rebaseCurr(prevDeltaOffset, currEdit) {
return new TextChange(currEdit.oldPosition - prevDeltaOffset, currEdit.oldText, currEdit.newPosition, currEdit.newText);
}
static _rebasePrev(currDeltaOffset, prevEdit) {
return new TextChange(prevEdit.oldPosition, prevEdit.oldText, prevEdit.newPosition + currDeltaOffset, prevEdit.newText);
}
static _splitPrev(edit, offset) {
const preText = edit.newText.substr(0, offset);
const postText = edit.newText.substr(offset);
return [
new TextChange(edit.oldPosition, edit.oldText, edit.newPosition, preText),
new TextChange(edit.oldEnd, '', edit.newPosition + offset, postText)
];
}
static _splitCurr(edit, offset) {
const preText = edit.oldText.substr(0, offset);
const postText = edit.oldText.substr(offset);
return [
new TextChange(edit.oldPosition, preText, edit.newPosition, edit.newText),
new TextChange(edit.oldPosition + offset, postText, edit.newEnd, '')
];
}
static _merge(edits) {
if (edits.length === 0) {
return edits;
}
const result = [];
let resultLen = 0;
let prev = edits[0];
for (let i = 1; i < edits.length; i++) {
const curr = edits[i];
if (prev.oldEnd === curr.oldPosition) {
// Merge into `prev`
prev = new TextChange(prev.oldPosition, prev.oldText + curr.oldText, prev.newPosition, prev.newText + curr.newText);
}
else {
result[resultLen++] = prev;
prev = curr;
}
}
result[resultLen++] = prev;
return result;
}
static _removeNoOps(edits) {
if (edits.length === 0) {
return edits;
}
const result = [];
let resultLen = 0;
for (let i = 0; i < edits.length; i++) {
const edit = edits[i];
if (edit.oldText === edit.newText) {
continue;
}
result[resultLen++] = edit;
}
return result;
}
}