109 lines · 3.8 KB
/** 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;
      }