diff --git a/src/public/app/services/note_autocomplete.ts b/src/public/app/services/note_autocomplete.ts index ca9894b1f..ae2522e18 100644 --- a/src/public/app/services/note_autocomplete.ts +++ b/src/public/app/services/note_autocomplete.ts @@ -1,6 +1,5 @@ import server from "./server.js"; import appContext from "../components/app_context.js"; -import utils from "./utils.js"; import noteCreateService from "./note_create.js"; import froca from "./froca.js"; import { t } from "./i18n.js"; @@ -10,6 +9,18 @@ const SELECTED_NOTE_PATH_KEY = "data-note-path"; const SELECTED_EXTERNAL_LINK_KEY = "data-external-link"; +// To prevent search lag when there are a large number of notes, set a delay based on the number of notes to avoid jitter. +const notesCount = await server.get(`autocomplete/notesCount`); +let debounceTimeoutId: ReturnType; + +function getSearchDelay(notesCount: number): number { + const maxNotes = 20000; + const maxDelay = 1000; + const delay = Math.min(maxDelay, (notesCount / maxNotes) * maxDelay); + return delay; +} +let searchDelay = getSearchDelay(notesCount); + export interface Suggestion { noteTitle?: string; externalLink?: string; @@ -72,10 +83,9 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void const activeNoteId = appContext.tabManager.getActiveContextNoteId(); const length = term.trim().length; - let results: Suggestion[] = []; - if (length >= 3) { - results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`); - } + let results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`); + + options.fastSearch = true; if (length >= 1 && options.allowCreatingNotes) { results = [ @@ -112,6 +122,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void } function clearText($el: JQuery) { + searchDelay = 0; $el.setSelectedNotePath(""); $el.autocomplete("val", "").trigger("change"); } @@ -122,6 +133,7 @@ function setText($el: JQuery, text: string) { } function showRecentNotes($el: JQuery) { + searchDelay = 0; $el.setSelectedNotePath(""); $el.autocomplete("val", ""); $el.autocomplete("open"); @@ -137,11 +149,8 @@ function fullTextSearch($el: JQuery, options: Options) { options.fastSearch = false; $el.autocomplete("val", ""); $el.setSelectedNotePath(""); + searchDelay = 0; $el.autocomplete("val", searchString); - // Set a delay to avoid resetting to true before full text search (await server.get) is called. - setTimeout(() => { - options.fastSearch = true; - }, 100); } function initNoteAutocomplete($el: JQuery, options?: Options) { @@ -154,6 +163,15 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { options = options || {}; + // Used to track whether the user is performing character composition with an input method (such as Chinese Pinyin, Japanese, Korean, etc.) and to avoid triggering a search during the composition process. + let isComposingInput = false; + $el.on("compositionstart", () => { + isComposingInput = true; + }); + $el.on("compositionend", () => { + isComposingInput = false; + }); + $el.addClass("note-autocomplete-input"); const $clearTextButton = $("").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field")); @@ -226,7 +244,19 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { }, [ { - source: (term, cb) => autocompleteSource(term, cb, options), + source: (term, cb) => { + clearTimeout(debounceTimeoutId); + debounceTimeoutId = setTimeout(() => { + if (isComposingInput) { + return; + } + autocompleteSource(term, cb, options); + }, searchDelay); + + if (searchDelay === 0) { + searchDelay = getSearchDelay(notesCount); + } + }, displayKey: "notePathTitle", templates: { suggestion: (suggestion) => suggestion.highlightedNotePathTitle diff --git a/src/routes/api/autocomplete.ts b/src/routes/api/autocomplete.ts index a357ca4b3..584c2c88a 100644 --- a/src/routes/api/autocomplete.ts +++ b/src/routes/api/autocomplete.ts @@ -8,6 +8,7 @@ import cls from "../../services/cls.js"; import becca from "../../becca/becca.js"; import type { Request } from "express"; import ValidationError from "../../errors/validation_error.js"; +import sql from "../../services/sql.js"; function getAutocomplete(req: Request) { if (typeof req.query.query !== "string") { @@ -79,6 +80,15 @@ function getRecentNotes(activeNoteId: string) { }); } +// Get the total number of notes +function getNotesCount(req: Request) { + const notesCount = sql.getRow( + `SELECT COUNT(*) AS count FROM notes WHERE isDeleted = 0;`, + ) as { count: number }; + return notesCount.count; +} + export default { - getAutocomplete + getAutocomplete, + getNotesCount }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index abae1acaa..a08a37c9b 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -273,6 +273,7 @@ function register(app: express.Application) { route(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler, false); apiRoute(GET, "/api/autocomplete", autocompleteApiRoute.getAutocomplete); + apiRoute(GET, "/api/autocomplete/notesCount", autocompleteApiRoute.getNotesCount); apiRoute(GET, "/api/quick-search/:searchString", searchRoute.quickSearch); apiRoute(GET, "/api/search-note/:noteId", searchRoute.searchFromNote); apiRoute(PST, "/api/search-and-execute-note/:noteId", searchRoute.searchAndExecute);