From 9db646836f0353cbbc7f2e7b02a1a2c72c31e636 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 11:22:20 +0200 Subject: [PATCH 1/9] impr(commandline): improve performance (@fehmer) --- frontend/src/ts/commandline/commandline.ts | 68 +++++++++++----------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 5df7d9e2e87d..057d594343ba 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -459,27 +459,6 @@ async function showCommands(): Promise { activeIndex = firstActive; } element.innerHTML = html; - - for (const command of element.querySelectorAll(".command")) { - command.addEventListener("mouseenter", async () => { - if (!mouseMode) return; - activeIndex = parseInt(command.getAttribute("data-index") ?? "0"); - await updateActiveCommand(); - }); - command.addEventListener("mouseleave", async () => { - if (!mouseMode) return; - activeIndex = parseInt(command.getAttribute("data-index") ?? "0"); - await updateActiveCommand(); - }); - command.addEventListener("click", async () => { - const previous = activeIndex; - activeIndex = parseInt(command.getAttribute("data-index") ?? "0"); - if (previous !== activeIndex) { - await updateActiveCommand(); - } - await runActiveCommand(); - }); - } } async function updateActiveCommand(): Promise { @@ -573,23 +552,20 @@ async function runActiveCommand(): Promise { } } +let lastActiveIndex: string | undefined; function keepActiveCommandInView(): void { if (mouseMode) return; - try { - const scroll = - Math.abs( - ($(".suggestions").offset()?.top as number) - - ($(".command.active").offset()?.top as number) - - ($(".suggestions").scrollTop() as number) - ) - - ($(".suggestions").outerHeight() as number) / 2 + - ($($(".command")[0] as HTMLElement).outerHeight() as number); - $(".suggestions").scrollTop(scroll); - } catch (e) { - if (e instanceof Error) { - console.log("could not scroll suggestions: " + e.message); - } + + const active: HTMLElement | null = document.querySelector( + ".suggestions .command.active" + ); + + if (active === null || active.dataset["index"] === lastActiveIndex) { + return; } + + active?.scrollIntoView({ behavior: "auto", block: "center" }); + lastActiveIndex = active.dataset["index"]; } function updateInput(setInput?: string): void { @@ -740,5 +716,27 @@ const modal = new AnimatedModal({ modalEl.addEventListener("mousemove", (_e) => { mouseMode = true; }); + + const suggestions = document.querySelector(".suggestions") as HTMLElement; + + let lastSelected: HTMLElement | undefined; + + suggestions.addEventListener("mousemove", async (e) => { + if (e.target !== lastSelected) { + lastSelected = e.target as HTMLElement; + activeIndex = parseInt(lastSelected.getAttribute("data-index") ?? "0"); + await updateActiveCommand(); + } + }); + suggestions.addEventListener("click", async (e) => { + const previous = activeIndex; + activeIndex = parseInt( + (e.target as HTMLElement).getAttribute("data-index") ?? "0" + ); + if (previous !== activeIndex) { + await updateActiveCommand(); + } + await runActiveCommand(); + }); }, }); From ae725529b0c2cbdbd03409401ca6477f2684f78a Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 11:40:17 +0200 Subject: [PATCH 2/9] skip showing the same commandlist again --- frontend/src/ts/commandline/commandline.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 057d594343ba..8d696b7470a6 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -11,6 +11,7 @@ import * as ActivePage from "../states/active-page"; import { focusWords } from "../test/test-ui"; import * as Loader from "../elements/loader"; import { Command, CommandsSubgroup } from "./types"; +import { areSortedArraysEqual } from "../utils/arrays"; type CommandlineMode = "search" | "input"; type InputModeParams = { @@ -349,6 +350,7 @@ async function getList(): Promise { return (await getSubgroup()).list; } +let lastList: Command[] | undefined; async function showCommands(): Promise { const element = document.querySelector("#commandLine .suggestions"); if (element === null) { @@ -361,6 +363,10 @@ async function showCommands(): Promise { } const list = (await getList()).filter((c) => c.found === true); + if (lastList && areSortedArraysEqual(list, lastList)) { + return; + } + lastList = list; let html = ""; let index = 0; @@ -458,6 +464,7 @@ async function showCommands(): Promise { if (firstActive !== null && !usingSingleList) { activeIndex = firstActive; } + element.innerHTML = html; } @@ -564,7 +571,7 @@ function keepActiveCommandInView(): void { return; } - active?.scrollIntoView({ behavior: "auto", block: "center" }); + active.scrollIntoView({ behavior: "auto", block: "center" }); lastActiveIndex = active.dataset["index"]; } From 27976c628fc4b3d21788547f61a03618e15ae8f0 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 12:03:15 +0200 Subject: [PATCH 3/9] fix mouse events --- frontend/src/ts/commandline/commandline.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 8d696b7470a6..70d9c89f8d35 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -725,17 +725,25 @@ const modal = new AnimatedModal({ }); const suggestions = document.querySelector(".suggestions") as HTMLElement; + const isCommand = (event: MouseEvent): boolean => + event.target !== null && + (event.target as HTMLElement).hasAttribute("data-index"); - let lastSelected: HTMLElement | undefined; + let lastHover: HTMLElement | undefined; suggestions.addEventListener("mousemove", async (e) => { - if (e.target !== lastSelected) { - lastSelected = e.target as HTMLElement; - activeIndex = parseInt(lastSelected.getAttribute("data-index") ?? "0"); + if (!isCommand(e)) return; + + if (e.target !== lastHover) { + lastHover = e.target as HTMLElement; + + activeIndex = parseInt(lastHover.getAttribute("data-index") ?? "0"); await updateActiveCommand(); } }); suggestions.addEventListener("click", async (e) => { + if (!isCommand(e)) return; + const previous = activeIndex; activeIndex = parseInt( (e.target as HTMLElement).getAttribute("data-index") ?? "0" From 23b24db03a5e3d396b6c1d7febdf2da8f9a2a547 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 12:30:57 +0200 Subject: [PATCH 4/9] fix mouse events --- frontend/__tests__/utils/numbers.spec.ts | 24 +++++++++++++++++ frontend/src/ts/commandline/commandline.ts | 30 ++++++++++++---------- frontend/src/ts/utils/numbers.ts | 14 ++++++++++ 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/frontend/__tests__/utils/numbers.spec.ts b/frontend/__tests__/utils/numbers.spec.ts index ff3a147b1f41..2b4a3a11c80e 100644 --- a/frontend/__tests__/utils/numbers.spec.ts +++ b/frontend/__tests__/utils/numbers.spec.ts @@ -46,4 +46,28 @@ describe("numbers", () => { expect(Numbers.abbreviateNumber((number *= 1000))).toEqual("1.0d"); }); }); + describe("parseIntOptional", () => { + it("should return a number when given a valid string", () => { + expect(Numbers.parseIntOptional("123")).toBe(123); + expect(Numbers.parseIntOptional("42")).toBe(42); + expect(Numbers.parseIntOptional("0")).toBe(0); + }); + + it("should return undefined when given null", () => { + expect(Numbers.parseIntOptional(null)).toBeUndefined(); + }); + + it("should return undefined when given undefined", () => { + expect(Numbers.parseIntOptional(undefined)).toBeUndefined(); + }); + + it("should handle non-numeric strings", () => { + expect(Numbers.parseIntOptional("abc")).toBeNaN(); + expect(Numbers.parseIntOptional("12abc")).toBe(12); // parseInt stops at non-numeric chars + }); + + it("should handle leading and trailing spaces", () => { + expect(Numbers.parseIntOptional(" 42 ")).toBe(42); + }); + }); }); diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 70d9c89f8d35..461855fde482 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -12,6 +12,7 @@ import { focusWords } from "../test/test-ui"; import * as Loader from "../elements/loader"; import { Command, CommandsSubgroup } from "./types"; import { areSortedArraysEqual } from "../utils/arrays"; +import { parseIntOptional } from "../utils/numbers"; type CommandlineMode = "search" | "input"; type InputModeParams = { @@ -725,29 +726,30 @@ const modal = new AnimatedModal({ }); const suggestions = document.querySelector(".suggestions") as HTMLElement; - const isCommand = (event: MouseEvent): boolean => - event.target !== null && - (event.target as HTMLElement).hasAttribute("data-index"); - let lastHover: HTMLElement | undefined; suggestions.addEventListener("mousemove", async (e) => { - if (!isCommand(e)) return; + const target = e.target as HTMLElement | null; + if (target === lastHover) return; - if (e.target !== lastHover) { - lastHover = e.target as HTMLElement; + const dataIndex = parseIntOptional(target?.getAttribute("data-index")); - activeIndex = parseInt(lastHover.getAttribute("data-index") ?? "0"); - await updateActiveCommand(); - } + if (!dataIndex) return; + + lastHover = e.target as HTMLElement; + activeIndex = dataIndex; + await updateActiveCommand(); }); + suggestions.addEventListener("click", async (e) => { - if (!isCommand(e)) return; + const target = e.target as HTMLElement | null; + + const dataIndex = parseIntOptional(target?.getAttribute("data-index")); + + if (!dataIndex) return; const previous = activeIndex; - activeIndex = parseInt( - (e.target as HTMLElement).getAttribute("data-index") ?? "0" - ); + activeIndex = dataIndex; if (previous !== activeIndex) { await updateActiveCommand(); } diff --git a/frontend/src/ts/utils/numbers.ts b/frontend/src/ts/utils/numbers.ts index d4a831f8e91f..fe71830cf0a6 100644 --- a/frontend/src/ts/utils/numbers.ts +++ b/frontend/src/ts/utils/numbers.ts @@ -133,3 +133,17 @@ export function findLineByLeastSquares( ]; return [returnpoint1, returnpoint2]; } + +/** + * Parses a string into an integer if it is not null or undefined, otherwise returns undefined. + * + * @param The string to parse or null or undefined. + * @returns A number if a string is provided, otherwise undefined. + */ +export function parseIntOptional( + value: T +): T extends string ? number : undefined { + return ( + value !== null && value !== undefined ? parseInt(value, 10) : undefined + ) as T extends string ? number : undefined; +} From 6223dc6f4aea3477bce2ecd824323688a0ce5efe Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 12:58:57 +0200 Subject: [PATCH 5/9] debouce commandline --- frontend/src/ts/commandline/commandline.ts | 35 ++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 461855fde482..a39543025122 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -13,6 +13,7 @@ import * as Loader from "../elements/loader"; import { Command, CommandsSubgroup } from "./types"; import { areSortedArraysEqual } from "../utils/arrays"; import { parseIntOptional } from "../utils/numbers"; +import { debounce } from "throttle-debounce"; type CommandlineMode = "search" | "input"; type InputModeParams = { @@ -649,22 +650,26 @@ const modal = new AnimatedModal({ setup: async (modalEl): Promise => { const input = modalEl.querySelector("input") as HTMLInputElement; - input.addEventListener("input", async (e) => { - inputValue = (e.target as HTMLInputElement).value; - if (subgroupOverride === null) { - if (Config.singleListCommandLine === "on") { - usingSingleList = true; - } else { - usingSingleList = inputValue.startsWith(">"); + input.addEventListener( + "input", + debounce(100, async (e) => { + inputValue = (e.target as HTMLInputElement).value; + console.log("update", { inputValue }); + if (subgroupOverride === null) { + if (Config.singleListCommandLine === "on") { + usingSingleList = true; + } else { + usingSingleList = inputValue.startsWith(">"); + } } - } - if (mode !== "search") return; - mouseMode = false; - activeIndex = 0; - await filterSubgroup(); - await showCommands(); - await updateActiveCommand(); - }); + if (mode !== "search") return; + mouseMode = false; + activeIndex = 0; + await filterSubgroup(); + await showCommands(); + await updateActiveCommand(); + }) + ); input.addEventListener("keydown", async (e) => { mouseMode = false; From 3c9b12de4150d19be2db9a0798122c8d7a5ac376 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 13:05:20 +0200 Subject: [PATCH 6/9] debouce commandline --- frontend/src/ts/commandline/commandline.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index a39543025122..5f935dc0f03c 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -652,9 +652,8 @@ const modal = new AnimatedModal({ input.addEventListener( "input", - debounce(100, async (e) => { + debounce(50, async (e) => { inputValue = (e.target as HTMLInputElement).value; - console.log("update", { inputValue }); if (subgroupOverride === null) { if (Config.singleListCommandLine === "on") { usingSingleList = true; From 96ae52c039e1dfec0ea1f0c268e98ece0ead28aa Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 13:14:06 +0200 Subject: [PATCH 7/9] fix html not set after empty list --- frontend/src/ts/commandline/commandline.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 5f935dc0f03c..fccc9bd840b9 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -365,7 +365,11 @@ async function showCommands(): Promise { } const list = (await getList()).filter((c) => c.found === true); - if (lastList && areSortedArraysEqual(list, lastList)) { + if ( + lastList && + element.innerHTML !== "" && + areSortedArraysEqual(list, lastList) + ) { return; } lastList = list; From 032ab339b7d0a7a8eddf1314de8158f43e86b29a Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 13:20:35 +0200 Subject: [PATCH 8/9] same, but better --- frontend/src/ts/commandline/commandline.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index fccc9bd840b9..b2370049d860 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -328,6 +328,7 @@ function hideCommands(): void { throw new Error("Commandline element not found"); } element.innerHTML = ""; + lastList = undefined; } let cachedSingleSubgroup: CommandsSubgroup | null = null; @@ -353,6 +354,7 @@ async function getList(): Promise { } let lastList: Command[] | undefined; + async function showCommands(): Promise { const element = document.querySelector("#commandLine .suggestions"); if (element === null) { @@ -360,16 +362,12 @@ async function showCommands(): Promise { } if (inputValue === "" && usingSingleList) { - element.innerHTML = ""; + hideCommands(); return; } const list = (await getList()).filter((c) => c.found === true); - if ( - lastList && - element.innerHTML !== "" && - areSortedArraysEqual(list, lastList) - ) { + if (lastList && areSortedArraysEqual(list, lastList)) { return; } lastList = list; From e347fd105498377336e3bf833222d95738175a04 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 13 May 2025 15:08:04 +0200 Subject: [PATCH 9/9] add radix --- frontend/__tests__/utils/numbers.spec.ts | 5 +++++ frontend/src/ts/utils/numbers.ts | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/__tests__/utils/numbers.spec.ts b/frontend/__tests__/utils/numbers.spec.ts index 2b4a3a11c80e..ebb06b50ab50 100644 --- a/frontend/__tests__/utils/numbers.spec.ts +++ b/frontend/__tests__/utils/numbers.spec.ts @@ -69,5 +69,10 @@ describe("numbers", () => { it("should handle leading and trailing spaces", () => { expect(Numbers.parseIntOptional(" 42 ")).toBe(42); }); + it("should return a number when given a valid string and radix", () => { + expect(Numbers.parseIntOptional("1010", 2)).toBe(10); + expect(Numbers.parseIntOptional("CF", 16)).toBe(207); + expect(Numbers.parseIntOptional("C", 26)).toBe(12); + }); }); }); diff --git a/frontend/src/ts/utils/numbers.ts b/frontend/src/ts/utils/numbers.ts index fe71830cf0a6..7e2a30a0a6cf 100644 --- a/frontend/src/ts/utils/numbers.ts +++ b/frontend/src/ts/utils/numbers.ts @@ -138,12 +138,14 @@ export function findLineByLeastSquares( * Parses a string into an integer if it is not null or undefined, otherwise returns undefined. * * @param The string to parse or null or undefined. + * @param radix A value between 2 and 36 that specifies the base of the number in `string`. * @returns A number if a string is provided, otherwise undefined. */ export function parseIntOptional( - value: T + value: T, + radix: number = 10 ): T extends string ? number : undefined { return ( - value !== null && value !== undefined ? parseInt(value, 10) : undefined + value !== null && value !== undefined ? parseInt(value, radix) : undefined ) as T extends string ? number : undefined; }