@@ -122,6 +122,8 @@ const MAX_HIGHLIGHTED_DIFF_LINES = 4000;
122122const ACTIVE_LINE_OUTLINE = "1px solid hsl(from var(--color-review-accent) h s l / 0.45)" ;
123123const LIKE_NOTE_PREFIX = "I like this change" ;
124124const DISLIKE_NOTE_PREFIX = "I don't like this change" ;
125+ const EMPTY_REVIEWS : Review [ ] = [ ] ;
126+ const EMPTY_COMMENT_LINE_INDICES = new Set < number > ( ) ;
125127
126128function getFileBaseName ( filePath : string ) : string {
127129 const segments = filePath . split ( / [ \\ / ] / ) ;
@@ -392,25 +394,38 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
392394 onMarkFileAsRead,
393395 onExit,
394396 onReviewNote,
397+ isRead,
395398 isTouchImmersive,
396399 } = props ;
397400 const isTouchExperience = isTouchImmersive === true ;
398401
399402 // Flatten file tree into ordered file list
400403 const fileList = useMemo ( ( ) => flattenFileTreeLeaves ( fileTree ) , [ fileTree ] ) ;
401- const reviewedHunkCount = allHunks . filter ( ( item ) => props . isRead ( item . id ) ) . length ;
402- // Weight immersive progress by changed LoC so a large hunk moves the bar more than a one-line nit.
403- const totalChangedLineCount = allHunks . reduce (
404- ( count , hunk ) => count + getChangedLineCount ( hunk ) ,
405- 0
406- ) ;
407- const reviewedChangedLineCount = allHunks . reduce ( ( count , hunk ) => {
408- if ( ! props . isRead ( hunk . id ) ) {
409- return count ;
404+ const reviewProgress = useMemo ( ( ) => {
405+ // Cursor movement should stay lightweight even in large diff-heavy files, so memoize
406+ // the per-hunk diff parsing instead of rescanning every hunk on each immersive render.
407+ let reviewedHunkCount = 0 ;
408+ let totalChangedLineCount = 0 ;
409+ let reviewedChangedLineCount = 0 ;
410+
411+ for ( const hunk of allHunks ) {
412+ const changedLineCount = getChangedLineCount ( hunk ) ;
413+ totalChangedLineCount += changedLineCount ;
414+ if ( isRead ( hunk . id ) ) {
415+ reviewedHunkCount += 1 ;
416+ reviewedChangedLineCount += changedLineCount ;
417+ }
410418 }
411419
412- return count + getChangedLineCount ( hunk ) ;
413- } , 0 ) ;
420+ return {
421+ reviewedHunkCount,
422+ totalChangedLineCount,
423+ reviewedChangedLineCount,
424+ } ;
425+ } , [ allHunks , isRead ] ) ;
426+ const reviewedHunkCount = reviewProgress . reviewedHunkCount ;
427+ const totalChangedLineCount = reviewProgress . totalChangedLineCount ;
428+ const reviewedChangedLineCount = reviewProgress . reviewedChangedLineCount ;
414429 const reviewCompletionWidthPercent =
415430 totalChangedLineCount === 0 ? 0 : ( reviewedChangedLineCount / totalChangedLineCount ) * 100 ;
416431 const reviewCompletionPercent = Math . round ( reviewCompletionWidthPercent ) ;
@@ -646,17 +661,26 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
646661 } ) ,
647662 [ props . reviewsByFilePath ]
648663 ) ;
664+ const activeFileReviews = useMemo (
665+ ( ) =>
666+ activeFilePath
667+ ? ( props . reviewsByFilePath . get ( activeFilePath ) ?? EMPTY_REVIEWS )
668+ : EMPTY_REVIEWS ,
669+ [ activeFilePath , props . reviewsByFilePath ]
670+ ) ;
649671
650- // Map review line ranges → diff line indices for minimap comment indicators
651- const commentLineIndices : ReadonlySet < number > = ( ( ) => {
652- if ( ! activeFilePath || overlayData . content . length === 0 ) return new Set < number > ( ) ;
653- const reviews = props . reviewsByFilePath . get ( activeFilePath ) ;
654- if ( ! reviews || reviews . length === 0 ) return new Set < number > ( ) ;
672+ // Map review line ranges → diff line indices for minimap comment indicators.
673+ // Memoize the line-number lookups so cursor movement does not rebuild multi-thousand-line
674+ // maps when neither the rendered overlay nor the file's review set changed.
675+ const commentLineIndices = useMemo < ReadonlySet < number > > ( ( ) => {
676+ if ( overlayData . content . length === 0 || activeFileReviews . length === 0 ) {
677+ return EMPTY_COMMENT_LINE_INDICES ;
678+ }
655679
656680 const newLineMap = buildNewLineNumberToIndexMap ( overlayData . content ) ;
657681 let oldLineMap : Map < number , number > | null = null ;
658682 const indices = new Set < number > ( ) ;
659- for ( const review of reviews ) {
683+ for ( const review of activeFileReviews ) {
660684 const parsed = parseReviewLineRange ( review . data . lineRange ) ;
661685 if ( ! parsed ) continue ;
662686
@@ -680,7 +704,7 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
680704 }
681705 }
682706 return indices ;
683- } ) ( ) ;
707+ } , [ activeFileReviews , overlayData . content ] ) ;
684708
685709 const [ inlineComposerRequest , setInlineComposerRequest ] = useState < InlineComposerRequest | null > (
686710 null
@@ -824,7 +848,7 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
824848 const activeLineIndexRef = useRef < number | null > ( null ) ;
825849 const selectedLineRangeRef = useRef < SelectedLineRange | null > ( null ) ;
826850 const selectedHunkIdRef = useRef < string | null > ( selectedHunkId ) ;
827- const isReadRef = useRef ( props . isRead ) ;
851+ const isReadRef = useRef ( isRead ) ;
828852 const onToggleReadRef = useRef ( onToggleRead ) ;
829853 const onSelectHunkRef = useRef ( onSelectHunk ) ;
830854 const allHunksRef = useRef ( allHunks ) ;
@@ -844,8 +868,8 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
844868 } , [ selectedHunkId ] ) ;
845869
846870 useEffect ( ( ) => {
847- isReadRef . current = props . isRead ;
848- } , [ props . isRead ] ) ;
871+ isReadRef . current = isRead ;
872+ } , [ isRead ] ) ;
849873
850874 useEffect ( ( ) => {
851875 onToggleReadRef . current = onToggleRead ;
@@ -1739,12 +1763,12 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
17391763 type = "button"
17401764 className = { cn (
17411765 "text-muted hover:text-read flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0 transition-colors duration-150 sm:hidden" ,
1742- props . isRead ( selectedHunk . id ) && "text-read"
1766+ isRead ( selectedHunk . id ) && "text-read"
17431767 ) }
17441768 onClick = { ( ) => handleToggleReadWithUndo ( selectedHunk . id ) }
1745- aria-label = { props . isRead ( selectedHunk . id ) ? "Mark hunk as unread" : "Mark hunk as read" }
1769+ aria-label = { isRead ( selectedHunk . id ) ? "Mark hunk as unread" : "Mark hunk as read" }
17461770 >
1747- { props . isRead ( selectedHunk . id ) ? (
1771+ { isRead ( selectedHunk . id ) ? (
17481772 < Check aria-hidden = "true" className = "h-3 w-3" />
17491773 ) : (
17501774 < Circle aria-hidden = "true" className = "h-3 w-3" />
@@ -1763,14 +1787,14 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
17631787 type = "button"
17641788 className = { cn (
17651789 "text-muted hover:text-read flex cursor-pointer items-center border-none bg-transparent p-0 transition-colors duration-150" ,
1766- props . isRead ( selectedHunk . id ) && "text-read"
1790+ isRead ( selectedHunk . id ) && "text-read"
17671791 ) }
17681792 onClick = { ( ) => handleToggleReadWithUndo ( selectedHunk . id ) }
17691793 aria-label = {
1770- props . isRead ( selectedHunk . id ) ? "Mark hunk as unread" : "Mark hunk as read"
1794+ isRead ( selectedHunk . id ) ? "Mark hunk as unread" : "Mark hunk as read"
17711795 }
17721796 >
1773- { props . isRead ( selectedHunk . id ) ? (
1797+ { isRead ( selectedHunk . id ) ? (
17741798 < Check aria-hidden = "true" className = "h-3 w-3" />
17751799 ) : (
17761800 < Circle aria-hidden = "true" className = "h-3 w-3" />
@@ -1890,9 +1914,7 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
18901914 < SelectableDiffRenderer
18911915 content = { overlayData . content }
18921916 filePath = { activeFilePath ?? currentFileHunks [ 0 ] . filePath }
1893- inlineReviews = {
1894- activeFilePath ? props . reviewsByFilePath . get ( activeFilePath ) : undefined
1895- }
1917+ inlineReviews = { activeFileReviews }
18961918 oldStart = { 1 }
18971919 newStart = { 1 }
18981920 fontSize = "11px"
0 commit comments