From 766e8251805ef41199beee2d6c9f89fa733d0690 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 11 Aug 2026 21:56:48 +0530 Subject: [PATCH] fix: support multi-word plugin search --- src/plugin-catalog.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/plugin-catalog.ts | 20 +++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/plugin-catalog.test.ts b/src/plugin-catalog.test.ts index eedb6dff..73995037 100644 --- a/src/plugin-catalog.test.ts +++ b/src/plugin-catalog.test.ts @@ -119,4 +119,39 @@ describe('plugin catalog', () => { expect(result.plugins.map((plugin) => plugin.name)).toEqual(['flights']); expect(result.errors).toEqual([{ sourceId: 'bad', manifestUrl: 'https://bad.test/webcmd-plugin.json', message: 'network failed' }]); }); + + + + it('matches multi-word plugin searches while requiring all terms', async () => { + const catalog: PluginCatalog = { + version: 1, + sources: [ + { + id: 'test', + source: 'github:test/repo', + manifestUrl: 'https://test.test/webcmd-plugin.json', + }, + ], + }; + + const fetchJson = async () => ({ + plugins: { + hackernews: { + path: 'plugins/hackernews', + description: 'Tools for technology discussions', + }, + }, + }); + + for (const query of ['hacker news', 'hacker-news', 'hackernews']) { + const result = await searchCatalogPlugins(catalog, { query, fetchJson }); + expect(result.plugins.map((plugin) => plugin.name)).toEqual(['hackernews']); + } + + const negative = await searchCatalogPlugins(catalog, { + query: 'hacker twitter', + fetchJson, + }); + expect(negative.plugins).toEqual([]); }); +}); \ No newline at end of file diff --git a/src/plugin-catalog.ts b/src/plugin-catalog.ts index c8b009f3..90144303 100644 --- a/src/plugin-catalog.ts +++ b/src/plugin-catalog.ts @@ -133,6 +133,10 @@ export function flattenPluginManifest(source: PluginCatalogSource, manifest: Plu }]; } +function normalizeSearchText(value: string): string { + return value.toLowerCase().replace(/[^a-z0-9]/g, ''); +} + export async function searchCatalogPlugins( catalog: PluginCatalog, options: { query?: string; fetchJson?: FetchJson } = {}, @@ -150,9 +154,19 @@ export async function searchCatalogPlugins( } })); - const query = options.query?.trim().toLowerCase(); - const filtered = query - ? plugins.filter((plugin) => `${plugin.name} ${plugin.description ?? ''}`.toLowerCase().includes(query)) + const query = options.query?.trim(); + const tokens = query + ? query.split(/\s+/).map(normalizeSearchText).filter(Boolean) + : []; + + const filtered = tokens.length + ? plugins.filter((plugin) => { + const haystack = normalizeSearchText( + `${plugin.name} ${plugin.description ?? ''}`, + ); + + return tokens.every((token) => haystack.includes(token)); + }) : plugins; filtered.sort((a, b) => a.name.localeCompare(b.name));