fix(search): match complete paths in file and project search
Treat slash-separated fragments as searchable text and use the same full-path fuzzy matcher for daemon results and remembered project paths.
This commit is contained in:
@@ -32,17 +32,30 @@ describe("buildWorkingDirectorySuggestions", () => {
|
|||||||
expect(results).toEqual(["/Users/me/projects/paseo-desktop"]);
|
expect(results).toEqual(["/Users/me/projects/paseo-desktop"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("leaves path-query semantics to the daemon", () => {
|
it("matches recommended paths using the complete path text", () => {
|
||||||
const results = buildWorkingDirectorySuggestions({
|
const results = buildWorkingDirectorySuggestions({
|
||||||
recommendedPaths: [
|
recommendedPaths: [
|
||||||
"/Users/me/archive/projects/paseo-desktop",
|
"/Users/me/archive/projects/paseo-desktop",
|
||||||
"/Users/me/projects/paseo-desktop",
|
"/Users/me/projects/paseo-desktop",
|
||||||
],
|
],
|
||||||
serverPaths: [],
|
serverPaths: [],
|
||||||
query: "~/projects/pso",
|
query: "projects/pso",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(results).toEqual([]);
|
expect(results).toEqual([
|
||||||
|
"/Users/me/archive/projects/paseo-desktop",
|
||||||
|
"/Users/me/projects/paseo-desktop",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fuzzy-matches recommended paths using their full path", () => {
|
||||||
|
const results = buildWorkingDirectorySuggestions({
|
||||||
|
recommendedPaths: ["/Users/me/projects/blankpage/editor"],
|
||||||
|
serverPaths: [],
|
||||||
|
query: "blank page editor",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toEqual(["/Users/me/projects/blankpage/editor"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats '~' as an active query and includes daemon suggestions", () => {
|
it("treats '~' as an active query and includes daemon suggestions", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { scoreMatch } from "@getpaseo/protocol/search/text-match";
|
import { scorePathMatch } from "@getpaseo/protocol/search/text-match";
|
||||||
|
|
||||||
export interface BuildWorkingDirectorySuggestionsInput {
|
export interface BuildWorkingDirectorySuggestionsInput {
|
||||||
recommendedPaths: string[];
|
recommendedPaths: string[];
|
||||||
@@ -19,9 +19,8 @@ export function buildWorkingDirectorySuggestions(
|
|||||||
recommendedPathMatchesQuery(path, query),
|
recommendedPathMatchesQuery(path, query),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The request owner correlates these results with the current query. The
|
// Server paths are already ranked by the daemon. Recommended paths use the
|
||||||
// daemon owns filesystem query parsing, filtering, and ranking; doing it
|
// same shared matcher, then keep their existing recommendation order.
|
||||||
// again here creates a second search implementation that can disagree.
|
|
||||||
return uniquePaths([...matchingRecommended, ...input.serverPaths]);
|
return uniquePaths([...matchingRecommended, ...input.serverPaths]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +44,8 @@ function recommendedPathMatchesQuery(path: string, query: string): boolean {
|
|||||||
if (["~", "~/"].includes(normalizedQuery)) {
|
if (["~", "~/"].includes(normalizedQuery)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (normalizedQuery.includes("/") || normalizedQuery.startsWith("~")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const basename = candidate.split("/").at(-1) ?? "";
|
return scorePathMatch(normalizedQuery, candidate) !== null;
|
||||||
return candidate.includes(normalizedQuery) || scoreMatch(normalizedQuery, basename) !== null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePath(value: string): string {
|
function normalizePath(value: string): string {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
fuzzyPolicyForToken,
|
fuzzyPolicyForToken,
|
||||||
matchRanges,
|
matchRanges,
|
||||||
scoreMatch,
|
scoreMatch,
|
||||||
|
scorePathMatch,
|
||||||
scoreTextFields,
|
scoreTextFields,
|
||||||
} from "./text-match.js";
|
} from "./text-match.js";
|
||||||
|
|
||||||
@@ -171,6 +172,28 @@ describe("scoreMatch typo tolerance", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("scorePathMatch", () => {
|
||||||
|
it("matches a literal fragment anywhere in the complete path", () => {
|
||||||
|
expect(
|
||||||
|
scorePathMatch("skills/", "something/something-else/skills/paseo-advisor/SKILL.md"),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fuzzy-matches spaced text against a compact path", () => {
|
||||||
|
expect(scorePathMatch("blank page editor", "blankpage/editor")).toEqual({
|
||||||
|
tier: 0,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the original path offset for compact matches", () => {
|
||||||
|
expect(scorePathMatch("blank page editor", "projects/blankpage/editor")).toEqual({
|
||||||
|
tier: 4,
|
||||||
|
offset: 9,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("scoreTextFields", () => {
|
describe("scoreTextFields", () => {
|
||||||
it("requires every token to match some field", () => {
|
it("requires every token to match some field", () => {
|
||||||
expect(scoreTextFields("stripe billing", ["add stripe billing system"])).not.toBeNull();
|
expect(scoreTextFields("stripe billing", ["add stripe billing system"])).not.toBeNull();
|
||||||
|
|||||||
@@ -201,6 +201,39 @@ export function scoreMatch(
|
|||||||
return fuzzy ? scoreFuzzyMatch(q, t, fuzzy) : null;
|
return fuzzy ? scoreFuzzyMatch(q, t, fuzzy) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CompactText {
|
||||||
|
value: string;
|
||||||
|
offsets: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactText(value: string): CompactText {
|
||||||
|
let compact = "";
|
||||||
|
const offsets: number[] = [];
|
||||||
|
for (let index = 0; index < value.length; index += 1) {
|
||||||
|
if (!/[a-z0-9]/i.test(value[index])) continue;
|
||||||
|
compact += value[index].toLowerCase();
|
||||||
|
offsets.push(index);
|
||||||
|
}
|
||||||
|
return { value: compact, offsets };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Match a query against a complete displayed path, including its separators. */
|
||||||
|
export function scorePathMatch(query: string, path: string): MatchScore | null {
|
||||||
|
const direct = scoreMatch(query, path);
|
||||||
|
if (direct) return direct;
|
||||||
|
|
||||||
|
const compactQuery = compactText(query);
|
||||||
|
const compactPath = compactText(path);
|
||||||
|
if (!compactQuery.value || !compactPath.value) return null;
|
||||||
|
|
||||||
|
const compactScore = scoreMatch(compactQuery.value, compactPath.value);
|
||||||
|
if (!compactScore) return null;
|
||||||
|
return {
|
||||||
|
...compactScore,
|
||||||
|
offset: compactPath.offsets[compactScore.offset] ?? compactScore.offset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface MatchRange {
|
export interface MatchRange {
|
||||||
start: number;
|
start: number;
|
||||||
length: number;
|
length: number;
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ describe("searchDirectoryEntries", () => {
|
|||||||
expect(suffixRootBrowses).toEqual([rootEntries, rootEntries]);
|
expect(suffixRootBrowses).toEqual([rootEntries, rootEntries]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("anchors rooted one-segment queries to their root parent", async () => {
|
it("matches rooted queries against the full path at any depth", async () => {
|
||||||
mkdirSync(path.join(searchRoot, "nested", "pso-global"), { recursive: true });
|
mkdirSync(path.join(searchRoot, "nested", "pso-global"), { recursive: true });
|
||||||
mkdirSync(path.join(searchRoot, "pso-root"), { recursive: true });
|
mkdirSync(path.join(searchRoot, "pso-root"), { recursive: true });
|
||||||
const absoluteQuery = path.join(configuredSearchRoot, "pso");
|
const absoluteQuery = path.join(configuredSearchRoot, "pso");
|
||||||
@@ -236,7 +236,11 @@ describe("searchDirectoryEntries", () => {
|
|||||||
pathQueryPolicy: "rooted" as const,
|
pathQueryPolicy: "rooted" as const,
|
||||||
rootAliases: ["~"],
|
rootAliases: ["~"],
|
||||||
};
|
};
|
||||||
const expected = [{ path: "pso-root", kind: "directory" }];
|
const expected = [
|
||||||
|
{ path: "pso-root", kind: "directory" },
|
||||||
|
{ path: "nested/pso-global", kind: "directory" },
|
||||||
|
{ path: "projects/paseo-desktop", kind: "directory" },
|
||||||
|
];
|
||||||
|
|
||||||
await expect(searchDirectoryEntries({ ...common, query: "~/pso" })).resolves.toEqual(expected);
|
await expect(searchDirectoryEntries({ ...common, query: "~/pso" })).resolves.toEqual(expected);
|
||||||
await expect(searchDirectoryEntries({ ...common, query: "./pso" })).resolves.toEqual(expected);
|
await expect(searchDirectoryEntries({ ...common, query: "./pso" })).resolves.toEqual(expected);
|
||||||
@@ -264,7 +268,10 @@ describe("searchDirectoryEntries", () => {
|
|||||||
{ path: "projects", kind: "directory" },
|
{ path: "projects", kind: "directory" },
|
||||||
{ path: "src", kind: "directory" },
|
{ path: "src", kind: "directory" },
|
||||||
],
|
],
|
||||||
projectEntries: [{ path: "projects/paseo-desktop", kind: "directory" }],
|
projectEntries: [
|
||||||
|
{ path: "projects", kind: "directory" },
|
||||||
|
{ path: "projects/paseo-desktop", kind: "directory" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -900,4 +907,46 @@ describe("relative typed-entry configuration", () => {
|
|||||||
|
|
||||||
expect(results).toEqual([{ path: "src/components", kind: "directory" }]);
|
expect(results).toEqual([{ path: "src/components", kind: "directory" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("matches a path fragment anywhere in the full file path", async () => {
|
||||||
|
const targetPath = path.join(
|
||||||
|
workspaceDir,
|
||||||
|
"something",
|
||||||
|
"something-else",
|
||||||
|
"skills",
|
||||||
|
"paseo-advisor",
|
||||||
|
"SKILL.md",
|
||||||
|
);
|
||||||
|
mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||||
|
writeFileSync(targetPath, "skill\n");
|
||||||
|
|
||||||
|
const results = await searchRelativeDirectoryEntries({
|
||||||
|
cwd: workspaceDir,
|
||||||
|
query: "skills/",
|
||||||
|
limit: 20,
|
||||||
|
includeFiles: true,
|
||||||
|
includeDirectories: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
path: "something/something-else/skills/paseo-advisor/SKILL.md",
|
||||||
|
kind: "file",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fuzzy-matches natural language against the full path", async () => {
|
||||||
|
mkdirSync(path.join(workspaceDir, "blankpage", "editor"), { recursive: true });
|
||||||
|
|
||||||
|
const results = await searchRelativeDirectoryEntries({
|
||||||
|
cwd: workspaceDir,
|
||||||
|
query: "blank page editor",
|
||||||
|
limit: 20,
|
||||||
|
includeFiles: false,
|
||||||
|
includeDirectories: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toEqual([{ path: "blankpage/editor", kind: "directory" }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Dirent, Stats } from "node:fs";
|
import type { Dirent, Stats } from "node:fs";
|
||||||
import { readdir, realpath, stat } from "node:fs/promises";
|
import { readdir, realpath, stat } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { scorePathMatch, type MatchScore } from "@getpaseo/protocol/search/text-match";
|
||||||
import { isPathInsideRoot } from "./path.js";
|
import { isPathInsideRoot } from "./path.js";
|
||||||
import { runGitCommand } from "./run-git-command.js";
|
import { runGitCommand } from "./run-git-command.js";
|
||||||
|
|
||||||
@@ -65,6 +66,13 @@ interface RankedEntry extends DirectorySuggestionEntry {
|
|||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RankFields {
|
||||||
|
matchTier: number;
|
||||||
|
segmentIndex: number;
|
||||||
|
matchOffset: number;
|
||||||
|
fuzzyScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface DirectoryListCacheEntry {
|
interface DirectoryListCacheEntry {
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
modifiedAtMs: number;
|
modifiedAtMs: number;
|
||||||
@@ -143,10 +151,9 @@ export async function searchDirectoryEntries(
|
|||||||
if (exact && input.limit === 1) return [exact];
|
if (exact && input.limit === 1) return [exact];
|
||||||
|
|
||||||
const browsesRoot = input.plan.isPathQuery && !input.plan.normalizedQuery;
|
const browsesRoot = input.plan.isPathQuery && !input.plan.normalizedQuery;
|
||||||
|
const browsesAbsoluteParent = input.plan.browseExactPath === true;
|
||||||
const ranked =
|
const ranked =
|
||||||
input.plan.isPathQuery && (input.matchMode === "fuzzy" || browsesRoot)
|
browsesRoot || browsesAbsoluteParent ? await searchChildren(input) : await searchTree(input);
|
||||||
? await searchChildren(input)
|
|
||||||
: await searchTree(input);
|
|
||||||
const results = sortAndFormat(ranked, input.root, input.pathFormat).slice(0, input.limit);
|
const results = sortAndFormat(ranked, input.root, input.pathFormat).slice(0, input.limit);
|
||||||
return exact
|
return exact
|
||||||
? [exact, ...results.filter((entry) => !sameEntry(entry, exact))].slice(0, input.limit)
|
? [exact, ...results.filter((entry) => !sameEntry(entry, exact))].slice(0, input.limit)
|
||||||
@@ -264,7 +271,9 @@ async function searchTree(input: SearchInput): Promise<RankedEntry[]> {
|
|||||||
if (shouldSuggest(entry, input)) ranked.push(rank(entry, input));
|
if (shouldSuggest(entry, input)) ranked.push(rank(entry, input));
|
||||||
if (
|
if (
|
||||||
scanned >= input.maxEntriesScanned ||
|
scanned >= input.maxEntriesScanned ||
|
||||||
(threshold && scanned >= threshold && hasConfidentResult(ranked, input.plan.searchTerm))
|
(threshold &&
|
||||||
|
scanned >= threshold &&
|
||||||
|
hasConfidentResult(ranked, input.plan.normalizedQuery || input.plan.searchTerm))
|
||||||
)
|
)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -346,45 +355,123 @@ function shouldSuggest(entry: TraversedEntry, input: SearchInput): boolean {
|
|||||||
if (!input.plan.normalizedQuery) return true;
|
if (!input.plan.normalizedQuery) return true;
|
||||||
if (input.matchMode === "suffix")
|
if (input.matchMode === "suffix")
|
||||||
return suffixMatches(entry.visiblePath, input.root, input.plan.normalizedQuery);
|
return suffixMatches(entry.visiblePath, input.root, input.plan.normalizedQuery);
|
||||||
return !input.plan.searchTerm || rank(entry, input).matchTier !== NO_MATCH_TIER;
|
return rank(entry, input).matchTier !== NO_MATCH_TIER;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rank(entry: TraversedEntry, input: SearchInput): RankedEntry {
|
function rank(entry: TraversedEntry, input: SearchInput): RankedEntry {
|
||||||
const relativePath = normalizeRelativePath(input.root, entry.visiblePath);
|
const relativePath = normalizeRelativePath(input.root, entry.visiblePath);
|
||||||
const lowerPath = relativePath.toLowerCase();
|
const lowerPath = relativePath.toLowerCase();
|
||||||
const query = input.plan.searchTerm.toLowerCase();
|
const query = getRankQuery(input);
|
||||||
const segments = lowerPath === "." ? [] : lowerPath.split("/");
|
const segments = lowerPath === "." ? [] : lowerPath.split("/");
|
||||||
const exact = findSegmentMatchIndex(segments, (segment) => segment === query);
|
const pathScore = query ? scorePathMatch(query, relativePath) : null;
|
||||||
const prefix = findSegmentMatchIndex(segments, (segment) => segment.startsWith(query));
|
const rankFields = input.plan.isPathQuery
|
||||||
const substring = findSegmentMatchIndex(segments, (segment) => segment.includes(query));
|
? rankPathMatch(pathScore)
|
||||||
const offset = lowerPath.indexOf(query);
|
: rankTextMatch({ query, lowerPath, pathFormat: input.pathFormat, pathScore, segments });
|
||||||
const fuzzyScore = scoreFuzzySubsequence(query, segments.at(-1) ?? "");
|
|
||||||
let matchTier = NO_MATCH_TIER;
|
|
||||||
let segmentIndex = NO_SEGMENT_INDEX;
|
|
||||||
if (!query) matchTier = 3;
|
|
||||||
else if (exact >= 0) {
|
|
||||||
matchTier = 0;
|
|
||||||
segmentIndex = exact;
|
|
||||||
} else if (prefix >= 0) {
|
|
||||||
matchTier = 1;
|
|
||||||
segmentIndex = prefix;
|
|
||||||
} else if (substring >= 0) {
|
|
||||||
matchTier = 2;
|
|
||||||
segmentIndex = substring;
|
|
||||||
} else if (input.pathFormat === "relative" ? lowerPath.startsWith(query) : offset >= 0)
|
|
||||||
matchTier = 3;
|
|
||||||
else if (fuzzyScore !== null) matchTier = 4;
|
|
||||||
return {
|
return {
|
||||||
path: entry.visiblePath,
|
path: entry.visiblePath,
|
||||||
kind: entry.kind,
|
kind: entry.kind,
|
||||||
matchTier,
|
...rankFields,
|
||||||
segmentIndex,
|
|
||||||
matchOffset: offset >= 0 ? offset : NO_MATCH_OFFSET,
|
|
||||||
fuzzyScore: fuzzyScore ?? NO_FUZZY_SCORE,
|
|
||||||
depth: relativePath === "." ? 0 : segments.length,
|
depth: relativePath === "." ? 0 : segments.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRankQuery(input: SearchInput): string {
|
||||||
|
return (
|
||||||
|
input.plan.isPathQuery ? input.plan.normalizedQuery : input.plan.searchTerm
|
||||||
|
).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankPathMatch(pathScore: MatchScore | null): RankFields {
|
||||||
|
return {
|
||||||
|
matchTier: pathScore ? Math.min(pathScore.tier, 4) : NO_MATCH_TIER,
|
||||||
|
segmentIndex: NO_SEGMENT_INDEX,
|
||||||
|
matchOffset: pathScore?.offset ?? NO_MATCH_OFFSET,
|
||||||
|
fuzzyScore: pathScore?.spread ?? NO_FUZZY_SCORE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankTextMatch(input: {
|
||||||
|
query: string;
|
||||||
|
lowerPath: string;
|
||||||
|
pathFormat: DirectorySuggestionPathFormat;
|
||||||
|
pathScore: MatchScore | null;
|
||||||
|
segments: string[];
|
||||||
|
}): RankFields {
|
||||||
|
const { query, lowerPath, pathFormat, pathScore, segments } = input;
|
||||||
|
const offset = lowerPath.indexOf(query);
|
||||||
|
const fuzzyScore = scoreFuzzySubsequence(query, segments.at(-1) ?? "");
|
||||||
|
if (!query) {
|
||||||
|
return {
|
||||||
|
matchTier: 3,
|
||||||
|
segmentIndex: NO_SEGMENT_INDEX,
|
||||||
|
matchOffset: NO_MATCH_OFFSET,
|
||||||
|
fuzzyScore: NO_FUZZY_SCORE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const segmentRank = rankSegmentMatch({ query, segments, offset, fuzzyScore });
|
||||||
|
if (segmentRank) return segmentRank;
|
||||||
|
if (pathFormat === "relative" ? lowerPath.startsWith(query) : offset >= 0) {
|
||||||
|
return {
|
||||||
|
matchTier: 3,
|
||||||
|
segmentIndex: NO_SEGMENT_INDEX,
|
||||||
|
matchOffset: offset,
|
||||||
|
fuzzyScore: fuzzyScore ?? NO_FUZZY_SCORE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (fuzzyScore !== null) {
|
||||||
|
return {
|
||||||
|
matchTier: 4,
|
||||||
|
segmentIndex: NO_SEGMENT_INDEX,
|
||||||
|
matchOffset: offset >= 0 ? offset : NO_MATCH_OFFSET,
|
||||||
|
fuzzyScore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
matchTier: pathScore ? Math.min(pathScore.tier, 4) : NO_MATCH_TIER,
|
||||||
|
segmentIndex: NO_SEGMENT_INDEX,
|
||||||
|
matchOffset: pathScore?.offset ?? NO_MATCH_OFFSET,
|
||||||
|
fuzzyScore: pathScore?.spread ?? NO_FUZZY_SCORE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankSegmentMatch(input: {
|
||||||
|
query: string;
|
||||||
|
segments: string[];
|
||||||
|
offset: number;
|
||||||
|
fuzzyScore: number | null;
|
||||||
|
}): RankFields | null {
|
||||||
|
const { query, segments, offset, fuzzyScore } = input;
|
||||||
|
const matchOffset = offset >= 0 ? offset : NO_MATCH_OFFSET;
|
||||||
|
const exact = findSegmentMatchIndex(segments, (segment) => segment === query);
|
||||||
|
if (exact >= 0) {
|
||||||
|
return {
|
||||||
|
matchTier: 0,
|
||||||
|
segmentIndex: exact,
|
||||||
|
matchOffset,
|
||||||
|
fuzzyScore: fuzzyScore ?? NO_FUZZY_SCORE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const prefix = findSegmentMatchIndex(segments, (segment) => segment.startsWith(query));
|
||||||
|
if (prefix >= 0) {
|
||||||
|
return {
|
||||||
|
matchTier: 1,
|
||||||
|
segmentIndex: prefix,
|
||||||
|
matchOffset,
|
||||||
|
fuzzyScore: fuzzyScore ?? NO_FUZZY_SCORE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const substring = findSegmentMatchIndex(segments, (segment) => segment.includes(query));
|
||||||
|
if (substring >= 0) {
|
||||||
|
return {
|
||||||
|
matchTier: 2,
|
||||||
|
segmentIndex: substring,
|
||||||
|
matchOffset,
|
||||||
|
fuzzyScore: fuzzyScore ?? NO_FUZZY_SCORE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function sortAndFormat(
|
function sortAndFormat(
|
||||||
entries: RankedEntry[],
|
entries: RankedEntry[],
|
||||||
root: string,
|
root: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user