@@ -12,6 +12,7 @@ import { CodeAnnotation, CodeAnnotationType, SelectedLineRange } from '@plannota
1212import { useResizablePanel } from '@plannotator/ui/hooks/useResizablePanel' ;
1313import { useCodeAnnotationDraft } from '@plannotator/ui/hooks/useCodeAnnotationDraft' ;
1414import { useGitAdd } from './hooks/useGitAdd' ;
15+ import { isTypingTarget , useReviewSearch } from './hooks/useReviewSearch' ;
1516import { useEditorAnnotations } from '@plannotator/ui/hooks/useEditorAnnotations' ;
1617import { exportEditorAnnotations } from '@plannotator/ui/utils/parser' ;
1718import { ResizeHandle } from '@plannotator/ui/components/ResizeHandle' ;
@@ -113,6 +114,32 @@ const ReviewApp: React.FC = () => {
113114
114115 const identity = useMemo ( ( ) => getIdentity ( ) , [ ] ) ;
115116
117+ const clearPendingSelection = useCallback ( ( ) => {
118+ setPendingSelection ( null ) ;
119+ } , [ ] ) ;
120+
121+ const {
122+ searchQuery,
123+ isSearchOpen,
124+ activeSearchMatchId,
125+ activeSearchMatch,
126+ activeFileSearchMatches,
127+ searchMatches,
128+ searchGroups,
129+ searchInputRef,
130+ openSearch,
131+ closeSearch,
132+ clearSearch,
133+ stepSearchMatch,
134+ handleSearchInputChange,
135+ handleSelectSearchMatch,
136+ } = useReviewSearch ( {
137+ files,
138+ activeFileIndex,
139+ setActiveFileIndex,
140+ clearPendingSelection,
141+ } ) ;
142+
116143 // Auto-save code annotation drafts
117144 const { draftBanner, restoreDraft, dismissDraft } = useCodeAnnotationDraft ( {
118145 annotations,
@@ -141,10 +168,28 @@ const ReviewApp: React.FC = () => {
141168 // Global keyboard shortcuts
142169 useEffect ( ( ) => {
143170 const handleKeyDown = ( e : KeyboardEvent ) => {
144- // Escape closes modals
171+ // Cmd/Ctrl+F to focus search (only when sidebar is rendered)
172+ if ( ( e . metaKey || e . ctrlKey ) && e . key . toLowerCase ( ) === 'f' && ! isTypingTarget ( e . target ) ) {
173+ if ( files . length > 1 || gitContext ?. diffOptions ) {
174+ e . preventDefault ( ) ;
175+ openSearch ( ) ;
176+ }
177+ return ;
178+ }
179+
180+ // Enter/F3 to step through search matches
181+ if ( ( e . key === 'Enter' || e . key === 'F3' ) && searchMatches . length > 0 && ! isTypingTarget ( e . target ) ) {
182+ e . preventDefault ( ) ;
183+ stepSearchMatch ( e . shiftKey ? - 1 : 1 ) ;
184+ return ;
185+ }
186+
187+ // Escape closes modals or clears search
145188 if ( e . key === 'Escape' ) {
146189 if ( showExportModal ) {
147190 setShowExportModal ( false ) ;
191+ } else if ( searchQuery ) {
192+ clearSearch ( ) ;
148193 }
149194 }
150195 // Cmd/Ctrl+Shift+C to copy diff
@@ -157,7 +202,7 @@ const ReviewApp: React.FC = () => {
157202 window . addEventListener ( 'keydown' , handleKeyDown ) ;
158203 return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown ) ;
159204 // eslint-disable-next-line react-hooks/exhaustive-deps
160- } , [ showExportModal ] ) ;
205+ } , [ showExportModal , searchQuery , searchMatches , openSearch , stepSearchMatch , clearSearch , files , gitContext ?. diffOptions ] ) ;
161206
162207 // Get annotations for active file
163208 const activeFileAnnotations = useMemo ( ( ) => {
@@ -818,6 +863,15 @@ const ReviewApp: React.FC = () => {
818863 onSelectWorktree = { handleWorktreeSwitch }
819864 currentBranch = { gitContext ?. currentBranch }
820865 stagedFiles = { stagedFiles }
866+ searchQuery = { searchQuery }
867+ searchInputRef = { searchInputRef }
868+ onSearchChange = { handleSearchInputChange }
869+ onSearchClear = { clearSearch }
870+ searchGroups = { searchGroups }
871+ searchMatches = { searchMatches }
872+ activeSearchMatchId = { activeSearchMatchId }
873+ onSelectSearchMatch = { handleSelectSearchMatch }
874+ onStepSearchMatch = { stepSearchMatch }
821875 />
822876 < ResizeHandle { ...fileTreeResize . handleProps } />
823877 </ >
@@ -862,6 +916,10 @@ const ReviewApp: React.FC = () => {
862916 onStage = { ( ) => stageFile ( activeFile . path ) }
863917 canStage = { canStageFiles }
864918 stageError = { stageError }
919+ searchQuery = { searchQuery }
920+ searchMatches = { activeFileSearchMatches }
921+ activeSearchMatchId = { activeSearchMatchId }
922+ activeSearchMatch = { activeSearchMatch ?. filePath === activeFile . path ? activeSearchMatch : null }
865923 />
866924 ) : (
867925 < div className = "h-full flex items-center justify-center" >
0 commit comments