a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: rename AceJump to Search, add debounce

- Rename AceJump.tsx → Search.tsx and update component name/props
- Rename aceJumpOpenAtom → searchOpenAtom in atoms.ts
- Rename inlineJumpQueryAtom → aceJumpQueryAtom (this is the actual AceJump feature)
- Update all imports and usages in AppShell.tsx and revision-graph/index.tsx
- Update KeyboardShortcutsHelp descriptions
- Add 150ms debounce to search input to prevent lag while typing
- Update TAT-sjxoqgfa issue title to reference Search instead of AceJump

+63 -43
+4 -3
apps/desktop/src/atoms.ts
··· 1 1 import { Atom } from "@effect-atom/atom"; 2 2 3 3 export const shortcutsHelpOpenAtom = Atom.make(false); 4 - export const aceJumpOpenAtom = Atom.make(false); 5 - // Inline jump mode: when active, shows jump hints on visible revision change IDs 4 + // Search dialog open state (/ key) 5 + export const searchOpenAtom = Atom.make(false); 6 + // AceJump mode: when active, shows jump hints on visible revision change IDs 6 7 // Stores the typed query prefix (empty string = initial state showing first letters) 7 - export const inlineJumpQueryAtom = Atom.make<string | null>(null); 8 + export const aceJumpQueryAtom = Atom.make<string | null>(null); 8 9 // View mode: 1 = overview (only revisions), 2 = split (revisions + diff panel) 9 10 export type ViewMode = 1 | 2; 10 11 export const viewModeAtom = Atom.make<ViewMode>(1);
+26 -7
apps/desktop/src/components/AceJump.tsx apps/desktop/src/components/Search.tsx
··· 1 1 import { useAtom } from "@effect-atom/atom-react"; 2 2 import { useQuery } from "@tanstack/react-query"; 3 3 import type React from "react"; 4 - import { useRef, useState, useMemo, useCallback, useDeferredValue } from "react"; 5 - import { aceJumpOpenAtom } from "@/atoms"; 4 + import { useRef, useState, useMemo, useCallback, useEffect } from "react"; 5 + import { searchOpenAtom } from "@/atoms"; 6 6 import { 7 7 CommandDialog, 8 8 CommandEmpty, ··· 15 15 import { useKeyboardShortcut } from "@/hooks/useKeyboard"; 16 16 import { resolveRevset, type Revision } from "@/tauri-commands"; 17 17 18 - interface AceJumpProps { 18 + interface SearchProps { 19 19 revisions: Revision[]; 20 20 repoPath: string | null; 21 21 onJump: (changeId: string) => void; ··· 64 64 return false; 65 65 } 66 66 67 - export function AceJump({ revisions, repoPath, onJump }: AceJumpProps) { 68 - const [open, setOpenRaw] = useAtom(aceJumpOpenAtom); 67 + export function Search({ revisions, repoPath, onJump }: SearchProps) { 68 + const [open, setOpenRaw] = useAtom(searchOpenAtom); 69 69 const [search, setSearch] = useState(""); 70 - // Use React's useDeferredValue for debouncing - defers updates during typing 71 - const debouncedSearch = useDeferredValue(search); 70 + // Debounce search input to prevent lag during typing 71 + const [debouncedSearch, setDebouncedSearch] = useState(""); 72 + const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); 73 + 74 + useEffect(() => { 75 + // Clear any pending debounce 76 + if (debounceTimerRef.current) { 77 + clearTimeout(debounceTimerRef.current); 78 + } 79 + 80 + // Update after 150ms of no changes 81 + debounceTimerRef.current = setTimeout(() => { 82 + setDebouncedSearch(search); 83 + }, 150); 84 + 85 + return () => { 86 + if (debounceTimerRef.current) { 87 + clearTimeout(debounceTimerRef.current); 88 + } 89 + }; 90 + }, [search]); 72 91 73 92 // Wrap setOpen to reset state when opening 74 93 const setOpen = useCallback(
+5 -5
apps/desktop/src/components/AppShell.tsx
··· 21 21 return useSyncExternalStore(subscribeToMediaQuery, getIsNarrowScreen, () => false); 22 22 } 23 23 24 - import { AceJump } from "@/components/AceJump"; 24 + import { Search } from "@/components/Search"; 25 25 import { AppHeader } from "@/components/AppHeader"; 26 26 import { CommandPalette } from "@/components/CommandPalette"; 27 27 import { PrerenderedDiffPanel } from "@/components/DiffPanel"; ··· 76 76 77 77 return ( 78 78 <> 79 - <AceJump revisions={[]} repoPath={null} onJump={() => {}} /> 79 + <Search revisions={[]} repoPath={null} onJump={() => {}} /> 80 80 <CommandPalette 81 81 onOpenRepo={handleAddRepository} 82 82 onOpenProjects={() => navigate({ to: "/repositories" })} ··· 378 378 } 379 379 380 380 function handleOpenSearch() { 381 - // Focus the AceJump / revision search 382 - // The "/" key already triggers this via AceJump 381 + // Focus the revision search dialog 382 + // The "/" key already triggers this via Search component 383 383 window.dispatchEvent(new KeyboardEvent("keydown", { key: "/" })); 384 384 } 385 385 ··· 397 397 onOpenSettings={() => navigate({ to: "/settings" })} 398 398 /> 399 399 <KeyboardShortcutsHelp /> 400 - <AceJump 400 + <Search 401 401 revisions={orderedRevisions} 402 402 repoPath={activeProject?.path ?? null} 403 403 onJump={(changeId) => {
+2 -2
apps/desktop/src/components/KeyboardShortcutsHelp.tsx
··· 12 12 { keys: ["J / -"], description: "Jump to parent revision" }, 13 13 { keys: ["K / + / ="], description: "Jump to child revision" }, 14 14 { keys: ["@"], description: "Jump to working copy" }, 15 - { keys: ["f"], description: "Inline jump to visible revision" }, 16 - { keys: ["/"], description: "Search revision by ID prefix" }, 15 + { keys: ["f"], description: "AceJump to visible revision" }, 16 + { keys: ["/"], description: "Search revisions" }, 17 17 { keys: ["g g"], description: "Jump to first revision" }, 18 18 { keys: ["G"], description: "Jump to last revision" }, 19 19 { keys: ["Esc"], description: "Deselect" },
+26 -26
apps/desktop/src/components/revision-graph/index.tsx
··· 17 17 debugOverlayEnabledAtom, 18 18 expandedStacksAtom, 19 19 hoveredStackIdAtom, 20 - inlineJumpQueryAtom, 20 + aceJumpQueryAtom, 21 21 revisionGraphScrollTopAtom, 22 22 } from "@/atoms"; 23 23 import { ··· 362 362 } = useMemo(() => buildGraph(stableRevisions), [stableRevisions]); 363 363 const search = useSearch({ from: Route.fullPath }); 364 364 const navigate = useNavigate({ from: Route.fullPath }); 365 - const [inlineJumpQuery, setInlineJumpQuery] = useAtom(inlineJumpQueryAtom); 366 - const inlineJumpMode = inlineJumpQuery !== null; 365 + const [aceJumpQuery, setAceJumpQuery] = useAtom(aceJumpQueryAtom); 366 + const aceJumpMode = aceJumpQuery !== null; 367 367 368 368 // Detect collapsible stacks 369 369 const stacks = useMemo(() => detectStacks(stableRevisions), [stableRevisions]); ··· 671 671 displayRows, 672 672 changeIdToIndex, 673 673 selectedRevision, 674 - enabled: !inlineJumpMode, 674 + enabled: !aceJumpMode, 675 675 scrollToIndex: (index) => scrollToIndexIfNeededRef.current?.(index), 676 676 onToggleStack: handleToggleStack, 677 677 hasFocus, ··· 688 688 // Track if we just activated jump mode to ignore the same 'f' keypress 689 689 const justActivatedRef = useRef(false); 690 690 691 - // Activate inline jump mode with 'f' key 691 + // Activate AceJump mode with 'f' key 692 692 useKeyboardShortcut({ 693 693 key: "f", 694 694 modifiers: {}, 695 695 onPress: () => { 696 696 justActivatedRef.current = true; 697 - setInlineJumpQuery(""); 697 + setAceJumpQuery(""); 698 698 // Clear the flag after a short delay (same event loop tick protection) 699 699 requestAnimationFrame(() => { 700 700 justActivatedRef.current = false; 701 701 }); 702 702 }, 703 - enabled: !inlineJumpMode, 703 + enabled: !aceJumpMode, 704 704 }); 705 705 706 - // Cancel inline jump mode with Escape 706 + // Cancel AceJump mode with Escape 707 707 useKeyboardShortcut({ 708 708 key: "Escape", 709 709 modifiers: {}, 710 - onPress: () => setInlineJumpQuery(null), 711 - enabled: inlineJumpMode, 710 + onPress: () => setAceJumpQuery(null), 711 + enabled: aceJumpMode, 712 712 }); 713 713 714 714 const COLLAPSED_STACK_HEIGHT = 32; ··· 961 961 const hints = new Map<string, string>(); 962 962 const matches: Array<{ changeId: string; shortId: string }> = []; 963 963 964 - if (inlineJumpMode && revisions.length > 0) { 965 - const query = inlineJumpQuery ?? ""; 964 + if (aceJumpMode && revisions.length > 0) { 965 + const query = aceJumpQuery ?? ""; 966 966 967 967 // First, collect all visible revisions that match the current query 968 968 for (const item of virtualItems) { ··· 1010 1010 } 1011 1011 1012 1012 return { jumpHintsMap: hints, matchingRevisions: matches }; 1013 - }, [inlineJumpMode, inlineJumpQuery, revisions.length, virtualItems, rows]); 1013 + }, [aceJumpMode, aceJumpQuery, revisions.length, virtualItems, rows]); 1014 1014 1015 1015 // Store matching revisions in a ref for use in the effect 1016 1016 const matchingRevisionsRef = useRef(matchingRevisions); 1017 1017 matchingRevisionsRef.current = matchingRevisions; 1018 1018 1019 - // Handle jump hint letter key presses (DOM event subscription) 1019 + // Handle AceJump letter key presses (DOM event subscription) 1020 1020 useEffect(() => { 1021 - if (!inlineJumpMode) return; 1021 + if (!aceJumpMode) return; 1022 1022 1023 1023 function handleJumpKey(event: KeyboardEvent) { 1024 1024 const activeElement = document.activeElement; ··· 1036 1036 // Handle backspace to remove last character 1037 1037 if (event.key === "Backspace") { 1038 1038 event.preventDefault(); 1039 - const currentQuery = inlineJumpQuery ?? ""; 1039 + const currentQuery = aceJumpQuery ?? ""; 1040 1040 if (currentQuery.length > 0) { 1041 - setInlineJumpQuery(currentQuery.slice(0, -1)); 1041 + setAceJumpQuery(currentQuery.slice(0, -1)); 1042 1042 } else { 1043 - setInlineJumpQuery(null); // Cancel if already empty 1043 + setAceJumpQuery(null); // Cancel if already empty 1044 1044 } 1045 1045 return; 1046 1046 } ··· 1048 1048 // Only accept alphanumeric characters for the query 1049 1049 if (/^[a-z0-9]$/i.test(key)) { 1050 1050 event.preventDefault(); 1051 - const newQuery = (inlineJumpQuery ?? "") + key; 1051 + const newQuery = (aceJumpQuery ?? "") + key; 1052 1052 1053 1053 // Find matching revisions with the new query 1054 1054 const matches = matchingRevisionsRef.current.filter(({ shortId }) => ··· 1057 1057 1058 1058 if (matches.length === 1) { 1059 1059 // Single match - jump directly 1060 - setInlineJumpQuery(null); 1060 + setAceJumpQuery(null); 1061 1061 // Look up revision by change_id (matchingRevisions stores change_id) 1062 1062 const revision = revisions.find((r) => r.change_id === matches[0].changeId); 1063 1063 if (revision) { ··· 1065 1065 } 1066 1066 } else if (matches.length === 0) { 1067 1067 // No matches - cancel 1068 - setInlineJumpQuery(null); 1068 + setAceJumpQuery(null); 1069 1069 } else { 1070 1070 // Multiple matches - update query to filter 1071 - setInlineJumpQuery(newQuery); 1071 + setAceJumpQuery(newQuery); 1072 1072 } 1073 1073 return; 1074 1074 } 1075 1075 1076 1076 // Any other non-modifier key cancels jump mode 1077 1077 if (!["Shift", "Control", "Alt", "Meta", "CapsLock"].includes(event.key)) { 1078 - setInlineJumpQuery(null); 1078 + setAceJumpQuery(null); 1079 1079 } 1080 1080 } 1081 1081 1082 1082 window.addEventListener("keydown", handleJumpKey); 1083 1083 return () => window.removeEventListener("keydown", handleJumpKey); 1084 - }, [inlineJumpMode, inlineJumpQuery, setInlineJumpQuery, revisions, onSelectRevision]); 1084 + }, [aceJumpMode, aceJumpQuery, setAceJumpQuery, revisions, onSelectRevision]); 1085 1085 1086 1086 if (revisions.length === 0) { 1087 1087 return ( ··· 1267 1267 isFlashing={isFlashing} 1268 1268 isDimmed={isDimmed} 1269 1269 isPendingAbandon={pendingAbandon?.change_id === row.revision.change_id} 1270 - jumpModeActive={inlineJumpMode} 1271 - jumpQuery={inlineJumpQuery ?? ""} 1270 + jumpModeActive={aceJumpMode} 1271 + jumpQuery={aceJumpQuery ?? ""} 1272 1272 jumpHint={jumpHintsMap.get(row.revision.change_id) ?? null} 1273 1273 hasFocus={hasFocus} 1274 1274 />