@@ -806,6 +806,7 @@ export class PRReviewStore {
806806 } ;
807807
808808 // Switch between left (old) and right (new) sides in split view
809+ // This uses the same split-pair logic as the UI to navigate like a grid
809810 navigateSide = ( direction : "left" | "right" ) => {
810811 const {
811812 focusedLine,
@@ -826,48 +827,90 @@ export class PRReviewStore {
826827 const diff = selectedFile ? loadedDiffs [ selectedFile ] : null ;
827828 if ( ! diff ?. hunks ) return ;
828829
829- // Helper to check if a line matches and handle switching
830- const trySwitch = ( line : DiffLine ) : boolean => {
831- // Check if this is the line we're currently focused on
832- const matchesOld =
833- focusedLineSide === "old" && line . oldLineNumber === focusedLine ;
834- const matchesNew =
835- focusedLineSide === "new" && line . newLineNumber === focusedLine ;
836-
837- if ( ! matchesOld && ! matchesNew ) return false ;
838-
839- // Only context/merged lines (type "normal") can be on both sides
840- // Delete lines only exist on old side, insert lines only on new side
841- if ( line . type !== "normal" ) return true ; // Found but can't switch
842-
843- // Get the line number for the target side
844- const targetLineNum =
845- targetSide === "old" ? line . oldLineNumber : line . newLineNumber ;
846- if ( targetLineNum !== undefined ) {
847- this . setFocusedLine ( targetLineNum , targetSide ) ;
848- this . setSelectionAnchor ( null , null ) ;
830+ // Collect all lines from hunks, substituting expanded skip blocks
831+ const allLines : DiffLine [ ] = [ ] ;
832+ let skipIndex = 0 ;
833+ for ( const hunk of diff . hunks ) {
834+ if ( hunk . type === "skip" ) {
835+ const key = `${ selectedFile } :${ skipIndex } ` ;
836+ const expandedLines = expandedSkipBlocks [ key ] ;
837+ if ( expandedLines ) {
838+ allLines . push ( ...expandedLines ) ;
839+ }
840+ skipIndex ++ ;
841+ } else if ( hunk . type === "hunk" ) {
842+ allLines . push ( ...hunk . lines ) ;
849843 }
850- return true ;
844+ }
845+
846+ // Convert to split pairs (same logic as convertToSplitPairs in pr-review.tsx)
847+ type SplitPair = {
848+ left : DiffLine | null ;
849+ right : DiffLine | null ;
851850 } ;
851+ const pairs : SplitPair [ ] = [ ] ;
852+ let i = 0 ;
853+
854+ while ( i < allLines . length ) {
855+ const line = allLines [ i ] ;
856+
857+ if ( line . type === "normal" ) {
858+ // Context line - show on both sides
859+ pairs . push ( { left : line , right : line } ) ;
860+ i ++ ;
861+ } else if ( line . type === "delete" ) {
862+ // Collect consecutive deletes
863+ const deletes : DiffLine [ ] = [ ] ;
864+ while ( i < allLines . length && allLines [ i ] . type === "delete" ) {
865+ deletes . push ( allLines [ i ] ) ;
866+ i ++ ;
867+ }
852868
853- // Search in regular hunks
854- for ( const hunk of diff . hunks ) {
855- if ( hunk . type !== "hunk" ) continue ;
856- for ( const line of hunk . lines ) {
857- if ( trySwitch ( line ) ) return ;
869+ // Collect consecutive inserts that follow
870+ const inserts : DiffLine [ ] = [ ] ;
871+ while ( i < allLines . length && allLines [ i ] . type === "insert" ) {
872+ inserts . push ( allLines [ i ] ) ;
873+ i ++ ;
874+ }
875+
876+ // Pair them up
877+ const maxLen = Math . max ( deletes . length , inserts . length ) ;
878+ for ( let j = 0 ; j < maxLen ; j ++ ) {
879+ pairs . push ( {
880+ left : deletes [ j ] || null ,
881+ right : inserts [ j ] || null ,
882+ } ) ;
883+ }
884+ } else if ( line . type === "insert" ) {
885+ // Standalone insert (no preceding delete)
886+ pairs . push ( { left : null , right : line } ) ;
887+ i ++ ;
858888 }
859889 }
860890
861- // Also search in expanded skip blocks
862- if ( selectedFile ) {
863- for ( const key of Object . keys ( expandedSkipBlocks ) ) {
864- if ( ! key . startsWith ( `${ selectedFile } :` ) ) continue ;
865- const expandedLines = expandedSkipBlocks [ key ] ;
866- if ( expandedLines ) {
867- for ( const line of expandedLines ) {
868- if ( trySwitch ( line ) ) return ;
891+ // Find the pair containing our currently focused line
892+ for ( const pair of pairs ) {
893+ const matchesLeft =
894+ focusedLineSide === "old" && pair . left ?. oldLineNumber === focusedLine ;
895+ const matchesRight =
896+ focusedLineSide === "new" && pair . right ?. newLineNumber === focusedLine ;
897+
898+ if ( matchesLeft || matchesRight ) {
899+ // Found the pair - switch to the other side if it exists
900+ if ( direction === "left" && pair . left ) {
901+ const targetLineNum = pair . left . oldLineNumber ;
902+ if ( targetLineNum !== undefined ) {
903+ this . setFocusedLine ( targetLineNum , "old" ) ;
904+ this . setSelectionAnchor ( null , null ) ;
905+ }
906+ } else if ( direction === "right" && pair . right ) {
907+ const targetLineNum = pair . right . newLineNumber ;
908+ if ( targetLineNum !== undefined ) {
909+ this . setFocusedLine ( targetLineNum , "new" ) ;
910+ this . setSelectionAnchor ( null , null ) ;
869911 }
870912 }
913+ return ;
871914 }
872915 }
873916 } ;
@@ -1188,7 +1231,209 @@ export class PRReviewStore {
11881231 }
11891232 }
11901233
1191- // Normal line/skip navigation - find current position in navigableItems
1234+ // In split view, navigate through visual rows while staying on the same side
1235+ const { diffViewMode } = this . state ;
1236+ if ( diffViewMode === "split" && focusedLine !== null && focusedLineSide ) {
1237+ // Collect all lines for split pair computation
1238+ const allLines : DiffLine [ ] = [ ] ;
1239+ let skipIdx = 0 ;
1240+ const skipBlockIndices : { pairIdx : number ; skipIndex : number } [ ] = [ ] ;
1241+
1242+ for ( const hunk of diff . hunks ) {
1243+ if ( hunk . type === "skip" ) {
1244+ const key = `${ selectedFile } :${ skipIdx } ` ;
1245+ const expandedLines = expandedSkipBlocks [ key ] ;
1246+ if ( expandedLines ) {
1247+ allLines . push ( ...expandedLines ) ;
1248+ } else {
1249+ // Mark where skip block would appear in pairs
1250+ skipBlockIndices . push ( {
1251+ pairIdx : allLines . length , // Will be adjusted after pair conversion
1252+ skipIndex : skipIdx ,
1253+ } ) ;
1254+ }
1255+ skipIdx ++ ;
1256+ } else if ( hunk . type === "hunk" ) {
1257+ allLines . push ( ...hunk . lines ) ;
1258+ }
1259+ }
1260+
1261+ // Convert to split pairs
1262+ type SplitPair = {
1263+ type : "pair" ;
1264+ left : DiffLine | null ;
1265+ right : DiffLine | null ;
1266+ } ;
1267+ type SplitSkip = { type : "skip" ; skipIndex : number } ;
1268+ type SplitItem = SplitPair | SplitSkip ;
1269+
1270+ const pairs : SplitItem [ ] = [ ] ;
1271+ let i = 0 ;
1272+ let lineIdx = 0 ;
1273+
1274+ // Insert skip blocks at correct positions
1275+ const getSkipAtLineIdx = ( idx : number ) =>
1276+ skipBlockIndices . find ( ( s ) => s . pairIdx === idx ) ;
1277+
1278+ while ( i < allLines . length ) {
1279+ // Check if there's a skip block before this line
1280+ const skipHere = getSkipAtLineIdx ( lineIdx ) ;
1281+ if ( skipHere ) {
1282+ pairs . push ( { type : "skip" , skipIndex : skipHere . skipIndex } ) ;
1283+ // Remove from tracking
1284+ skipBlockIndices . splice ( skipBlockIndices . indexOf ( skipHere ) , 1 ) ;
1285+ }
1286+
1287+ const line = allLines [ i ] ;
1288+ lineIdx = i ;
1289+
1290+ if ( line . type === "normal" ) {
1291+ pairs . push ( { type : "pair" , left : line , right : line } ) ;
1292+ i ++ ;
1293+ } else if ( line . type === "delete" ) {
1294+ const deletes : DiffLine [ ] = [ ] ;
1295+ while ( i < allLines . length && allLines [ i ] . type === "delete" ) {
1296+ deletes . push ( allLines [ i ] ) ;
1297+ i ++ ;
1298+ }
1299+ const inserts : DiffLine [ ] = [ ] ;
1300+ while ( i < allLines . length && allLines [ i ] . type === "insert" ) {
1301+ inserts . push ( allLines [ i ] ) ;
1302+ i ++ ;
1303+ }
1304+ const maxLen = Math . max ( deletes . length , inserts . length ) ;
1305+ for ( let j = 0 ; j < maxLen ; j ++ ) {
1306+ pairs . push ( {
1307+ type : "pair" ,
1308+ left : deletes [ j ] || null ,
1309+ right : inserts [ j ] || null ,
1310+ } ) ;
1311+ }
1312+ } else if ( line . type === "insert" ) {
1313+ pairs . push ( { type : "pair" , left : null , right : line } ) ;
1314+ i ++ ;
1315+ }
1316+ }
1317+
1318+ // Add any remaining skip blocks at the end
1319+ for ( const skip of skipBlockIndices ) {
1320+ pairs . push ( { type : "skip" , skipIndex : skip . skipIndex } ) ;
1321+ }
1322+
1323+ // Find current pair index
1324+ const currentPairIdx = pairs . findIndex ( ( item ) => {
1325+ if ( item . type === "skip" ) return false ;
1326+ if ( focusedLineSide === "old" ) {
1327+ return item . left ?. oldLineNumber === focusedLine ;
1328+ } else {
1329+ return item . right ?. newLineNumber === focusedLine ;
1330+ }
1331+ } ) ;
1332+
1333+ if ( currentPairIdx !== - 1 ) {
1334+ let nextPairIdx : number ;
1335+ if ( direction === "down" ) {
1336+ nextPairIdx = Math . min ( currentPairIdx + jumpCount , pairs . length - 1 ) ;
1337+ } else {
1338+ nextPairIdx = Math . max ( currentPairIdx - jumpCount , 0 ) ;
1339+ }
1340+
1341+ const nextPair = pairs [ nextPairIdx ] ;
1342+
1343+ // If next is a skip block, focus it
1344+ if ( nextPair . type === "skip" ) {
1345+ this . set ( {
1346+ focusedSkipBlockIndex : nextPair . skipIndex ,
1347+ focusedLine : null ,
1348+ focusedLineSide : null ,
1349+ selectionAnchor : null ,
1350+ selectionAnchorSide : null ,
1351+ focusedCommentId : null ,
1352+ focusedPendingCommentId : null ,
1353+ } ) ;
1354+ return ;
1355+ }
1356+
1357+ // Try to stay on the same side
1358+ let nextLine : number | undefined ;
1359+ let nextSide = focusedLineSide ;
1360+
1361+ if ( focusedLineSide === "old" && nextPair . left ) {
1362+ nextLine = nextPair . left . oldLineNumber ;
1363+ } else if ( focusedLineSide === "new" && nextPair . right ) {
1364+ nextLine = nextPair . right . newLineNumber ;
1365+ } else if ( nextPair . left ) {
1366+ // Fallback to left side if preferred side not available
1367+ nextLine = nextPair . left . oldLineNumber ;
1368+ nextSide = "old" ;
1369+ } else if ( nextPair . right ) {
1370+ // Fallback to right side
1371+ nextLine = nextPair . right . newLineNumber ;
1372+ nextSide = "new" ;
1373+ }
1374+
1375+ if ( nextLine !== undefined ) {
1376+ // Handle up navigation - check for comments
1377+ if (
1378+ direction === "up" &&
1379+ ( nextLine !== focusedLine || nextSide !== focusedLineSide )
1380+ ) {
1381+ const targetLineComments = getLineComments ( nextLine ) ;
1382+ if ( targetLineComments . length > 0 ) {
1383+ this . set ( {
1384+ focusedCommentId :
1385+ targetLineComments [ targetLineComments . length - 1 ] . id ,
1386+ focusedLine : null ,
1387+ focusedLineSide : null ,
1388+ focusedSkipBlockIndex : null ,
1389+ selectionAnchor : null ,
1390+ selectionAnchorSide : null ,
1391+ } ) ;
1392+ return ;
1393+ }
1394+ const targetLinePending = getLinePendingComments ( nextLine ) ;
1395+ if ( targetLinePending . length > 0 ) {
1396+ this . set ( {
1397+ focusedPendingCommentId :
1398+ targetLinePending [ targetLinePending . length - 1 ] . id ,
1399+ focusedLine : null ,
1400+ focusedLineSide : null ,
1401+ focusedSkipBlockIndex : null ,
1402+ selectionAnchor : null ,
1403+ selectionAnchorSide : null ,
1404+ } ) ;
1405+ return ;
1406+ }
1407+ }
1408+
1409+ if ( withShift ) {
1410+ this . set ( {
1411+ focusedLine : nextLine ,
1412+ focusedLineSide : nextSide ,
1413+ selectionAnchor : selectionAnchor ?? focusedLine ?? nextLine ,
1414+ selectionAnchorSide :
1415+ selectionAnchorSide ?? focusedLineSide ?? nextSide ,
1416+ focusedSkipBlockIndex : null ,
1417+ focusedCommentId : null ,
1418+ focusedPendingCommentId : null ,
1419+ } ) ;
1420+ } else {
1421+ this . set ( {
1422+ focusedLine : nextLine ,
1423+ focusedLineSide : nextSide ,
1424+ selectionAnchor : null ,
1425+ selectionAnchorSide : null ,
1426+ focusedSkipBlockIndex : null ,
1427+ focusedCommentId : null ,
1428+ focusedPendingCommentId : null ,
1429+ } ) ;
1430+ }
1431+ return ;
1432+ }
1433+ }
1434+ }
1435+
1436+ // Normal line/skip navigation (unified view or fallback)
11921437 const currentIdx =
11931438 focusedLine !== null
11941439 ? navigableItems . findIndex (
0 commit comments