From 0f9340d883b12e24cdb167feeaf1db53530acfba Mon Sep 17 00:00:00 2001 From: Bernhard Jackiewicz Date: Sun, 4 Oct 2026 19:19:51 +0200 Subject: [PATCH] fix(ui): rank a search hit by its name in the index too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the branch node reads "cbm · working tree", a search for "working" ranked it behind the folders of the other hits and cut it from the list. Signed-off-by: Bernhard Jackiewicz --- .../src/galaxy/GalaxyNavigator.rank.test.ts | 24 +++++++++++++++++++ graph-ui/src/galaxy/GalaxyNavigator.tsx | 9 +++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 graph-ui/src/galaxy/GalaxyNavigator.rank.test.ts diff --git a/graph-ui/src/galaxy/GalaxyNavigator.rank.test.ts b/graph-ui/src/galaxy/GalaxyNavigator.rank.test.ts new file mode 100644 index 000000000..ae83de73b --- /dev/null +++ b/graph-ui/src/galaxy/GalaxyNavigator.rank.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { choicesFromSearchHits, searchPriority } from './GalaxyNavigator'; + +/* + * Handtest 04.10. (abends): In cbm fand "working" den Branch-Knoten nicht mehr. + * Seit er "cbm · working tree" heisst, beginnt sein gezeigter Name nicht mit dem + * Suchwort, und die Ordner der anderen Treffer schoben ihn aus den zwoelf Plaetzen. + * Sein Name im Index ("working-tree") zaehlt wie der gezeigte. + */ +describe('search priority', () => { + const [branch] = choicesFromSearchHits([{ qualified_name: 'cbm.__branch__.working-tree', name: 'working-tree', label: 'Branch', file_path: '{}' } as never]); + + it('ranks a hit whose name in the index starts with the search word with the name matches', () => { + expect(branch.name).toBe('cbm · working tree'); + expect(searchPriority(branch, 'working')).toBe(1); + expect(searchPriority(branch, 'working-tree')).toBe(0); + }); + + it('keeps folders behind name matches and other hits behind folders', () => { + const folder = { key: 'docs/', name: 'docs/', detail: 'Indexed folder', kind: 'Folder', scope: { kind: 'folder' as const, path: 'docs', name: 'docs/' } }; + expect(searchPriority(folder, 'working')).toBe(2); + expect(searchPriority({ ...branch, name: 'other', scope: { kind: 'symbol' as const, qualifiedName: 'a.b', name: 'other' } }, 'working')).toBe(3); + }); +}); diff --git a/graph-ui/src/galaxy/GalaxyNavigator.tsx b/graph-ui/src/galaxy/GalaxyNavigator.tsx index 8cf4e30f1..9d0c51b95 100644 --- a/graph-ui/src/galaxy/GalaxyNavigator.tsx +++ b/graph-ui/src/galaxy/GalaxyNavigator.tsx @@ -32,6 +32,12 @@ export function choicesFromSearchHits(hits: readonly SearchGraphHit[]): Choice[] } return [...choices.values()]; } +/** Exact names first, then names that start with the search word, then folders and files, then the rest. + * The name in the index counts like the shown one: "cbm · working tree" is "working-tree" there. */ +export function searchPriority(choice: Choice, needle: string): number { + const names = [choice.name, 'name' in choice.scope ? choice.scope.name : undefined].filter((name): name is string => Boolean(name)).map(name => name.toLocaleLowerCase()); + return names.includes(needle) ? 0 : names.some(name => name.startsWith(needle)) ? 1 : choice.kind === 'Folder' || choice.kind === 'File' ? 2 : 3; +} /** One search for graph symbols and file/folder clusters, including unloaded symbols. */ export default function GalaxyNavigator({ nodes, onSelect, onSelectScope, project, fetch: fetchImpl, embedded = false }: { nodes: readonly GraphNode[]; onSelect: (node: GraphNode) => void; onSelectScope?: (scope: GraphScope) => void; @@ -97,8 +103,7 @@ export default function GalaxyNavigator({ nodes, onSelect, onSelectScope, projec choices.push({ key: path, name: path, detail: `${count.toLocaleString()} loaded nodes`, kind: folder ? 'Folder' : 'File', scope: { kind: folder ? 'folder' : 'file', path: path.replace(/\/$/, ''), name: path } }); } - const priority = (choice: Choice) => choice.name.toLocaleLowerCase() === needle ? 0 - : choice.name.toLocaleLowerCase().startsWith(needle) ? 1 : choice.kind === 'Folder' || choice.kind === 'File' ? 2 : 3; + const priority = (choice: Choice) => searchPriority(choice, needle); const unique = new Map(choices.map(choice => [choice.key, choice])); for (const choice of remote) if (!unique.has(choice.key)) unique.set(choice.key, choice); return [...unique.values()].sort((a, b) => priority(a) - priority(b) || a.name.localeCompare(b.name)).slice(0, 12);