import type { ProjectEntry } from "@t3tools/contracts"; import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; export const PROJECT_FILE_PICKER_RESULT_LIMIT = 200; export interface ProjectFilePickerMatch { readonly name: string; readonly nameMatchIndices: ReadonlyArray; readonly path: string; readonly pathMatchIndices: ReadonlyArray; } function fileName(path: string): string { return path.slice(path.lastIndexOf("/") + 1); } /** * First ordered subsequence of `query` inside `value`, as highlight indices. * Returns null when `value` does not contain the subsequence at all — the * entry still renders (the server matched it), just without highlights. */ function findMatchIndices(value: string, query: string): number[] | null { if (!query) return []; const normalizedValue = value.toLowerCase(); const indices: number[] = []; let queryIndex = 0; for (let valueIndex = 0; valueIndex < normalizedValue.length; valueIndex += 1) { if (normalizedValue[valueIndex] !== query[queryIndex]) continue; indices.push(valueIndex); queryIndex += 1; if (queryIndex === query.length) return indices; } return null; } /** * Maps server search results to picker rows. Server ordering is preserved — * ranking already happened there; this pass only filters to files and * computes highlight positions with the same query normalization the server * applied. */ export function getProjectFilePickerMatches( entries: ReadonlyArray, rawQuery: string, limit = PROJECT_FILE_PICKER_RESULT_LIMIT, ): ProjectFilePickerMatch[] { if (limit <= 0) return []; const query = normalizeSearchQuery(rawQuery, { trimLeadingPattern: /^[@./]+/, }).replaceAll(/\s/g, ""); const matches: ProjectFilePickerMatch[] = []; for (const entry of entries) { if (entry.kind !== "file") continue; const name = fileName(entry.path); const nameMatchIndices = findMatchIndices(name, query); const pathMatchIndices = findMatchIndices(entry.path, query); matches.push({ name, nameMatchIndices: nameMatchIndices ?? [], path: entry.path, pathMatchIndices: pathMatchIndices ?? [], }); if (matches.length >= limit) break; } return matches; }