@pierre/diffs
Version:
706 lines (705 loc) • 26.2 kB
JavaScript
import { computeLineOffsets } from "../utils/computeFileOffsets.js";
//#region src/editor/pieceTable.ts
const MAX_FIND_MATCHES = 1e5;
const LINE_FEED = 10;
const CARRIAGE_RETURN = 13;
const WORD_SEPARATORS = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";
var Piece = class {
source;
offset;
length;
lineOffsetStart;
lineOffsetEnd;
hasVirtualTrailingCRBreak;
firstCharCode;
lastCharCode;
static Original = 0;
static Added = 1;
constructor(source, offset, length, lineOffsetStart, lineOffsetEnd, hasVirtualTrailingCRBreak, firstCharCode, lastCharCode) {
this.source = source;
this.offset = offset;
this.length = length;
this.lineOffsetStart = lineOffsetStart;
this.lineOffsetEnd = lineOffsetEnd;
this.hasVirtualTrailingCRBreak = hasVirtualTrailingCRBreak;
this.firstCharCode = firstCharCode;
this.lastCharCode = lastCharCode;
}
get lineBreakCount() {
return this.lineOffsetEnd - this.lineOffsetStart + (this.hasVirtualTrailingCRBreak ? 1 : 0);
}
};
var TextBuffer = class {
text;
lineOffsets;
constructor(text) {
this.text = text;
this.lineOffsets = computeLineOffsets(text);
}
append(text) {
const offset = this.text.length;
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i);
if (charCode !== LINE_FEED && charCode !== CARRIAGE_RETURN) continue;
if (charCode === CARRIAGE_RETURN && text.charCodeAt(i + 1) === LINE_FEED) i++;
this.lineOffsets.push(offset + i + 1);
}
this.text += text;
return offset;
}
};
var PieceNode = class {
piece;
subtreeLength;
subtreeLineBreakCount;
subtreeFirstCharCode;
subtreeLastCharCode;
left = null;
right = null;
parent = null;
priority = 0;
constructor(piece, subtreeLength = piece.length, subtreeLineBreakCount = piece.lineBreakCount, subtreeFirstCharCode = piece.firstCharCode, subtreeLastCharCode = piece.lastCharCode) {
this.piece = piece;
this.subtreeLength = subtreeLength;
this.subtreeLineBreakCount = subtreeLineBreakCount;
this.subtreeFirstCharCode = subtreeFirstCharCode;
this.subtreeLastCharCode = subtreeLastCharCode;
}
updateSubtreeLength() {
this.subtreeLength = (this.left?.subtreeLength ?? 0) + this.piece.length + (this.right?.subtreeLength ?? 0);
this.subtreeLineBreakCount = (this.left?.subtreeLineBreakCount ?? 0) + this.piece.lineBreakCount + (this.right?.subtreeLineBreakCount ?? 0) - (formsCRLF(this.left?.subtreeLastCharCode, this.piece.firstCharCode) ? 1 : 0) - (formsCRLF(this.piece.lastCharCode, this.right?.subtreeFirstCharCode) ? 1 : 0);
this.subtreeFirstCharCode = this.left?.subtreeFirstCharCode ?? this.piece.firstCharCode;
this.subtreeLastCharCode = this.right?.subtreeLastCharCode ?? this.piece.lastCharCode;
}
};
/**
* A piece table is a data structure that allows for efficient insertion and deletion of text.
* It is a tree of pieces, where each piece is a segment of text that is either original or added.
* The tree is a treap (a binary search tree that also keeps each node's random priority in heap
* order, which keeps the tree balanced without an explicit rebalancing pass). Each edit reshapes
* only the nodes along one root-to-leaf path via split and merge in O(log P), instead of
* rebuilding all P pieces.
* Inspired by https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation
*/
var PieceTable = class {
#original;
#add = new TextBuffer("");
#root = null;
#length = 0;
#lineCount = 0;
#lastVisitedLine = null;
#lastVisitedLineLength = null;
#lastPosition = null;
#priorityState = 2654435769;
constructor(originalText) {
this.#original = new TextBuffer(originalText);
const piece = this.#createPiece(Piece.Original, 0, originalText.length);
this.#root = piece.length > 0 ? this.#createNode(piece) : null;
this.#length = this.#root?.subtreeLength ?? 0;
this.#lineCount = (this.#root?.subtreeLineBreakCount ?? 0) + 1;
}
get lineCount() {
return this.#lineCount;
}
getText(range) {
if (range === void 0) return this.#textFromPieces();
const start = this.offsetAt(range.start);
const end = this.offsetAt(range.end);
return this.getTextSlice(start, end);
}
getLineText(line, includeLineBreak = false) {
if (this.#lastVisitedLine !== null && this.#lastVisitedLine[0] === line && this.#lastVisitedLine[1] === includeLineBreak) return this.#lastVisitedLine[2];
const offset = this.#getLineOffset(line);
if (offset === void 0) throw new Error(`Line index out of range: ${line}`);
const text = this.getTextSlice(offset[0], offset[1], !includeLineBreak);
this.#lastVisitedLine = [
line,
includeLineBreak,
text
];
this.#lastVisitedLineLength = [
line,
includeLineBreak,
text.length
];
return text;
}
getLineLength(line, includeLineBreak = false) {
const lastVisitedLineLength = this.#lastVisitedLineLength;
const lastVisitedLine = this.#lastVisitedLine;
if (lastVisitedLineLength !== null && lastVisitedLineLength[0] === line && lastVisitedLineLength[1] === includeLineBreak) return lastVisitedLineLength[2];
if (lastVisitedLine !== null && lastVisitedLine[0] === line && lastVisitedLine[1] === includeLineBreak) {
const length = lastVisitedLine[2].length;
this.#lastVisitedLineLength = [
line,
includeLineBreak,
length
];
return length;
}
const offset = this.#getLineOffset(line);
if (offset === void 0) throw new Error(`Line index out of range: ${line}`);
const [start, end] = offset;
let length = end - start;
if (!includeLineBreak) while (length > 0 && isEOL(this.charAt(start + length - 1).charCodeAt(0))) length--;
this.#lastVisitedLineLength = [
line,
includeLineBreak,
length
];
return length;
}
getTextSlice(start, end, trimEOF = false) {
if (start >= end) return "";
const sliceStart = clamp(start, 0, this.#length);
const sliceEnd = clamp(end, sliceStart, this.#length);
if (sliceStart >= sliceEnd) return "";
const location = this.#findPieceAtOffset(sliceStart);
if (location === void 0) return "";
const chunks = [];
let [node, offsetInPiece] = location;
let remaining = sliceEnd - sliceStart;
while (node !== null && remaining > 0) {
const takeLength = Math.min(node.piece.length - offsetInPiece, remaining);
const buffer = this.#bufferFor(node.piece.source);
const start = node.piece.offset + offsetInPiece;
let end = start + takeLength;
if (trimEOF) while (end > start && isEOL(buffer.text.charCodeAt(end - 1))) end--;
chunks.push(buffer.text.slice(start, end));
remaining -= takeLength;
offsetInPiece = 0;
node = this.#nextNode(node);
}
return chunks.join("");
}
charAt(offset) {
const location = this.#findPieceAtOffset(offset);
if (location === void 0) return "";
const [node, offsetInPiece] = location;
return this.#bufferFor(node.piece.source).text.charAt(node.piece.offset + offsetInPiece);
}
includes(needle) {
if (needle.length === 0) return true;
const prefixTable = createPrefixTable(needle);
let matched = 0;
let found = false;
this.#forEachPieceSegment((segment) => {
for (let offset = segment.start; offset < segment.end; offset++) {
const charCode = segment.text.charCodeAt(offset);
while (matched > 0 && charCode !== needle.charCodeAt(matched)) matched = prefixTable[matched - 1];
if (charCode === needle.charCodeAt(matched)) matched++;
if (matched === needle.length) {
found = true;
return false;
}
}
return true;
});
return found;
}
findNextNonOverlappingSubstring(needle, occupied) {
if (needle.length === 0 || needle.length > this.#length) return;
const ranges = normalizeRanges(occupied, this.#length);
const pivot = ranges.reduce((max, [, end]) => Math.max(max, end), 0);
const prefixTable = createPrefixTable(needle);
let matched = 0;
let documentOffset = 0;
let wrappedOffset;
let foundOffset;
this.#forEachPieceSegment((segment) => {
for (let offset = segment.start; offset < segment.end; offset++) {
const charCode = segment.text.charCodeAt(offset);
while (matched > 0 && charCode !== needle.charCodeAt(matched)) matched = prefixTable[matched - 1];
if (charCode === needle.charCodeAt(matched)) matched++;
if (matched === needle.length) {
const start = documentOffset - needle.length + 1;
if (!rangeOverlaps(ranges, start, start + needle.length)) {
if (start >= pivot) {
foundOffset = start;
return false;
}
wrappedOffset ??= start;
}
matched = prefixTable[matched - 1];
}
documentOffset++;
}
return true;
});
return foundOffset ?? wrappedOffset;
}
search(searchParams) {
if (searchParams.text.length === 0 || this.#length === 0) return [];
if (searchParams.text.includes("\n") || searchParams.text.includes("\r") || searchParams.regex && (searchParams.text.includes("\\n") || searchParams.text.includes("\\r"))) return [];
let pattern;
try {
pattern = compileSearchRegExp(searchParams.text, searchParams.regex, searchParams.caseSensitive);
} catch {
return [];
}
return this.#collectSearchMatchesLineByLine(pattern, searchParams.wholeWord, MAX_FIND_MATCHES);
}
#collectSearchMatchesLineByLine(pattern, wholeWord, limit) {
const out = [];
const documentText = this.#textFromPieces();
const docLength = documentText.length;
const lineOffsets = computeLineOffsets(documentText);
for (let line = 0; line < lineOffsets.length; line++) {
const lineStart = lineOffsets[line];
let lineEnd = lineOffsets[line + 1] ?? docLength;
while (lineEnd > lineStart && isEOL(documentText.charCodeAt(lineEnd - 1))) lineEnd--;
const lineText = documentText.slice(lineStart, lineEnd);
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(lineText)) !== null) {
const rel = match.index;
const fragment = match[0];
if (fragment.length === 0) {
pattern.lastIndex = advancePastEmptyMatch(lineText, rel);
continue;
}
const docStart = lineStart + rel;
if (!wholeWord || isWholeWordAtDocOffsets(documentText, docStart, fragment.length)) {
out.push([docStart, docStart + fragment.length]);
if (out.length >= limit) return out;
}
if (rel === pattern.lastIndex) pattern.lastIndex = advancePastEmptyMatch(lineText, rel);
}
}
return out;
}
insert(text, offset) {
if (text.length === 0) return;
const start = clamp(offset, 0, this.#length);
this.#replaceRangeIncremental(start, start, text);
this.#invalidateCaches();
}
delete(offset, length) {
if (length <= 0 || this.#length === 0) return;
const start = clamp(offset, 0, this.#length);
const end = clamp(start + length, start, this.#length);
if (start === end) return;
this.#replaceRangeIncremental(start, end, "");
this.#invalidateCaches();
}
applyEdits(edits) {
if (edits.length === 0) return;
for (let i = edits.length - 1; i >= 0; i--) {
const edit = edits[i];
const start = clamp(edit.start, 0, this.#length);
const end = clamp(edit.end, start, this.#length);
this.#replaceRangeIncremental(start, end, edit.text);
}
this.#invalidateCaches();
}
positionAt(offset) {
const clampedOffset = clamp(offset, 0, this.#length);
if (this.#length === 0) return {
line: 0,
character: 0
};
const line = this.#lineAtOffset(clampedOffset);
const character = clampedOffset - (line === 0 ? 0 : this.#lineBreakOffset(line - 1));
this.#lastPosition = [
line,
character,
clampedOffset
];
return {
line,
character
};
}
positionsAt(offsets) {
const positions = Array.from({ length: offsets.length });
if (offsets.length === 0) return positions;
if (this.#length === 0) return positions.fill({
line: 0,
character: 0
});
for (let i = 0; i < offsets.length; i++) positions[i] = this.positionAt(offsets[i]);
return positions;
}
offsetAt(position) {
if (position.line < 0 || this.#length === 0) return 0;
if (position.line >= this.#lineCount) throw new Error(`Line index out of range: ${position.line}`);
const lastPosition = this.#lastPosition;
if (lastPosition !== null && lastPosition[0] === position.line && lastPosition[1] === position.character) return lastPosition[2];
const offset = this.#getLineOffset(position.line);
if (offset === void 0) throw new Error(`Line index out of range: ${position.line}`);
const character = clamp(position.character, 0, offset[1] - offset[0]);
return offset[0] + character;
}
#findPieceAtOffset(offset) {
if (offset < 0 || offset >= this.#length) return;
let node = this.#root;
let remaining = offset;
while (node !== null) {
const leftLength = node.left?.subtreeLength ?? 0;
if (remaining < leftLength) {
node = node.left;
continue;
}
remaining -= leftLength;
if (remaining < node.piece.length) return [node, remaining];
remaining -= node.piece.length;
node = node.right;
}
}
#nextNode(node) {
if (node.right !== null) {
let next = node.right;
while (next.left !== null) next = next.left;
return next;
}
let current = node;
while (current.parent !== null && current === current.parent.right) current = current.parent;
return current.parent;
}
#getLineOffset(line) {
if (line < 0) throw new Error(`Line index out of range: ${line}`);
if (this.#length === 0) {
if (line === 0) return [0, 0];
throw new Error(`Line index out of range: ${line}`);
}
if (line >= this.#lineCount) throw new Error(`Line index out of range: ${line}`);
return [line === 0 ? 0 : this.#lineBreakOffset(line - 1), line < this.#lineCount - 1 ? this.#lineBreakOffset(line) : this.#length];
}
#lineAtOffset(offset) {
let node = this.#root;
let remaining = clamp(offset, 0, this.#length);
let line = 0;
while (node !== null) {
const leftLength = node.left?.subtreeLength ?? 0;
if (remaining < leftLength) {
node = node.left;
continue;
}
line += (node.left?.subtreeLineBreakCount ?? 0) - (formsCRLF(node.left?.subtreeLastCharCode, node.piece.firstCharCode) ? 1 : 0);
remaining -= leftLength;
if (remaining <= node.piece.length) {
const buffer = this.#bufferFor(node.piece.source);
const lineOffsetEnd = Math.min(upperBound(buffer.lineOffsets, node.piece.offset + remaining), node.piece.lineOffsetEnd);
line += lineOffsetEnd - node.piece.lineOffsetStart;
if (remaining === node.piece.length && node.piece.hasVirtualTrailingCRBreak) line++;
if (remaining === node.piece.length && formsCRLF(node.piece.lastCharCode, this.#nextNode(node)?.piece.firstCharCode)) line--;
return line;
}
line += node.piece.lineBreakCount - (formsCRLF(node.piece.lastCharCode, node.right?.subtreeFirstCharCode) ? 1 : 0);
remaining -= node.piece.length;
node = node.right;
}
return this.#lineCount - 1;
}
#lineBreakOffset(lineBreakIndex) {
let node = this.#root;
let remaining = lineBreakIndex;
let documentOffset = 0;
let followingCharCode;
while (node !== null) {
const leftLineBreakCount = (node.left?.subtreeLineBreakCount ?? 0) - (formsCRLF(node.left?.subtreeLastCharCode, node.piece.firstCharCode) ? 1 : 0);
if (remaining < leftLineBreakCount) {
followingCharCode = node.piece.firstCharCode;
node = node.left;
continue;
}
const leftLength = node.left?.subtreeLength ?? 0;
documentOffset += leftLength;
remaining -= leftLineBreakCount;
const nextCharCode = node.right?.subtreeFirstCharCode ?? followingCharCode;
const pieceLineBreakCount = node.piece.lineBreakCount - (formsCRLF(node.piece.lastCharCode, nextCharCode) ? 1 : 0);
if (remaining < pieceLineBreakCount) return documentOffset + this.#pieceLineBreakOffset(node.piece, remaining);
documentOffset += node.piece.length;
remaining -= pieceLineBreakCount;
node = node.right;
}
return this.#length;
}
#pieceLineBreakOffset(piece, lineBreakIndex) {
if (lineBreakIndex < piece.lineOffsetEnd - piece.lineOffsetStart) return this.#bufferFor(piece.source).lineOffsets[piece.lineOffsetStart + lineBreakIndex] - piece.offset;
return piece.length;
}
#textFromPieces() {
const chunks = [];
this.#forEachPieceSegment((segment) => {
chunks.push(segment.text.slice(segment.start, segment.end));
});
return chunks.join("");
}
#forEachPieceSegment(callback) {
this.#walk(this.#root, (node) => {
const buffer = this.#bufferFor(node.piece.source);
return callback({
text: buffer.text,
lineOffsets: buffer.lineOffsets,
lineOffsetStart: node.piece.lineOffsetStart,
lineOffsetEnd: node.piece.lineOffsetEnd,
start: node.piece.offset,
end: node.piece.offset + node.piece.length
});
});
}
#bufferFor(source) {
return source === Piece.Original ? this.#original : this.#add;
}
#createPiece(source, offset, length) {
const buffer = this.#bufferFor(source);
const end = offset + length;
const lineOffsetStart = upperBound(buffer.lineOffsets, offset);
const lineOffsetEnd = upperBound(buffer.lineOffsets, end);
const lastCharCode = buffer.text.charCodeAt(end - 1);
return new Piece(source, offset, length, lineOffsetStart, lineOffsetEnd, lastCharCode === CARRIAGE_RETURN && buffer.lineOffsets[lineOffsetEnd - 1] !== end, buffer.text.charCodeAt(offset), lastCharCode);
}
#replaceRangeIncremental(start, end, text) {
if (start === end && text.length === 0) return;
const [left, rest] = this.#split(this.#root, start);
const right = this.#dropPrefix(rest, end - start);
let root;
if (text.length > 0) {
const insertedPiece = this.#createPiece(Piece.Added, this.#add.append(text), text.length);
root = this.#mergeNodes(this.#appendCoalescing(left, insertedPiece), right);
} else {
const [seamPiece, restRight] = this.#popLeftmost(right);
root = seamPiece === void 0 ? left : this.#mergeNodes(this.#appendCoalescing(left, seamPiece), restRight);
}
if (root !== null) root.parent = null;
this.#root = root;
this.#length = root?.subtreeLength ?? 0;
this.#lineCount = (root?.subtreeLineBreakCount ?? 0) + 1;
}
#invalidateCaches() {
this.#lastVisitedLine = null;
this.#lastVisitedLineLength = null;
this.#lastPosition = null;
}
#nextPriority() {
this.#priorityState = Math.imul(this.#priorityState, 1664525) + 1013904223 >>> 0;
return this.#priorityState;
}
#createNode(piece) {
const node = new PieceNode(piece);
node.priority = this.#nextPriority();
return node;
}
#setLeft(node, child) {
node.left = child;
if (child !== null) child.parent = node;
}
#setRight(node, child) {
node.right = child;
if (child !== null) child.parent = node;
}
#split(node, offset) {
if (node === null) return [null, null];
if (offset <= 0) return [null, node];
if (offset >= node.subtreeLength) return [node, null];
const leftLength = node.left?.subtreeLength ?? 0;
if (offset <= leftLength) {
const [left, right] = this.#split(node.left, offset);
this.#setLeft(node, right);
node.updateSubtreeLength();
return [left, node];
}
const pieceLength = node.piece.length;
if (offset >= leftLength + pieceLength) {
const [left, right] = this.#split(node.right, offset - leftLength - pieceLength);
this.#setRight(node, left);
node.updateSubtreeLength();
return [node, right];
}
const inPiece = offset - leftLength;
const leftNode = new PieceNode(this.#createPiece(node.piece.source, node.piece.offset, inPiece));
const rightNode = new PieceNode(this.#createPiece(node.piece.source, node.piece.offset + inPiece, pieceLength - inPiece));
leftNode.priority = node.priority;
rightNode.priority = node.priority;
this.#setLeft(leftNode, node.left);
this.#setRight(rightNode, node.right);
leftNode.updateSubtreeLength();
rightNode.updateSubtreeLength();
return [leftNode, rightNode];
}
#dropPrefix(node, offset) {
if (node === null || offset >= node.subtreeLength) return null;
if (offset <= 0) return node;
const leftLength = node.left?.subtreeLength ?? 0;
if (offset <= leftLength) {
this.#setLeft(node, this.#dropPrefix(node.left, offset));
node.updateSubtreeLength();
return node;
}
const pieceEnd = leftLength + node.piece.length;
if (offset >= pieceEnd) return this.#dropPrefix(node.right, offset - pieceEnd);
const inPiece = offset - leftLength;
const rightNode = new PieceNode(this.#createPiece(node.piece.source, node.piece.offset + inPiece, node.piece.length - inPiece));
rightNode.priority = node.priority;
this.#setRight(rightNode, node.right);
rightNode.updateSubtreeLength();
return rightNode;
}
#mergeNodes(left, right) {
if (left === null) return right;
if (right === null) return left;
if (left.priority >= right.priority) {
this.#setRight(left, this.#mergeNodes(left.right, right));
left.updateSubtreeLength();
return left;
}
this.#setLeft(right, this.#mergeNodes(left, right.left));
right.updateSubtreeLength();
return right;
}
#appendCoalescing(tree, piece) {
if (tree === null) return this.#createNode(piece);
let last = tree;
while (last.right !== null) last = last.right;
if (canCoalescePieces(last.piece, piece)) {
last.piece = coalesceTwoPieces(last.piece, piece);
for (let node = last; node !== null;) {
node.updateSubtreeLength();
if (node === tree) break;
node = node.parent;
}
return tree;
}
return this.#mergeNodes(tree, this.#createNode(piece));
}
#popLeftmost(tree) {
if (tree === null) return [void 0, null];
if (tree.left === null) return [tree.piece, tree.right];
const [piece, newLeft] = this.#popLeftmost(tree.left);
this.#setLeft(tree, newLeft);
tree.updateSubtreeLength();
return [piece, tree];
}
#walk(node, visit) {
if (node === null) return true;
if (!this.#walk(node.left, visit)) return false;
if (visit(node) === false) return false;
return this.#walk(node.right, visit);
}
};
function isEOL(charCode) {
return charCode === 10 || charCode === 13;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function createPrefixTable(text) {
const table = Array.from({ length: text.length }).fill(0);
let matched = 0;
for (let i = 1; i < text.length; i++) {
const charCode = text.charCodeAt(i);
while (matched > 0 && charCode !== text.charCodeAt(matched)) matched = table[matched - 1];
if (charCode === text.charCodeAt(matched)) matched++;
table[i] = matched;
}
return table;
}
function normalizeRanges(ranges, length) {
const normalized = [];
for (const [rawStart, rawEnd] of ranges) {
const start = clamp(rawStart, 0, length);
const end = clamp(rawEnd, start, length);
if (start < end) normalized.push([start, end]);
}
normalized.sort((a, b) => a[0] - b[0]);
const merged = [];
for (const range of normalized) {
const previous = merged[merged.length - 1];
if (previous !== void 0 && range[0] <= previous[1]) {
previous[1] = Math.max(previous[1], range[1]);
continue;
}
merged.push(range);
}
return merged;
}
function rangeOverlaps(ranges, start, end) {
let low = 0;
let high = ranges.length;
while (low < high) {
const mid = low + Math.floor((high - low) / 2);
if (ranges[mid][1] <= start) low = mid + 1;
else high = mid;
}
const range = ranges[low];
return range !== void 0 && range[0] < end;
}
function canCoalescePieces(prev, next) {
return prev.source === next.source && prev.offset + prev.length === next.offset && !formsCRLF(prev.lastCharCode, next.firstCharCode);
}
function coalesceTwoPieces(prev, next) {
return new Piece(prev.source, prev.offset, prev.length + next.length, prev.lineOffsetStart, next.lineOffsetEnd, next.hasVirtualTrailingCRBreak, prev.firstCharCode, next.lastCharCode);
}
function formsCRLF(leftCharCode, rightCharCode) {
return leftCharCode === CARRIAGE_RETURN && rightCharCode === LINE_FEED;
}
function upperBound(values, target) {
let lo = 0;
let hi = values.length;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (values[mid] <= target) lo = mid + 1;
else hi = mid;
}
return lo;
}
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function isWordSeparatorCharCode(charCode) {
if (charCode <= 32 || charCode === 127) return true;
const ch = String.fromCharCode(charCode);
return WORD_SEPARATORS.includes(ch);
}
function isWholeWordAtDocOffsets(text, docStart, length) {
const beforeOk = docStart <= 0 || isWordSeparatorCharCode(text.charCodeAt(docStart - 1));
const afterOk = docStart + length >= text.length || isWordSeparatorCharCode(text.charCodeAt(docStart + length));
return beforeOk && afterOk;
}
function compileSearchRegExp(source, isRegex, caseSensitive) {
const body = isRegex ? source : escapeRegExp(source);
return new RegExp(body, `g${caseSensitive ? "" : "i"}${isRegex ? "m" : ""}`);
}
/** Expands `$&`, `$1`, `$$`, etc. in a regex replace string using a match. */
function expandReplaceString(replacement, match) {
return replacement.replace(/\$([$&]|\d+)/g, (_token, group) => {
if (group === "$") return "$";
if (group === "&") return match[0] ?? "";
return match[Number(group)] ?? "";
});
}
/**
* Builds the text to insert for one search match, including regex capture
* substitution when regex mode is enabled.
*/
function buildSearchReplacementText(positionAt, offsetAt, getLineText, searchParams, matchStart, matchEnd) {
if (!searchParams.regex) return searchParams.replaceText;
const position = positionAt(matchStart);
const lineText = getLineText(position.line);
const relStart = matchStart - offsetAt({
line: position.line,
character: 0
});
let pattern;
try {
pattern = compileSearchRegExp(searchParams.text, true, searchParams.caseSensitive);
} catch {
return searchParams.replaceText;
}
pattern.lastIndex = relStart;
const match = pattern.exec(lineText);
if (match === null || match.index !== relStart || match[0].length !== matchEnd - matchStart) return searchParams.replaceText;
return expandReplaceString(searchParams.replaceText, match);
}
function advancePastEmptyMatch(text, index) {
if (index + 1 < text.length) {
const first = text.charCodeAt(index);
const second = text.charCodeAt(index + 1);
if (first >= 55296 && first <= 56319 && second >= 56320 && second <= 57343) return index + 2;
}
return index + 1;
}
//#endregion
export { PieceTable, buildSearchReplacementText };
//# sourceMappingURL=pieceTable.js.map