From 63bcfdeae941afb3ce5293ae867c2da5e3963424 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Wed, 29 Oct 2025 21:33:45 +0000 Subject: [PATCH] fix: remove min query length requirement and improve search UI styling - Remove 3-character minimum search query requirement - Add dynamic padding to search results container - Style search results with icons, grid layout, and hover states - Fix missing semicolons in ChatInput event handlers - Implement auto-scroll for selected search result - Initialize vault cache after metadata resolution - Update tests to reflect removed query length validation --- Components/ChatInput.svelte | 8 +- Components/ChatSearchResults.svelte | 91 +++++++++++++++++++- Services/UserInputService.ts | 4 +- Services/VaultCacheService.ts | 10 ++- __tests__/Services/UserInputService.test.ts | 20 +++-- __tests__/Services/VaultCacheService.test.ts | 29 ++++++- 6 files changed, 144 insertions(+), 18 deletions(-) diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index bc50602..cd3b315 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -79,7 +79,7 @@ if (SearchTrigger.isSearchTrigger(e.key)) { e.preventDefault(); - const position = inputService.getCursorPosition(textareaElement) + const position = inputService.getCursorPosition(textareaElement); const trigger = SearchTrigger.fromInput(e.key); searchStateStore.initializeSearch(trigger, position); @@ -112,7 +112,7 @@ } if (e.key === "ArrowDown") { e.preventDefault(); - searchStateStore.setSelectedResultToNext() + searchStateStore.setSelectedResultToNext(); } return; } @@ -200,7 +200,7 @@
-
+
0 ? "var(--size-4-2)" : 0}>
@@ -259,7 +259,7 @@ #input-search-results-container { grid-row: 1; - grid-column: 2 / 8; + grid-column: 2 / 7; } #input-field { diff --git a/Components/ChatSearchResults.svelte b/Components/ChatSearchResults.svelte index 240f082..afb3a0e 100644 --- a/Components/ChatSearchResults.svelte +++ b/Components/ChatSearchResults.svelte @@ -1,14 +1,45 @@
- {#each searchState.results as searchResult} -
{searchResult}
+ {#each searchState.results as searchResult, index} +
+
+
{basename(searchResult)}
+
{searchResult}
+
{/each}
@@ -33,7 +83,6 @@ transition: height 0.2s ease-out; overflow: auto; scroll-behavior: smooth; - scroll-snap-type: mandatory; } #input-search-results::-webkit-scrollbar { @@ -43,6 +92,40 @@ #input-search-results-inner-container { display: flex; flex-direction: column; - gap: 2px; + gap: var(--size-2-2); + } + + .input-search-result-container { + display: grid; + grid-template-rows: auto auto; + grid-template-columns: auto 1fr; + border-style: solid; + border-radius: var(--size-2-2); + border-color: var(--background-primary-alt); + border-width: 1px; + padding: var(--size-2-2) var(--size-4-2); + } + + .input-search-result-icon { + grid-row: 1 / 3; + grid-column: 1; + display: flex; + align-items: center; + justify-content: center; + padding-right: var(--size-4-2); + } + + .input-search-result-title { + grid-row: 1; + grid-column: 2; + font-family: var(--font-interface-theme); + } + + .input-search-result-subtitle { + grid-row: 2; + grid-column: 2; + font-family: var(--font-interface-theme); + font-size: var(--font-smallest); + color: var(--text-muted); } \ No newline at end of file diff --git a/Services/UserInputService.ts b/Services/UserInputService.ts index 729fd7e..f86c0d1 100644 --- a/Services/UserInputService.ts +++ b/Services/UserInputService.ts @@ -22,9 +22,7 @@ export class UserInputService { public performSearch() { const state = get(this.searchStateStore.searchState); - if (!state.active || - state.trigger == null || - state.query.trim().length < 3) { + if (!state.active || state.trigger == null) { this.searchStateStore.setResults([]); return; } diff --git a/Services/VaultCacheService.ts b/Services/VaultCacheService.ts index ccf4647..293b31b 100644 --- a/Services/VaultCacheService.ts +++ b/Services/VaultCacheService.ts @@ -29,12 +29,20 @@ export class VaultCacheService { private preparedFiles: { prepared: Fuzzysort.Prepared, file: TFile }[] = []; private preparedFolders: { prepared: Fuzzysort.Prepared, folder: TFolder }[] = []; + private initialised = false; + public constructor() { this.plugin = Resolve(Services.AIAgentPlugin); this.vaultService = Resolve(Services.VaultService); this.metaDataCache = this.plugin.app.metadataCache; - this.setupCaches(); this.registerFileEvents(); + + this.plugin.app.metadataCache.on("resolved", () => { + if (!this.initialised) { + this.setupCaches(); + this.initialised = true; + } + }); } public matchTag(input: string): Fuzzysort.KeyResults<{ prepared: Fuzzysort.Prepared, tag: string }> { diff --git a/__tests__/Services/UserInputService.test.ts b/__tests__/Services/UserInputService.test.ts index a58db3a..aa4ca93 100644 --- a/__tests__/Services/UserInputService.test.ts +++ b/__tests__/Services/UserInputService.test.ts @@ -132,10 +132,14 @@ describe('UserInputService', () => { selectedResult: "" }); + mockVaultCacheService.matchTag.mockReturnValue([ + { obj: { tag: '#ab' }, score: 0 } + ]); + userInputService.performSearch(); - expect(mockSearchStateStore.setResults).toHaveBeenCalledWith([]); - expect(mockVaultCacheService.matchTag).not.toHaveBeenCalled(); + expect(mockVaultCacheService.matchTag).toHaveBeenCalledWith("ab"); + expect(mockSearchStateStore.setResults).toHaveBeenCalledWith(['#ab']); }); it('should set empty results when query is empty', () => { @@ -148,10 +152,12 @@ describe('UserInputService', () => { selectedResult: "" }); + mockVaultCacheService.matchTag.mockReturnValue([]); + userInputService.performSearch(); + expect(mockVaultCacheService.matchTag).toHaveBeenCalledWith(""); expect(mockSearchStateStore.setResults).toHaveBeenCalledWith([]); - expect(mockVaultCacheService.matchTag).not.toHaveBeenCalled(); }); it('should set empty results when trimmed query length is less than 3', () => { @@ -164,10 +170,12 @@ describe('UserInputService', () => { selectedResult: "" }); + mockVaultCacheService.matchTag.mockReturnValue([]); + userInputService.performSearch(); + expect(mockVaultCacheService.matchTag).toHaveBeenCalledWith(" a "); expect(mockSearchStateStore.setResults).toHaveBeenCalledWith([]); - expect(mockVaultCacheService.matchTag).not.toHaveBeenCalled(); }); it('should proceed with search when query length is exactly 3', () => { @@ -578,10 +586,12 @@ describe('UserInputService', () => { selectedResult: "" }); + mockVaultCacheService.matchTag.mockReturnValue([]); + userInputService.performSearch(); + expect(mockVaultCacheService.matchTag).toHaveBeenCalledWith(" "); expect(mockSearchStateStore.setResults).toHaveBeenCalledWith([]); - expect(mockVaultCacheService.matchTag).not.toHaveBeenCalled(); }); it('should preserve query with internal spaces', () => { diff --git a/__tests__/Services/VaultCacheService.test.ts b/__tests__/Services/VaultCacheService.test.ts index e124c7e..76e5c3c 100644 --- a/__tests__/Services/VaultCacheService.test.ts +++ b/__tests__/Services/VaultCacheService.test.ts @@ -54,7 +54,8 @@ vi.mock('fuzzysort', () => { // Create mock instances const mockMetadataCache = { - getCache: vi.fn() + getCache: vi.fn(), + on: vi.fn() }; const mockVault = { @@ -152,16 +153,42 @@ describe('VaultCacheService - Integration Tests', () => { fileEventHandler = handler; }); + let resolvedHandler: any = null; + const mockMetadataCacheWithHandler = { + getCache: mockMetadataCache.getCache, + on: vi.fn((event: string, handler: any) => { + if (event === 'resolved') { + resolvedHandler = handler; + } + }) + }; + + const mockPluginWithMetadata = { + app: { + vault: mockVault, + metadataCache: mockMetadataCacheWithHandler + }, + settings: { + exclusions: [] + }, + registerEvent: vi.fn() + }; + const mockVaultServiceWithContent = { registerFileEvents: mockRegisterEvents, listVaultContents: mockListVaultContents }; + RegisterSingleton(Services.AIAgentPlugin, mockPluginWithMetadata as any); RegisterSingleton(Services.VaultService, mockVaultServiceWithContent as any); // Create new instance to trigger initialization new VaultCacheService(); + // Trigger the resolved event + expect(resolvedHandler).toBeDefined(); + resolvedHandler(); + expect(mockListVaultContents).toHaveBeenCalled(); }); });