1import type { ProjectEntry } from "@t3tools/contracts";
2import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";
3
4export const PROJECT_FILE_PICKER_RESULT_LIMIT = 200;
5
6export interface ProjectFilePickerMatch {
7 readonly name: string;
8 readonly nameMatchIndices: ReadonlyArray<number>;
9 readonly path: string;
10 readonly pathMatchIndices: ReadonlyArray<number>;
11}
12
13function fileName(path: string): string {
14 return path.slice(path.lastIndexOf("/") + 1);
15}
16
17/**
18 * First ordered subsequence of `query` inside `value`, as highlight indices.
19 * Returns null when `value` does not contain the subsequence at all — the
20 * entry still renders (the server matched it), just without highlights.
21 */
22function findMatchIndices(value: string, query: string): number[] | null {
23 if (!query) return [];
24
25 const normalizedValue = value.toLowerCase();
26 const indices: number[] = [];
27 let queryIndex = 0;
28
29 for (let valueIndex = 0; valueIndex < normalizedValue.length; valueIndex += 1) {
30 if (normalizedValue[valueIndex] !== query[queryIndex]) continue;
31 indices.push(valueIndex);
32 queryIndex += 1;
33 if (queryIndex === query.length) return indices;
34 }
35
36 return null;
37}
38
39/**
40 * Maps server search results to picker rows. Server ordering is preserved —
41 * ranking already happened there; this pass only filters to files and
42 * computes highlight positions with the same query normalization the server
43 * applied.
44 */
45export function getProjectFilePickerMatches(
46 entries: ReadonlyArray<ProjectEntry>,
47 rawQuery: string,
48 limit = PROJECT_FILE_PICKER_RESULT_LIMIT,
49): ProjectFilePickerMatch[] {
50 if (limit <= 0) return [];
51
52 const query = normalizeSearchQuery(rawQuery, {
53 trimLeadingPattern: /^[@./]+/,
54 }).replaceAll(/\s/g, "");
55 const matches: ProjectFilePickerMatch[] = [];
56
57 for (const entry of entries) {
58 if (entry.kind !== "file") continue;
59
60 const name = fileName(entry.path);
61 const nameMatchIndices = findMatchIndices(name, query);
62 const pathMatchIndices = findMatchIndices(entry.path, query);
63 matches.push({
64 name,
65 nameMatchIndices: nameMatchIndices ?? [],
66 path: entry.path,
67 pathMatchIndices: pathMatchIndices ?? [],
68 });
69 if (matches.length >= limit) break;
70 }
71
72 return matches;
73}