diff --git a/fluxer_app/src/features/search/state/QuickSwitcher.tsx b/fluxer_app/src/features/search/state/QuickSwitcher.tsx index 77bc2224d..297c6d5fd 100644 --- a/fluxer_app/src/features/search/state/QuickSwitcher.tsx +++ b/fluxer_app/src/features/search/state/QuickSwitcher.tsx @@ -19,6 +19,7 @@ import { generateQueryModeResults, resolveTransformedMember, } from '@app/features/search/state/QuickSwitcherResultGenerators'; +import {resolveRecomputedSelectedIndex} from '@app/features/search/state/QuickSwitcherSelection'; import type { CandidateSets, ComputeResultsForQueryResult, @@ -404,10 +405,11 @@ class QuickSwitcher { if (options.invalidateCandidates ?? true) { this.invalidateCandidateSets(); } + const previous = this.results[this.selectedIndex]; const {queryMode, results, selectedIndex} = this.computeResultsForQuery(this.query); this.queryMode = queryMode; this.results = results; - this.selectedIndex = selectedIndex; + this.selectedIndex = resolveRecomputedSelectedIndex(previous, results, selectedIndex); } private computeResultsForQuery(query: string): ComputeResultsForQueryResult { diff --git a/fluxer_app/src/features/search/state/QuickSwitcherSelection.test.ts b/fluxer_app/src/features/search/state/QuickSwitcherSelection.test.ts new file mode 100644 index 000000000..0d4247627 --- /dev/null +++ b/fluxer_app/src/features/search/state/QuickSwitcherSelection.test.ts @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {resolveRecomputedSelectedIndex} from '@app/features/search/state/QuickSwitcherSelection'; +import type {QuickSwitcherResult} from '@app/features/search/state/QuickSwitcherTypes'; +import {QuickSwitcherResultTypes} from '@fluxer/constants/src/QuickSwitcherConstants'; +import {describe, expect, it} from 'vitest'; + +const result = (type: QuickSwitcherResult['type'], id: string) => ({type, id}) as QuickSwitcherResult; + +describe('resolveRecomputedSelectedIndex', () => { + it('follows the focused result when the list is reordered', () => { + const previous = result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c3'); + const results = [ + result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c3'), + result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c1'), + result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c2'), + ]; + expect(resolveRecomputedSelectedIndex(previous, results, 0)).toBe(0); + }); + + it('keeps the focused result when entries are inserted above it', () => { + const previous = result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c2'); + const results = [ + result(QuickSwitcherResultTypes.HEADER, 'h1'), + result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c9'), + result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c2'), + ]; + expect(resolveRecomputedSelectedIndex(previous, results, 1)).toBe(2); + }); + + it('falls back when the focused result disappears', () => { + const previous = result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'gone'); + const results = [result(QuickSwitcherResultTypes.TEXT_CHANNEL, 'c1')]; + expect(resolveRecomputedSelectedIndex(previous, results, 0)).toBe(0); + }); + + it('does not confuse ids that repeat across result types', () => { + const previous = result(QuickSwitcherResultTypes.GUILD, '42'); + const results = [result(QuickSwitcherResultTypes.TEXT_CHANNEL, '42'), result(QuickSwitcherResultTypes.GUILD, '42')]; + expect(resolveRecomputedSelectedIndex(previous, results, 0)).toBe(1); + }); + + it('uses the fallback when nothing was focused', () => { + expect(resolveRecomputedSelectedIndex(undefined, [result(QuickSwitcherResultTypes.GUILD, 'g')], 0)).toBe(0); + }); +}); diff --git a/fluxer_app/src/features/search/state/QuickSwitcherSelection.ts b/fluxer_app/src/features/search/state/QuickSwitcherSelection.ts new file mode 100644 index 000000000..80aeae2b1 --- /dev/null +++ b/fluxer_app/src/features/search/state/QuickSwitcherSelection.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {QuickSwitcherResult} from '@app/features/search/state/QuickSwitcherTypes'; + +export function resolveRecomputedSelectedIndex( + previous: QuickSwitcherResult | undefined, + results: ReadonlyArray, + fallbackIndex: number, +): number { + if (!previous) { + return fallbackIndex; + } + const restored = results.findIndex((result) => result.type === previous.type && result.id === previous.id); + return restored >= 0 ? restored : fallbackIndex; +}