a very good jj gui
0
fork

Configure Feed

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

feat: add ace jump shortcut (f) for quick revision navigation

Issue: TAT-52

+106 -1
+1
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);
+97
apps/desktop/src/components/AceJump.tsx
··· 1 + import { useAtom } from "@effect-atom/atom-react"; 2 + import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 3 + import { aceJumpOpenAtom } from "@/atoms"; 4 + import { useKeyboardShortcut } from "@/hooks/useKeyboard"; 5 + import type { Revision } from "@/tauri-commands"; 6 + 7 + interface AceJumpProps { 8 + revisions: Revision[]; 9 + onJump: (changeId: string) => void; 10 + } 11 + 12 + export function AceJump({ revisions, onJump }: AceJumpProps) { 13 + const [open, setOpen] = useAtom(aceJumpOpenAtom); 14 + const [query, setQuery] = useState(""); 15 + const inputRef = useRef<HTMLInputElement>(null); 16 + 17 + useKeyboardShortcut({ 18 + key: "f", 19 + onPress: () => setOpen(true), 20 + enabled: !open, 21 + }); 22 + 23 + useEffect(() => { 24 + if (open) { 25 + setQuery(""); 26 + requestAnimationFrame(() => inputRef.current?.focus()); 27 + } 28 + }, [open]); 29 + 30 + const matches = useMemo(() => { 31 + if (!query) return []; 32 + const lowerQuery = query.toLowerCase(); 33 + return revisions.filter((r) => r.change_id.toLowerCase().startsWith(lowerQuery)); 34 + }, [revisions, query]); 35 + 36 + const handleSubmit = useCallback(() => { 37 + if (matches.length === 1) { 38 + onJump(matches[0].change_id); 39 + setOpen(false); 40 + } else if (matches.length > 1) { 41 + onJump(matches[0].change_id); 42 + setOpen(false); 43 + } 44 + }, [matches, onJump, setOpen]); 45 + 46 + useEffect(() => { 47 + if (matches.length === 1 && query.length >= 2) { 48 + onJump(matches[0].change_id); 49 + setOpen(false); 50 + } 51 + }, [matches, query, onJump, setOpen]); 52 + 53 + const handleKeyDown = useCallback( 54 + (e: React.KeyboardEvent) => { 55 + if (e.key === "Escape") { 56 + e.preventDefault(); 57 + setOpen(false); 58 + } else if (e.key === "Enter") { 59 + e.preventDefault(); 60 + handleSubmit(); 61 + } 62 + }, 63 + [handleSubmit, setOpen], 64 + ); 65 + 66 + if (!open) return null; 67 + 68 + return ( 69 + <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]"> 70 + <div className="bg-background border border-border rounded-md shadow-lg p-3 min-w-[200px]"> 71 + <div className="flex items-center gap-2 mb-2"> 72 + <span className="text-xs text-muted-foreground">Jump to revision:</span> 73 + </div> 74 + <input 75 + ref={inputRef} 76 + type="text" 77 + value={query} 78 + onChange={(e) => setQuery(e.target.value)} 79 + onKeyDown={handleKeyDown} 80 + placeholder="Type change ID prefix..." 81 + className="w-full px-2 py-1 text-sm bg-muted border border-border rounded font-mono focus:outline-none focus:ring-1 focus:ring-ring" 82 + autoComplete="off" 83 + spellCheck={false} 84 + /> 85 + {query && ( 86 + <div className="mt-2 text-xs text-muted-foreground"> 87 + {matches.length === 0 && <span>No matches</span>} 88 + {matches.length === 1 && ( 89 + <span className="text-green-500">Match: {matches[0].change_id.slice(0, 12)}</span> 90 + )} 91 + {matches.length > 1 && <span>{matches.length} matches - keep typing...</span>} 92 + </div> 93 + )} 94 + </div> 95 + </div> 96 + ); 97 + }
+7 -1
apps/desktop/src/components/AppShell.tsx
··· 4 4 import { open } from "@tauri-apps/plugin-dialog"; 5 5 import { Effect } from "effect"; 6 6 import { useCallback, useMemo, useState } from "react"; 7 + import { AceJump } from "@/components/AceJump"; 7 8 import { CommandPalette } from "@/components/CommandPalette"; 8 9 import { KeyboardShortcutsHelp } from "@/components/KeyboardShortcutsHelp"; 9 10 import { reorderForGraph, RevisionGraph } from "@/components/RevisionGraph"; ··· 175 176 }, [selectedRevision, projectId, triggerFlash]); 176 177 177 178 useKeySequence({ sequence: "yy", onTrigger: handleYankId, enabled: !!selectedRevision }); 178 - useKeySequence({ sequence: "yY", onTrigger: handleYankLink, enabled: !!selectedRevision && !!projectId }); 179 + useKeySequence({ 180 + sequence: "yY", 181 + onTrigger: handleYankLink, 182 + enabled: !!selectedRevision && !!projectId, 183 + }); 179 184 180 185 const closestBookmark = useMemo(() => { 181 186 const workingCopy = revisions.find((r) => r.is_working_copy); ··· 220 225 onOpenRepo={handleOpenRepo} 221 226 /> 222 227 <KeyboardShortcutsHelp /> 228 + <AceJump revisions={orderedRevisions} onJump={handleNavigateToChangeId} /> 223 229 <div className="flex flex-col h-screen overflow-hidden"> 224 230 <Toolbar repoPath={activeProject?.path ?? null} /> 225 231 <section className="flex-1 min-h-0" aria-label="Revision list">
+1
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: "Jump to revision by ID prefix" }, 15 16 { keys: ["g g"], description: "Jump to first revision" }, 16 17 { keys: ["G"], description: "Jump to last revision" }, 17 18 { keys: ["Esc"], description: "Deselect" },