textPattern
A pattern as tcm's text searches take it (tcm search, tcm view --grep, the agent tools', code search in an
editor): the text as written (a|b is that text), or a regular expression written /…/, flags after it
(/get|set/i). regex takes all the text as a regular expression, without slashes (an editor's toggle, and
tcm search --regex, the old way). caseSensitive is for text and a regular expression without slashes: one
written /…/ matches case as its i flag says. wholeWord keeps to matches that are whole words. find gives a
line's matches, as [start, end); a regular expression that isn't one is an error.
import { writtenRegex } from "#codebase/tcmts/cli/pattern"; export function textPattern( text: string, opts: { caseSensitive?: boolean; wholeWord?: boolean; regex?: boolean } = {}, ): | { find: (line: string) => [number, number][]; regex: boolean; error?: undefined } | { error: string } { const written = opts.regex ? undefined : writtenRegex(text); const source = written ? written.source : opts.regex ? text : text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // (g, to find each match in a line; a written g or y would only carry on from where the last line's ended) const flags = written ? `${written.flags.replace(/[gy]/g, "")}g` : opts.caseSensitive ? "g" : "gi"; let re: RegExp; try { re = new RegExp(opts.wholeWord ? `\\b(?:${source})\\b` : source, flags); } catch (e) { return { error: (e as Error).message }; } const find = (line: string) => { const ranges: [number, number][] = []; re.lastIndex = 0; for (let m = re.exec(line); m; m = re.exec(line)) { if (!m[0].length) { re.lastIndex++; continue; } ranges.push([m.index, m.index + m[0].length]); } return ranges; }; return { find, regex: !!written || !!opts.regex }; }