Файли
openidea/src/editor/refactor/occurrences.ts
T
l0ng-ai d37b96002b fix: avoid lookbehind in application regexes for older WebKit
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
2026-10-29 15:01:01 +08:00

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;
}