@@ -2,8 +2,10 @@ import React from 'react';
22
33import { DiffRange } from './codes' ;
44import { closest , copyOnlyMatching , distributeSpans } from './dom-utils' ;
5- import { addCharacterDiffs } from './char-diffs' ;
65import { stringAsLines } from './string-utils' ;
6+ import { isLegitKeypress } from '../file_diff' ;
7+ import { DiffRow } from './DiffRow' ;
8+ import { SkipRange , SkipRow } from './SkipRow' ;
79
810export interface PatchOptions {
911 /** Minimum number of skipped lines to elide into a "jump" row */
@@ -88,6 +90,38 @@ export function CodeDiff(props: Props) {
8890 ) ;
8991}
9092
93+ function moveUpDown (
94+ dir : 'up' | 'down' ,
95+ selectedLine : number | undefined ,
96+ ops : readonly DiffRange [ ] ,
97+ ) : number | undefined {
98+ if ( dir === 'up' ) {
99+ if ( selectedLine === undefined ) {
100+ return 0 ;
101+ } else {
102+ for ( const range of ops ) {
103+ const { after} = range ;
104+ const afterStart = after [ 0 ] ;
105+ if ( selectedLine < afterStart ) {
106+ return afterStart ;
107+ }
108+ }
109+ // TODO: if the last hunk was already selected, advance to the next file.
110+ }
111+ } else {
112+ if ( selectedLine !== undefined ) {
113+ for ( let i = ops . length - 1 ; i >= 0 ; i -- ) {
114+ const range = ops [ i ] ;
115+ const { after} = range ;
116+ const afterStart = after [ 0 ] ;
117+ if ( selectedLine > afterStart ) {
118+ return afterStart ;
119+ }
120+ }
121+ }
122+ }
123+ }
124+
91125interface CodeDiffViewProps {
92126 beforeLines : readonly string [ ] ;
93127 afterLines : readonly string [ ] ;
@@ -114,6 +148,7 @@ const CodeDiffView = React.memo((props: CodeDiffViewProps) => {
114148 // this will blow away all "show more lines" actions
115149 setOps ( initOps ) ;
116150 } , [ initOps ] ) ;
151+ const [ selectedLine , setSelectedLine ] = React . useState < number | undefined > ( ) ;
117152 const handleShowMore = ( existing : SkipRange , num : number ) => {
118153 setOps ( oldOps =>
119154 oldOps . flatMap ( op => {
@@ -150,6 +185,21 @@ const CodeDiffView = React.memo((props: CodeDiffViewProps) => {
150185 ) ;
151186 } ;
152187
188+ React . useEffect ( ( ) => {
189+ const handleKeydown = ( e : KeyboardEvent ) => {
190+ if ( ! isLegitKeypress ( e ) ) return ;
191+ if ( e . code !== 'KeyN' && e . code !== 'KeyP' ) return ;
192+ const newSelectedLine = moveUpDown ( e . code === 'KeyN' ? 'up' : 'down' , selectedLine , ops ) ;
193+ if ( newSelectedLine !== undefined ) {
194+ setSelectedLine ( newSelectedLine ) ;
195+ }
196+ } ;
197+ document . addEventListener ( 'keydown' , handleKeydown ) ;
198+ return ( ) => {
199+ document . removeEventListener ( 'keydown' , handleKeydown ) ;
200+ } ;
201+ } , [ ops , selectedLine ] ) ;
202+
153203 const diffRows = [ ] ;
154204 for ( const range of ops ) {
155205 const { type} = range ;
@@ -159,6 +209,7 @@ const CodeDiffView = React.memo((props: CodeDiffViewProps) => {
159209 const numRows = Math . max ( numBeforeRows , numAfterRows ) ;
160210 const beforeStartLine = before [ 0 ] ;
161211 const afterStartLine = after [ 0 ] ;
212+ const isSelected = afterStartLine === selectedLine ;
162213 if ( type == 'skip' ) {
163214 diffRows . push (
164215 < SkipRow
@@ -169,6 +220,7 @@ const CodeDiffView = React.memo((props: CodeDiffViewProps) => {
169220 header = { range . header ?? null }
170221 expandLines = { expandLines }
171222 onShowMore = { handleShowMore }
223+ isSelected = { isSelected }
172224 /> ,
173225 ) ;
174226 } else {
@@ -193,6 +245,7 @@ const CodeDiffView = React.memo((props: CodeDiffViewProps) => {
193245 beforeHTML = { beforeHTML }
194246 afterText = { afterText }
195247 afterHTML = { afterHTML }
248+ isSelected = { j === 0 && isSelected }
196249 /> ,
197250 ) ;
198251 }
@@ -237,127 +290,3 @@ const CodeDiffView = React.memo((props: CodeDiffViewProps) => {
237290 </ div >
238291 ) ;
239292} ) ;
240-
241- interface SkipRange {
242- beforeStartLine : number ;
243- afterStartLine : number ;
244- numRows : number ;
245- }
246-
247- export interface SkipRowProps extends SkipRange {
248- header : string | null ;
249- expandLines : number ;
250- /** positive num = expand down, negative num = expand up */
251- onShowMore : ( existing : SkipRange , num : number ) => void ;
252- }
253-
254- function SkipRow ( props : SkipRowProps ) {
255- const { expandLines, header, onShowMore, ...range } = props ;
256- const { numRows} = range ;
257- const showAll = ( e : React . MouseEvent ) => {
258- e . preventDefault ( ) ;
259- onShowMore ( range , numRows ) ;
260- } ;
261- const arrows =
262- numRows <= expandLines ? (
263- < span className = "skip" title = { `show ${ numRows } skipped lines` } onClick = { showAll } >
264- ↕
265- </ span >
266- ) : (
267- < >
268- < span
269- className = "skip expand-up"
270- title = { `show ${ expandLines } more lines above` }
271- onClick = { ( ) => {
272- onShowMore ( range , - expandLines ) ;
273- } } >
274- ↥
275- </ span >
276- < span
277- className = "skip expand-down"
278- title = { `show ${ expandLines } more lines below` }
279- onClick = { ( ) => {
280- onShowMore ( range , expandLines ) ;
281- } } >
282- ↧
283- </ span >
284- </ >
285- ) ;
286- const showMore = (
287- < a href = "#" onClick = { showAll } >
288- Show { numRows } more lines
289- </ a >
290- ) ;
291- const headerHTML = header ? < span className = "hunk-header" > ${ header } </ span > : '' ;
292- return (
293- < tr className = "skip-row" >
294- < td colSpan = { 4 } className = "skip code" >
295- < span className = "arrows-left" > { arrows } </ span >
296- { showMore } { headerHTML }
297- < span className = "arrows-right" > { arrows } </ span >
298- </ td >
299- </ tr >
300- ) ;
301- }
302-
303- // TODO: factor out a {text, html} type
304- interface DiffRowProps {
305- type : DiffRange [ 'type' ] ;
306- beforeLineNum : number | null ;
307- afterLineNum : number | null ;
308- beforeText : string | undefined ;
309- beforeHTML ?: string ;
310- afterText : string | undefined ;
311- afterHTML ?: string ;
312- }
313-
314- function escapeHtml ( unsafe : string ) {
315- return unsafe
316- . replaceAll ( '&' , '&' )
317- . replaceAll ( '<' , '<' )
318- . replaceAll ( '>' , '>' )
319- . replaceAll ( '"' , '"' )
320- . replaceAll ( "'" , ''' ) ;
321- }
322-
323- const makeCodeTd = ( type : string , text : string | undefined , html : string | undefined ) => {
324- if ( text === undefined ) {
325- return { text : '' , html : '' , className : 'empty code' } ;
326- }
327- if ( html === undefined ) {
328- html = escapeHtml ( text ) ;
329- }
330- text = text . replaceAll ( '\t' , '\u00a0\u00a0\u00a0\u00a0' ) ;
331- html = html . replaceAll ( '\t' , '\u00a0\u00a0\u00a0\u00a0' ) ;
332- const className = 'code ' + type ;
333- return { className, html, text} ;
334- } ;
335-
336- function DiffRow ( props : DiffRowProps ) {
337- const { beforeLineNum, afterLineNum, type} = props ;
338- const cells = [
339- makeCodeTd ( type , props . beforeText , props . beforeHTML ) ,
340- makeCodeTd ( type , props . afterText , props . afterHTML ) ,
341- ] ;
342- let [ beforeHtml , afterHtml ] = [ cells [ 0 ] . html , cells [ 1 ] . html ] ;
343- if ( type === 'replace' ) {
344- [ beforeHtml , afterHtml ] = addCharacterDiffs (
345- cells [ 0 ] . text ,
346- cells [ 0 ] . html ,
347- cells [ 1 ] . text ,
348- cells [ 1 ] . html ,
349- ) ;
350- }
351- return (
352- < tr >
353- < td className = "line-no" > { beforeLineNum ?? '' } </ td >
354- < td
355- className = { cells [ 0 ] . className + ' before' }
356- dangerouslySetInnerHTML = { { __html : beforeHtml } } > </ td >
357- < td
358- className = { cells [ 1 ] . className + ' after' }
359- dangerouslySetInnerHTML = { { __html : afterHtml } } > </ td >
360- < td className = "line-no" > { afterLineNum ?? '' } </ td >
361- </ tr >
362- ) ;
363- }
0 commit comments