дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Application regex lookbehind prevented older WebKit from loading the startup module graph. Replace lookbehind with boundary checks across run/terminal links, SQL aliases, debugger targets and refactoring; preserve Unicode boundaries, UTF-16 offsets and empty-match progress. Add compatibility and regression tests. Fixes #224 Refs #208
120 рядки
3.7 KiB
TypeScript
120 рядки
3.7 KiB
TypeScript
/**
|
|
* Textual candidates for "replace all occurrences": spans of `text` equal to
|
|
* an expression up to whitespace between tokens. Callers confirm each span
|
|
* with the syntax tree (same node kind, same resolution) before using it.
|
|
*/
|
|
|
|
const WORD = /[\w$]/;
|
|
|
|
/**
|
|
* Collapses whitespace between tokens (kept as one space between two word
|
|
* characters), leaving string / char literal contents untouched.
|
|
*/
|
|
export function normalizeExpression(text: string): string {
|
|
let out = "";
|
|
let pendingSpace = false;
|
|
for (let i = 0; i < text.length; i++) {
|
|
const c = text[i]!;
|
|
if (c === '"' || c === "'" || c === "`") {
|
|
if (pendingSpace && out && WORD.test(out[out.length - 1]!)) out += " ";
|
|
pendingSpace = false;
|
|
let j = i + 1;
|
|
while (j < text.length && text[j] !== c) j += text[j] === "\\" ? 2 : 1;
|
|
out += text.slice(i, j + 1);
|
|
i = j;
|
|
continue;
|
|
}
|
|
if (/\s/.test(c)) {
|
|
pendingSpace = true;
|
|
continue;
|
|
}
|
|
if (pendingSpace && out && WORD.test(out[out.length - 1]!) && WORD.test(c)) out += " ";
|
|
pendingSpace = false;
|
|
out += c;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function escape(s: string): string {
|
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
/** Regex source matching `expr` with any whitespace between its tokens. */
|
|
export function expressionPattern(expr: string): string {
|
|
const norm = normalizeExpression(expr.trim());
|
|
const tokens: string[] = [];
|
|
let i = 0;
|
|
while (i < norm.length) {
|
|
const c = norm[i]!;
|
|
if (c === " ") {
|
|
tokens.push("\\s+");
|
|
i++;
|
|
} else if (c === '"' || c === "'" || c === "`") {
|
|
let j = i + 1;
|
|
while (j < norm.length && norm[j] !== c) j += norm[j] === "\\" ? 2 : 1;
|
|
tokens.push(escape(norm.slice(i, j + 1)));
|
|
i = j + 1;
|
|
} else if (WORD.test(c)) {
|
|
let j = i;
|
|
while (j < norm.length && WORD.test(norm[j]!)) j++;
|
|
tokens.push(escape(norm.slice(i, j)));
|
|
i = j;
|
|
} else {
|
|
tokens.push(escape(c));
|
|
i++;
|
|
}
|
|
}
|
|
let src = "";
|
|
tokens.forEach((t, k) => {
|
|
if (k > 0 && t !== "\\s+" && tokens[k - 1] !== "\\s+") src += "\\s*";
|
|
src += t;
|
|
});
|
|
const tail = WORD.test(norm[norm.length - 1] ?? "") ? "(?![\\w$])" : "";
|
|
return src + tail;
|
|
}
|
|
|
|
/** Spans in `text[from, to)` matching `expr` (absolute offsets), at most `limit`. */
|
|
export function findTextOccurrences(
|
|
text: string,
|
|
from: number,
|
|
to: number,
|
|
expr: string,
|
|
limit = 100,
|
|
): { from: number; to: number }[] {
|
|
const re = new RegExp(expressionPattern(expr), "g");
|
|
const scope = text.slice(from, to);
|
|
const want = normalizeExpression(expr.trim());
|
|
const out: { from: number; to: number }[] = [];
|
|
for (let m = re.exec(scope); m; m = re.exec(scope)) {
|
|
if (!m[0].length) re.lastIndex = m.index + 1;
|
|
if (WORD.test(want[0] ?? "") && m.index > 0 && WORD.test(scope[m.index - 1]!)) {
|
|
re.lastIndex = m.index + 1;
|
|
continue;
|
|
}
|
|
if (normalizeExpression(m[0]) !== want) continue;
|
|
out.push({ from: from + m.index!, to: from + m.index! + m[0].length });
|
|
if (out.length >= limit) break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Identifier-like words of an expression outside literals, with offsets relative to it. */
|
|
export function expressionWords(expr: string): { word: string; offset: number }[] {
|
|
const out: { word: string; offset: number }[] = [];
|
|
let i = 0;
|
|
while (i < expr.length) {
|
|
const c = expr[i]!;
|
|
if (c === '"' || c === "'" || c === "`") {
|
|
let j = i + 1;
|
|
while (j < expr.length && expr[j] !== c) j += expr[j] === "\\" ? 2 : 1;
|
|
i = j + 1;
|
|
} else if (/[A-Za-z_$]/.test(c) && (i === 0 || !WORD.test(expr[i - 1]!))) {
|
|
let j = i;
|
|
while (j < expr.length && WORD.test(expr[j]!)) j++;
|
|
out.push({ word: expr.slice(i, j), offset: i });
|
|
i = j;
|
|
} else i++;
|
|
}
|
|
return out;
|
|
}
|