@@ -1129,7 +1129,9 @@ export interface ConciseDiffViewProps<K> {
11291129 jumpToSelection ?: boolean ;
11301130}
11311131
1132- export type ConciseDiffViewStateProps < K > = ReadableBoxedValues < {
1132+ export type ConciseDiffViewStateProps < K > = {
1133+ rootElementId : string ;
1134+ } & ReadableBoxedValues < {
11331135 patch : StructuredPatch ;
11341136
11351137 syntaxHighlighting : boolean ;
@@ -1153,6 +1155,10 @@ export class ConciseDiffViewState<K> {
11531155
11541156 private readonly props : ConciseDiffViewStateProps < K > ;
11551157
1158+ // Drag selection state
1159+ private dragState : { hunk : DiffViewerPatchHunk ; hunkIdx : number ; line : PatchLine ; lineIdx : number ; didMove : boolean } | null = null ;
1160+ private suppressNextClick = false ;
1161+
11561162 constructor ( props : ConciseDiffViewStateProps < K > ) {
11571163 this . props = props ;
11581164
@@ -1255,92 +1261,174 @@ export class ConciseDiffViewState<K> {
12551261 }
12561262
12571263 const destroyClick = on ( element , "click" , ( e ) => {
1264+ // Only handle click if we didn't just finish dragging
1265+ if ( this . suppressNextClick ) {
1266+ this . suppressNextClick = false ;
1267+ return ;
1268+ }
12581269 this . updateSelection ( hunk , hunkIdx , line , lineIdx , e . shiftKey ) ;
12591270 } ) ;
1271+
1272+ const destroyPointerDown = on ( element , "pointerdown" , ( e : PointerEvent ) => {
1273+ // Only start drag on left click without shift key
1274+ if ( e . button === 0 && ! e . shiftKey ) {
1275+ this . startDrag ( element , e . pointerId , hunk , hunkIdx , line , lineIdx ) ;
1276+ }
1277+ } ) ;
1278+
12601279 return ( ) => {
12611280 destroyClick ( ) ;
1281+ destroyPointerDown ( ) ;
12621282 } ;
12631283 } ;
12641284 }
12651285
1266- updateSelection ( hunk : DiffViewerPatchHunk , hunkIdx : number , line : PatchLine , lineIdx : number , shift : boolean ) {
1267- const existingSelection = this . props . selection . current ;
1286+ private startDrag ( element : HTMLElement , pointerId : number , hunk : DiffViewerPatchHunk , hunkIdx : number , line : PatchLine , lineIdx : number ) {
1287+ this . dragState = { hunk, hunkIdx, line, lineIdx, didMove : false } ;
1288+
1289+ // Set initial selection
1290+ this . props . selection . current = {
1291+ hunk : hunkIdx ,
1292+ start : this . createLineRef ( line , lineIdx ) ,
1293+ end : this . createLineRef ( line , lineIdx ) ,
1294+ } ;
1295+
1296+ // Capture pointer events to this element
1297+ element . setPointerCapture ( pointerId ) ;
1298+
1299+ const abortController = new AbortController ( ) ;
1300+ const { signal } = abortController ;
1301+
1302+ on (
1303+ element ,
1304+ "pointermove" ,
1305+ ( e : PointerEvent ) => {
1306+ if ( ! this . dragState ) return ;
1307+
1308+ // Get the root element for this diff view
1309+ const rootElement = document . getElementById ( this . props . rootElementId ) ;
1310+ if ( ! rootElement ) return ;
12681311
1269- const clicked : LineRef = {
1312+ // Get the element at the pointer position
1313+ const elementAtPoint = document . elementFromPoint ( e . clientX , e . clientY ) ;
1314+ if ( ! elementAtPoint ) return ;
1315+
1316+ // Only process if the element is within this diff view's root element
1317+ if ( ! rootElement . contains ( elementAtPoint ) ) return ;
1318+
1319+ const lineElement = elementAtPoint . closest ( "[data-hunk-idx][data-line-idx]" ) as HTMLElement | null ;
1320+ if ( ! lineElement ) return ;
1321+
1322+ const currentHunkIdx = Number ( lineElement . dataset . hunkIdx ) ;
1323+ const currentLineIdx = Number ( lineElement . dataset . lineIdx ) ;
1324+
1325+ // Only allow dragging within the same hunk
1326+ if ( currentHunkIdx !== this . dragState . hunkIdx || ! Number . isFinite ( currentHunkIdx ) || ! Number . isFinite ( currentLineIdx ) ) {
1327+ return ;
1328+ }
1329+
1330+ if ( this . dragState ) {
1331+ this . dragState . didMove = true ;
1332+ }
1333+ this . updateDragSelection ( currentLineIdx ) ;
1334+ } ,
1335+ { signal } ,
1336+ ) ;
1337+
1338+ const onDragEnd = ( e : PointerEvent ) => {
1339+ element . releasePointerCapture ( e . pointerId ) ;
1340+ abortController . abort ( ) ;
1341+
1342+ // Suppress the click event only if we actually moved during the drag
1343+ if ( this . dragState ?. didMove ) {
1344+ this . suppressNextClick = true ;
1345+ }
1346+ this . dragState = null ;
1347+ } ;
1348+
1349+ on ( element , "pointerup" , onDragEnd , { signal } ) ;
1350+ on ( element , "pointercancel" , onDragEnd , { signal } ) ;
1351+ }
1352+
1353+ private createLineRef ( line : PatchLine , lineIdx : number ) : LineRef {
1354+ return {
12701355 idx : lineIdx ,
12711356 no : line . newLineNo ?? line . oldLineNo ! ,
12721357 new : line . newLineNo !== undefined ,
12731358 } ;
1359+ }
12741360
1275- // New selection
1276- if ( ! shift || existingSelection === undefined || existingSelection . hunk !== hunkIdx ) {
1277- this . props . selection . current = {
1278- hunk : hunkIdx ,
1279- start : clicked ,
1280- end : clicked ,
1281- } ;
1361+ private updateDragSelection ( currentLineIdx : number ) {
1362+ if ( ! this . dragState ) return ;
1363+
1364+ const { hunk, hunkIdx, lineIdx : startIdx } = this . dragState ;
1365+ const currentLine = hunk . lines [ currentLineIdx ] ;
1366+
1367+ if ( currentLine . type === PatchLineType . SPACER || currentLine . type === PatchLineType . HEADER ) {
1368+ return ;
1369+ }
1370+
1371+ const minIdx = Math . min ( startIdx , currentLineIdx ) ;
1372+ const maxIdx = Math . max ( startIdx , currentLineIdx ) ;
1373+
1374+ this . props . selection . current = {
1375+ hunk : hunkIdx ,
1376+ start : this . createLineRef ( hunk . lines [ minIdx ] , minIdx ) ,
1377+ end : this . createLineRef ( hunk . lines [ maxIdx ] , maxIdx ) ,
1378+ } ;
1379+ }
1380+
1381+ updateSelection ( hunk : DiffViewerPatchHunk , hunkIdx : number , line : PatchLine , lineIdx : number , shift : boolean ) {
1382+ const existingSelection = this . props . selection . current ;
1383+ const clicked = this . createLineRef ( line , lineIdx ) ;
1384+
1385+ // New selection (no shift or different hunk)
1386+ if ( ! shift || ! existingSelection || existingSelection . hunk !== hunkIdx ) {
1387+ this . props . selection . current = { hunk : hunkIdx , start : clicked , end : clicked } ;
12821388 return ;
12831389 }
12841390
1285- // Shift click idx == start == end : clear selection
1391+ // Shift click on single-line selection : clear selection
12861392 if ( existingSelection . start . idx === existingSelection . end . idx && lineIdx === existingSelection . start . idx ) {
12871393 this . props . selection . current = undefined ;
12881394 return ;
12891395 }
12901396
12911397 // Shift click outside selection: expand selection
12921398 if ( lineIdx < existingSelection . start . idx ) {
1293- this . props . selection . current = {
1294- ...existingSelection ,
1295- start : clicked ,
1296- } ;
1399+ this . props . selection . current = { ...existingSelection , start : clicked } ;
12971400 return ;
12981401 }
12991402 if ( lineIdx > existingSelection . end . idx ) {
1300- this . props . selection . current = {
1301- ...existingSelection ,
1302- end : clicked ,
1303- } ;
1403+ this . props . selection . current = { ...existingSelection , end : clicked } ;
13041404 return ;
13051405 }
13061406
1307- // Shift click inside selection: shrink closest side (start/end) of selection to exclude clicked
1407+ // Shift click inside selection: shrink closest side
13081408 const distToStart = lineIdx - existingSelection . start . idx ;
13091409 const distToEnd = existingSelection . end . idx - lineIdx ;
13101410
13111411 if ( distToStart <= distToEnd ) {
13121412 // Shrink from start: move start to line after clicked
13131413 const newStartIdx = lineIdx + 1 ;
13141414 if ( newStartIdx > existingSelection . end . idx ) {
1315- // Selection would be empty, clear it
13161415 this . props . selection . current = undefined ;
13171416 return ;
13181417 }
1319- const newStartLine = hunk . lines [ newStartIdx ] ;
13201418 this . props . selection . current = {
13211419 ...existingSelection ,
1322- start : {
1323- idx : newStartIdx ,
1324- no : newStartLine . newLineNo ?? newStartLine . oldLineNo ! ,
1325- new : newStartLine . newLineNo !== undefined ,
1326- } ,
1420+ start : this . createLineRef ( hunk . lines [ newStartIdx ] , newStartIdx ) ,
13271421 } ;
13281422 } else {
13291423 // Shrink from end: move end to line before clicked
13301424 const newEndIdx = lineIdx - 1 ;
13311425 if ( newEndIdx < existingSelection . start . idx ) {
1332- // Selection would be empty, clear it
13331426 this . props . selection . current = undefined ;
13341427 return ;
13351428 }
1336- const newEndLine = hunk . lines [ newEndIdx ] ;
13371429 this . props . selection . current = {
13381430 ...existingSelection ,
1339- end : {
1340- idx : newEndIdx ,
1341- no : newEndLine . newLineNo ?? newEndLine . oldLineNo ! ,
1342- new : newEndLine . newLineNo !== undefined ,
1343- } ,
1431+ end : this . createLineRef ( hunk . lines [ newEndIdx ] , newEndIdx ) ,
13441432 } ;
13451433 }
13461434 }
0 commit comments