fix(shortcuts): show effective bindings in the cheat sheet (#2985)

Render and search the shortcut chord that actually fires, including rebinding default-only display tokens such as question mark and index wildcards. Unassigned rows show no empty badge, and named-key overrides use the shared display vocabulary.
This commit is contained in:
Christoph Leiter
2026-08-09 01:56:32 +02:00
committed by GitHub
parent 90aa8bbefc
commit 74cf1931b1
8 changed files with 501 additions and 160 deletions
@@ -6,37 +6,16 @@ import { getIsElectronRuntime } from "@/constants/layout";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { Shortcut } from "@/components/ui/shortcut";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { formatShortcut } from "@/utils/format-shortcut";
import { getShortcutOs } from "@/utils/shortcut-platform";
import {
buildEffectiveBindings,
buildKeyboardShortcutHelpSections,
} from "@/keyboard/keyboard-shortcuts";
import { filterShortcutHelpSections } from "@/keyboard/shortcut-help-search";
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
const SNAP_POINTS: string[] = ["70%", "92%"];
function shortcutSearchAliases(keys: string[], shortcutOs: "mac" | "non-mac"): string {
const aliases = keys.map((key) => {
if (shortcutOs === "mac") {
if (key === "mod" || key === "meta") return ["cmd", "command"];
if (key === "alt") return ["alt", "option"];
} else {
if (key === "mod" || key === "ctrl") return ["ctrl", "control"];
if (key === "meta") return ["win", "windows"];
}
return [key];
});
const combinations = aliases.reduce<string[][]>(
(prefixes, choices) =>
prefixes.flatMap((prefix) => choices.map((choice) => [...prefix, choice])),
[[]],
);
return combinations
.flatMap((combination) => [combination.join(" "), combination.join("+")])
.join(" ");
}
export function KeyboardShortcutsDialog() {
const { t } = useTranslation();
const open = useKeyboardShortcutsStore((s) => s.shortcutsDialogOpen);
@@ -54,33 +33,10 @@ export function KeyboardShortcutsDialog() {
() => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }, bindings),
[bindings, isDesktopApp, isMac],
);
const visibleSections = useMemo(() => {
const normalizedQuery = query.trim().toLocaleLowerCase();
if (!normalizedQuery) return sections;
return sections.flatMap((section) => {
const sectionTitle = t(section.titleKey);
if (sectionTitle.toLocaleLowerCase().includes(normalizedQuery)) {
return [section];
}
const rows = section.rows.filter((row) => {
const searchText = [
t(row.labelKey),
row.noteKey ? t(row.noteKey) : row.note,
row.keys.join(" "),
formatShortcut(row.keys, shortcutOs),
shortcutSearchAliases(row.keys, shortcutOs),
]
.filter(Boolean)
.join(" ")
.toLocaleLowerCase();
return searchText.includes(normalizedQuery);
});
return rows.length > 0 ? [{ ...section, rows }] : [];
});
}, [query, sections, shortcutOs, t]);
const visibleSections = useMemo(
() => filterShortcutHelpSections({ sections, query, translate: t, shortcutOs }),
[query, sections, shortcutOs, t],
);
useEffect(() => {
if (!open) setQuery("");
@@ -114,14 +70,18 @@ export function KeyboardShortcutsDialog() {
<Text style={styles.sectionTitle}>{t(section.titleKey)}</Text>
<View style={styles.rows}>
{section.rows.map((row) => (
<View key={row.id} style={styles.row}>
<View key={row.id} style={styles.row} testID={`shortcut-help-row-${row.id}`}>
<View style={styles.rowText}>
<Text style={styles.rowLabel}>{t(row.labelKey)}</Text>
{row.note ? (
<Text style={styles.rowNote}>{row.noteKey ? t(row.noteKey) : row.note}</Text>
) : null}
</View>
<Shortcut keys={row.keys} style={styles.rowShortcut} />
{row.chord === null ? (
<Text style={styles.rowUnassigned}>{t("settings.shortcuts.unassigned")}</Text>
) : (
<Shortcut chord={row.chord} style={styles.rowShortcut} />
)}
</View>
))}
</View>
@@ -179,6 +139,13 @@ const styles = StyleSheet.create((theme) => ({
rowShortcut: {
alignSelf: "flex-start",
},
// Matches the settings row's unassigned label, so the two screens describe the
// same state the same way.
rowUnassigned: {
alignSelf: "flex-start",
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
empty: {
paddingVertical: theme.spacing[6],
textAlign: "center",