63 lines · 2.5 KB
import { all, first } from "../db";
import { visibleReposClause } from "../repos";
import type { CodeSearchHit } from "~/lib/code-intel";
import type { Viewer } from "~/lib/types";

const MIN_TERM_LENGTH = 3;
const MAX_TERMS = 8;
const MAX_TERM_LENGTH = 100;
const RESULT_LIMIT = 30;
const SNIPPET_TOKENS = 24;
const PATH_PREFIX = "path:";

const SNIPPET = `snippet(code_fts, 1, char(1), char(2), '…', ${SNIPPET_TOKENS})`;

function quote(term: string): string {
  return `"${term.replaceAll('"', '""')}"`;
}

/** Turns free text into a safe FTS5 expression: every term is quoted (trigram needs 3+ chars); `path:x` filters by path. */
export function ftsQuery(input: string): string | null {
  const terms: string[] = [];
  for (const raw of input.trim().split(/\s+/)) {
    const isPath = raw.startsWith(PATH_PREFIX);
    const term = (isPath ? raw.slice(PATH_PREFIX.length) : raw).slice(0, MAX_TERM_LENGTH);
    if (term.length < MIN_TERM_LENGTH) continue;
    terms.push(isPath ? `path : ${quote(term)}` : quote(term));
    if (terms.length >= MAX_TERMS) break;
  }
  return terms.length > 0 ? terms.join(" ") : null;
}

const FROM = `FROM code_fts JOIN code_files f ON f.id = code_fts.rowid
  JOIN repos r ON r.id = f.repo_id JOIN accounts a ON a.id = r.owner_id`;

export async function searchRepoCode(repoId: string, query: string): Promise<CodeSearchHit[]> {
  return all<CodeSearchHit>(
    `SELECT a.name AS owner, r.name AS repo, r.default_branch AS ref, f.path, ${SNIPPET} AS snippet ${FROM}
     WHERE code_fts MATCH ?1 AND f.repo_id = ?2 ORDER BY rank LIMIT ${RESULT_LIMIT}`,
    query,
    repoId,