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<number>; readonly path: string; readonly pathMatchIndices: ReadonlyArray<number>;} 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<ProjectEntry>, 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;}
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<number>;
readonly path: string;
readonly pathMatchIndices: ReadonlyArray<number>;
}
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<ProjectEntry>,
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;
}