diff --git a/apps/client/src/widgets/quick_search.ts b/apps/client/src/widgets/quick_search.ts index 2d06baafe..3c23eaea5 100644 --- a/apps/client/src/widgets/quick_search.ts +++ b/apps/client/src/widgets/quick_search.ts @@ -45,6 +45,13 @@ const MAX_DISPLAYED_NOTES = 15; // TODO: Deduplicate with server. interface QuickSearchResponse { searchResultNoteIds: string[]; + searchResults?: Array<{ + notePath: string; + noteTitle: string; + notePathTitle: string; + highlightedNotePathTitle: string; + icon: string; + }>; error: string; } @@ -115,7 +122,7 @@ export default class QuickSearchWidget extends BasicWidget { this.$dropdownMenu.empty(); this.$dropdownMenu.append(`${t("quick-search.searching")}`); - const { searchResultNoteIds, error } = await server.get(`quick-search/${encodeURIComponent(searchString)}`); + const { searchResultNoteIds, searchResults, error } = await server.get(`quick-search/${encodeURIComponent(searchString)}`); if (error) { let tooltip = new Tooltip(this.$searchString[0], { @@ -129,44 +136,88 @@ export default class QuickSearchWidget extends BasicWidget { setTimeout(() => tooltip.dispose(), 4000); } - const displayedNoteIds = searchResultNoteIds.slice(0, Math.min(MAX_DISPLAYED_NOTES, searchResultNoteIds.length)); - this.$dropdownMenu.empty(); - if (displayedNoteIds.length === 0) { - this.$dropdownMenu.append(`${t("quick-search.no-results")}`); - } + // Use highlighted search results if available, otherwise fall back to basic display + if (searchResults && searchResults.length > 0) { + const displayedResults = searchResults.slice(0, Math.min(MAX_DISPLAYED_NOTES, searchResults.length)); - for (const note of await froca.getNotes(displayedNoteIds)) { - const $link = await linkService.createLink(note.noteId, { showNotePath: true, showNoteIcon: true }); - $link.addClass("dropdown-item"); - $link.attr("tabIndex", "0"); - $link.on("click", (e) => { - this.dropdown.hide(); + if (displayedResults.length === 0) { + this.$dropdownMenu.append(`${t("quick-search.no-results")}`); + } + + for (const result of displayedResults) { + const noteId = result.notePath.split("/").pop(); + if (!noteId) continue; + + const $item = $(''); + $item.html(` ${result.highlightedNotePathTitle}`); + + $item.on("click", (e) => { + this.dropdown.hide(); + e.preventDefault(); + + const activeContext = appContext.tabManager.getActiveContext(); + if (activeContext) { + activeContext.setNote(noteId); + } + }); + + shortcutService.bindElShortcut($item, "return", () => { + this.dropdown.hide(); + + const activeContext = appContext.tabManager.getActiveContext(); + if (activeContext) { + activeContext.setNote(noteId); + } + }); + + this.$dropdownMenu.append($item); + } + + if (searchResults.length > MAX_DISPLAYED_NOTES) { + const numRemainingResults = searchResults.length - MAX_DISPLAYED_NOTES; + this.$dropdownMenu.append(`${t("quick-search.more-results", { number: numRemainingResults })}`); + } + } else { + // Fallback to original behavior if no highlighted results + const displayedNoteIds = searchResultNoteIds.slice(0, Math.min(MAX_DISPLAYED_NOTES, searchResultNoteIds.length)); + + if (displayedNoteIds.length === 0) { + this.$dropdownMenu.append(`${t("quick-search.no-results")}`); + } + + for (const note of await froca.getNotes(displayedNoteIds)) { + const $link = await linkService.createLink(note.noteId, { showNotePath: true, showNoteIcon: true }); + $link.addClass("dropdown-item"); + $link.attr("tabIndex", "0"); + $link.on("click", (e) => { + this.dropdown.hide(); + + if (!e.target || e.target.nodeName !== "A") { + // click on the link is handled by link handling, but we want the whole item clickable + const activeContext = appContext.tabManager.getActiveContext(); + if (activeContext) { + activeContext.setNote(note.noteId); + } + } + }); + shortcutService.bindElShortcut($link, "return", () => { + this.dropdown.hide(); - if (!e.target || e.target.nodeName !== "A") { - // click on the link is handled by link handling, but we want the whole item clickable const activeContext = appContext.tabManager.getActiveContext(); if (activeContext) { activeContext.setNote(note.noteId); } - } - }); - shortcutService.bindElShortcut($link, "return", () => { - this.dropdown.hide(); + }); - const activeContext = appContext.tabManager.getActiveContext(); - if (activeContext) { - activeContext.setNote(note.noteId); - } - }); + this.$dropdownMenu.append($link); + } - this.$dropdownMenu.append($link); - } - - if (searchResultNoteIds.length > MAX_DISPLAYED_NOTES) { - const numRemainingResults = searchResultNoteIds.length - MAX_DISPLAYED_NOTES; - this.$dropdownMenu.append(`${t("quick-search.more-results", { number: numRemainingResults })}`); + if (searchResultNoteIds.length > MAX_DISPLAYED_NOTES) { + const numRemainingResults = searchResultNoteIds.length - MAX_DISPLAYED_NOTES; + this.$dropdownMenu.append(`${t("quick-search.more-results", { number: numRemainingResults })}`); + } } const $showInFullButton = $('').text(t("quick-search.show-in-full-search")); diff --git a/apps/server/src/routes/api/search.ts b/apps/server/src/routes/api/search.ts index 0e9304a07..29d75c6dc 100644 --- a/apps/server/src/routes/api/search.ts +++ b/apps/server/src/routes/api/search.ts @@ -52,10 +52,15 @@ function quickSearch(req: Request) { fuzzyAttributeSearch: false }); - const resultNoteIds = searchService.findResultsWithQuery(searchString, searchContext).map((sr) => sr.noteId); + // Use the same highlighting logic as autocomplete for consistency + const searchResults = searchService.searchNotesForAutocomplete(searchString, false); + + // Extract note IDs for backward compatibility + const resultNoteIds = searchResults.map((result) => result.notePath.split("/").pop()).filter(Boolean) as string[]; return { searchResultNoteIds: resultNoteIds, + searchResults: searchResults, error: searchContext.getError() }; }