/** Workspace text search shared by the editor API (head tree) and the editor extension (live room files). */
export type TextQuery = {
pattern: string;
isRegExp?: boolean;
isCaseSensitive?: boolean;
isWordMatch?: boolean;
};
/** One matching line: `ranges` are [start, end) columns of each match within `text`. */
export type LineMatch = { line: number; text: string; ranges: Array<[number, number]> };
export const MAX_PATTERN_LENGTH = 1000;
/** Long lines (minified bundles) are cut for the preview; matches past the cut are dropped. */
const MAX_PREVIEW_CHARS = 1000;
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/** The query as a global RegExp; throws SyntaxError for an invalid regular expression. */
export function compileQuery(query: TextQuery): RegExp {
let source = query.isRegExp ? query.pattern : escapeRegExp(query.pattern);
if (query.isWordMatch) source = `\\b(?:${source})\\b`;
return new RegExp(source, query.isCaseSensitive ? "g" : "gi");
}
/** Matching lines of `content`, at most `limit` of them. */
export function matchLines(content: string, regex: RegExp, limit: number): LineMatch[] {
const out: LineMatch[] = [];
const lines = content.split(/\r?\n/);
for (let index = 0; index < lines.length && out.length < limit; index++) {
const line = lines[index];
const text = line.length > MAX_PREVIEW_CHARS ? line.slice(0, MAX_PREVIEW_CHARS) : line;
const ranges: Array<[number, number]> = [];
regex.lastIndex = 0;
for (let match = regex.exec(text); match; match = regex.exec(text)) {
if (match[0].length === 0) {
regex.lastIndex++;
continue;
}