export type CandidateKind = "session" | "command" | "directory"; export interface SessionCandidate { kind: "session"; fields: { name: string; directory?: string; command?: string; tool?: string; }; } export interface CommandCandidate { kind: "command"; fields: { label: string }; } export interface DirectoryCandidate { kind: "directory"; fields: { path: string }; } export type MatchCandidate = SessionCandidate | CommandCandidate | DirectoryCandidate; export type MatchFieldName = "name" | "directory" | "command" | "tool" | "label" | "path"; export type MatchRange = readonly [start: number, end: number]; export interface MatchResult { candidate: C; score: number; field: MatchFieldName | undefined; ranges: MatchRange[]; } interface Field { name: MatchFieldName; text: string; weight: number; } interface Embedding { score: number; positions: number[]; } interface RankedResult extends MatchResult { matchedText: string; inputIndex: number; } const KIND_ORDER: Record = { session: 0, command: 1, directory: 2, }; function fields(candidate: MatchCandidate): Field[] { switch (candidate.kind) { case "session": { const values: Array<[MatchFieldName, string | undefined, number]> = [ ["name", candidate.fields.name, 0], ["directory", candidate.fields.directory, -24], ["command", candidate.fields.command, -32], ["tool", candidate.fields.tool, -40], ]; return values.flatMap(([name, text, weight]) => text === undefined ? [] : [{ name, text, weight }], ); } case "command": return [{ name: "label", text: candidate.fields.label, weight: 0 }]; case "directory": return [{ name: "path", text: candidate.fields.path, weight: 0 }]; } } function normalizeQuery(query: string): string { return query.toLowerCase().replace(/\s/g, ""); } function boundaryBonus(text: string, position: number): number { if (position === 0) return 18; const previous = text[position - 1]; if ("/\\-_ .:".includes(previous)) return 16; const current = text[position]; const isUppercase = current !== current.toLowerCase() && current === current.toUpperCase(); const previousIsLowercase = previous !== previous.toUpperCase() && previous === previous.toLowerCase(); if (isUppercase && previousIsLowercase) return 12; const isDigit = current >= "0" && current <= "9"; const previousIsDigit = previous >= "0" && previous <= "9"; if (isDigit && !previousIsDigit) return 12; return 0; } function contribution(text: string, position: number, previousPosition: number): number { const gap = position - previousPosition - 1; return 16 + boundaryBonus(text, position) + (gap === 0 ? 8 : -Math.min(gap, 12)); } function isBetterEmbedding(candidate: Embedding, incumbent: Embedding | undefined): boolean { if (!incumbent || candidate.score !== incumbent.score) { return !incumbent || candidate.score > incumbent.score; } for (let index = 0; index < candidate.positions.length; index++) { if (candidate.positions[index] !== incumbent.positions[index]) { return candidate.positions[index] < incumbent.positions[index]; } } return false; } function bestEmbedding(query: string, text: string): Embedding | undefined { const lowerText = text.toLowerCase(); let previousStates = new Map(); for (let queryIndex = 0; queryIndex < query.length; queryIndex++) { const states = new Map(); for (let position = 0; position < lowerText.length; position++) { if (lowerText[position] !== query[queryIndex]) continue; if (queryIndex === 0) { states.set(position, { score: contribution(text, position, -1), positions: [position], }); continue; } for (const [previousPosition, previous] of previousStates) { if (previousPosition >= position) continue; const candidate = { score: previous.score + contribution(text, position, previousPosition), positions: [...previous.positions, position], }; if (isBetterEmbedding(candidate, states.get(position))) states.set(position, candidate); } } if (states.size === 0) return undefined; previousStates = states; } let best: Embedding | undefined; for (const embedding of previousStates.values()) { if (isBetterEmbedding(embedding, best)) best = embedding; } return best; } function toRanges(positions: number[]): MatchRange[] { const ranges: MatchRange[] = []; for (const position of positions) { const previous = ranges[ranges.length - 1]; if (previous && previous[1] === position) { ranges[ranges.length - 1] = [previous[0], position + 1]; } else { ranges.push([position, position + 1]); } } return ranges; } function compareByCodePoint(left: string, right: string): number { const leftPoints = Array.from(left, (character) => character.codePointAt(0)!); const rightPoints = Array.from(right, (character) => character.codePointAt(0)!); const length = Math.min(leftPoints.length, rightPoints.length); for (let index = 0; index < length; index++) { if (leftPoints[index] !== rightPoints[index]) return leftPoints[index] - rightPoints[index]; } return leftPoints.length - rightPoints.length; } export function matchAndRank( query: string, candidates: readonly C[], ): MatchResult[] { const normalizedQuery = normalizeQuery(query); if (normalizedQuery === "") { return candidates.map((candidate) => ({ candidate, score: 0, field: undefined, ranges: [] })); } const results: RankedResult[] = []; candidates.forEach((candidate, inputIndex) => { let best: | { field: Field; score: number; positions: number[] } | undefined; for (const field of fields(candidate)) { const embedding = bestEmbedding(normalizedQuery, field.text); if (!embedding) continue; const compactText = field.text.toLowerCase().replace(/\s/g, ""); const adjustment = compactText === normalizedQuery ? 64 : compactText.startsWith(normalizedQuery) ? 32 : 0; const score = embedding.score + adjustment + field.weight; if (!best || score > best.score) best = { field, score, positions: embedding.positions }; } if (best) { results.push({ candidate, score: best.score, field: best.field.name, ranges: toRanges(best.positions), matchedText: best.field.text, inputIndex, }); } }); results.sort( (left, right) => right.score - left.score || KIND_ORDER[left.candidate.kind] - KIND_ORDER[right.candidate.kind] || left.matchedText.length - right.matchedText.length || compareByCodePoint(left.matchedText, right.matchedText) || left.inputIndex - right.inputIndex, ); return results.map(({ candidate, score, field, ranges }) => ({ candidate, score, field, ranges })); }