@@ -25,6 +25,7 @@ import {
2525import './styles.css' ;
2626import { Search } from "./search" ;
2727import { computeGitChanges , DiffInfo } from "./diff" ;
28+ import { getGapElementData } from "./renderer/DiffRenderer" ;
2829
2930export interface EditorSettings {
3031 lineHeight : number ;
@@ -36,6 +37,8 @@ export interface EditorOptions {
3637 column ?: number ;
3738 theme ?: any ;
3839 readOnly ?: boolean ;
40+ focusedDiffEnabled ?: boolean ;
41+ focusedDiffContextLines ?: number ;
3942}
4043
4144export interface EditorState {
@@ -89,6 +92,8 @@ export class AnycodeEditor {
8992 private search : Search = new Search ( ) ;
9093
9194 private diffEnabled : boolean = false ;
95+ private focusedDiffEnabled : boolean ;
96+ private focusedDiffContextLines : number ;
9297 private originalCode ?: string ;
9398 private diffs ?: Map < number , DiffInfo > ;
9499 private readonly readOnly : boolean ;
@@ -101,6 +106,8 @@ export class AnycodeEditor {
101106 ) {
102107 this . code = new Code ( initialText , filename , language ) ;
103108 this . readOnly = options . readOnly ?? false ;
109+ this . focusedDiffEnabled = options . focusedDiffEnabled ?? false ;
110+ this . focusedDiffContextLines = Math . max ( 0 , options . focusedDiffContextLines ?? 3 ) ;
104111 // Set initial cursor position
105112 if ( options . line !== undefined && options . column !== undefined ) {
106113 this . offset = this . code . getOffset ( options . line , options . column ) ;
@@ -122,6 +129,7 @@ export class AnycodeEditor {
122129 addCssToDocument ( css , 'anyeditor-theme' ) ;
123130 this . createDomElements ( ) ;
124131 this . renderer = new Renderer ( this . container , this . buttonsColumn , this . gutter , this . codeContent ) ;
132+ this . renderer . setFocusedDiffMode ( this . focusedDiffEnabled , this . focusedDiffContextLines ) ;
125133 }
126134
127135 private createDomElements ( ) {
@@ -173,11 +181,7 @@ export class AnycodeEditor {
173181
174182 public setText ( newText : string ) {
175183 this . code . setContent ( newText ) ;
176- if ( this . diffEnabled && this . originalCode !== undefined ) {
177- this . diffs = computeGitChanges ( this . originalCode , newText ) ;
178- } else {
179- this . diffs = undefined ;
180- }
184+ this . recomputeDiffs ( ) ;
181185 }
182186
183187 public updateTextIncremental ( newText : string ) {
@@ -220,12 +224,7 @@ export class AnycodeEditor {
220224 this . selection = null ;
221225 this . offset = Math . min ( this . offset , this . code . getContentLength ( ) ) ;
222226
223- if ( this . diffEnabled && this . originalCode !== undefined ) {
224- const updatedText = this . code . getContent ( ) ;
225- this . diffs = computeGitChanges ( this . originalCode , updatedText ) ;
226- } else {
227- this . diffs = undefined ;
228- }
227+ this . recomputeDiffs ( ) ;
229228
230229 if ( this . search . isActive ( ) ) {
231230 const matches = this . code . search ( this . search . getPattern ( ) ) ;
@@ -395,6 +394,7 @@ export class AnycodeEditor {
395394
396395 this . handleClick = this . handleClick . bind ( this ) ;
397396 this . codeContent . addEventListener ( 'click' , this . handleClick ) ;
397+ this . gutter . addEventListener ( 'click' , this . handleClick ) ;
398398
399399 this . handleKeydown = this . handleKeydown . bind ( this ) ;
400400 this . codeContent . addEventListener ( 'keydown' , this . handleKeydown ) ;
@@ -427,6 +427,7 @@ export class AnycodeEditor {
427427 private removeEventListeners ( ) {
428428 this . container . removeEventListener ( "scroll" , this . handleScroll ) ;
429429 this . codeContent . removeEventListener ( 'click' , this . handleClick ) ;
430+ this . gutter . removeEventListener ( 'click' , this . handleClick ) ;
430431 this . codeContent . removeEventListener ( 'keydown' , this . handleKeydown ) ;
431432 this . codeContent . removeEventListener ( 'paste' , this . handlePasteEvent ) ;
432433 this . container . removeEventListener ( 'beforeinput' , this . handleBeforeInput ) ;
@@ -493,11 +494,53 @@ export class AnycodeEditor {
493494 this . renderer . renderCursorOrSelection ( this . getEditorState ( ) ) ;
494495 }
495496
497+ private getDiffGapTarget ( target : EventTarget | null ) : HTMLElement | null {
498+ if ( ! ( target instanceof Element ) ) {
499+ return null ;
500+ }
501+ const match = target . closest ( '.diff-gap, .diff-gap-expand-btn' ) ;
502+ return match instanceof HTMLElement ? match : null ;
503+ }
504+
505+ private handleDiffGapExpandClick ( e : MouseEvent ) : boolean {
506+ const gapTarget = this . getDiffGapTarget ( e . target ) ;
507+ if ( ! gapTarget ) return false ;
508+
509+ e . preventDefault ( ) ;
510+ e . stopPropagation ( ) ;
511+
512+ const gapData = getGapElementData ( gapTarget ) ;
513+ if ( ! gapData || gapData . hiddenStart < 0 || gapData . hiddenEnd < gapData . hiddenStart ) {
514+ return true ;
515+ }
516+
517+ const prevScrollTop = this . container . scrollTop ;
518+ const expanded = this . renderer . expandFocusedHiddenRange (
519+ gapData . hiddenStart ,
520+ gapData . hiddenEnd ,
521+ gapData . expandStep ,
522+ gapData . expandDirection ,
523+ ) ;
524+ if ( expanded ) {
525+ this . renderer . render ( this . getEditorState ( ) , this . search ) ;
526+ this . container . scrollTop = prevScrollTop ;
527+ if ( ! this . readOnly ) {
528+ this . codeContent . focus ( { preventScroll : true } ) ;
529+ }
530+ }
531+
532+ return true ;
533+ }
534+
496535 private handleClick ( e : MouseEvent ) : void {
497536 console . log ( "click" , e ) ;
498537 this . clearPendingHover ( ) ;
499538 this . closeHover ( ) ;
500539
540+ if ( this . handleDiffGapExpandClick ( e ) ) {
541+ return ;
542+ }
543+
501544 const oldCursor = this . code . getPosition ( this . offset ) ;
502545
503546 if ( this . selection && this . selection . nonEmpty ( ) ) { return ; }
@@ -612,6 +655,11 @@ export class AnycodeEditor {
612655
613656 private handleMouseDown ( e : MouseEvent ) {
614657 if ( e . button !== 0 ) return ;
658+ if ( this . getDiffGapTarget ( e . target ) ) {
659+ e . preventDefault ( ) ;
660+ e . stopPropagation ( ) ;
661+ return ;
662+ }
615663 if ( isInsideDiagnostic ( e . target as Node ) ) return ;
616664 e . preventDefault ( ) ;
617665 this . clearPendingHover ( ) ;
@@ -937,6 +985,7 @@ export class AnycodeEditor {
937985 } ;
938986
939987 const result = await executeAction ( action , ctx ) ;
988+ this . adjustFocusedDiffNavigationOffset ( result , action ) ;
940989 this . applyEditResult ( result ) ;
941990
942991 if ( this . isCompletionOpen ) {
@@ -950,6 +999,60 @@ export class AnycodeEditor {
950999 }
9511000 }
9521001
1002+ private adjustFocusedDiffNavigationOffset ( result : ActionResult , action : Action ) : void {
1003+ if ( ! this . focusedDiffEnabled ) return ;
1004+ if (
1005+ action !== Action . ARROW_LEFT
1006+ && action !== Action . ARROW_RIGHT
1007+ && action !== Action . ARROW_LEFT_ALT
1008+ && action !== Action . ARROW_RIGHT_ALT
1009+ && action !== Action . ARROW_UP
1010+ && action !== Action . ARROW_DOWN
1011+ ) {
1012+ return ;
1013+ }
1014+
1015+ const visibleLines = this . renderer . getVisibleRealLineIndices ( ) ;
1016+ if ( visibleLines . size === 0 ) return ;
1017+ const visibleLineList = Array . from ( visibleLines ) ;
1018+
1019+ const pos = result . ctx . code . getPosition ( result . ctx . offset ) ;
1020+ if ( visibleLines . has ( pos . line ) ) return ;
1021+
1022+ const preferNext =
1023+ action === Action . ARROW_RIGHT
1024+ || action === Action . ARROW_RIGHT_ALT
1025+ || action === Action . ARROW_DOWN ;
1026+
1027+ let targetLine : number | null = null ;
1028+ if ( preferNext ) {
1029+ targetLine = visibleLineList . find ( ( line ) => line > pos . line ) ?? null ;
1030+ if ( targetLine === null ) {
1031+ targetLine = visibleLineList [ visibleLineList . length - 1 ] ?? null ;
1032+ }
1033+ } else {
1034+ for ( let i = visibleLineList . length - 1 ; i >= 0 ; i -- ) {
1035+ if ( visibleLineList [ i ] < pos . line ) {
1036+ targetLine = visibleLineList [ i ] ;
1037+ break ;
1038+ }
1039+ }
1040+ if ( targetLine === null ) {
1041+ targetLine = visibleLineList [ 0 ] ?? null ;
1042+ }
1043+ }
1044+
1045+ if ( targetLine === null ) return ;
1046+
1047+ const targetColumn = Math . min ( pos . column , result . ctx . code . lineLength ( targetLine ) ) ;
1048+ const targetOffset = result . ctx . code . getOffset ( targetLine , targetColumn ) ;
1049+ result . ctx . offset = targetOffset ;
1050+
1051+ if ( result . ctx . selection && result . ctx . event ?. shiftKey ) {
1052+ result . ctx . selection = result . ctx . selection . fromCursor ( targetOffset ) ;
1053+ }
1054+ }
1055+
9531056 private getActionFromKey ( event : KeyboardEvent ) : Action | null {
9541057 const { key, altKey, ctrlKey, metaKey, shiftKey } = event ;
9551058
@@ -1021,13 +1124,7 @@ export class AnycodeEditor {
10211124
10221125 if ( textChanged ) {
10231126 this . code = result . ctx . code ;
1024- // calculate diff when text changes
1025- if ( this . diffEnabled && this . originalCode !== undefined ) {
1026- const currentText = this . code . getContent ( ) ;
1027- this . diffs = computeGitChanges ( this . originalCode , currentText ) ;
1028- } else {
1029- this . diffs = undefined ;
1030- }
1127+ this . recomputeDiffs ( ) ;
10311128 }
10321129 if ( offsetChanged ) this . offset = result . ctx . offset ;
10331130 if ( selectionChanged ) this . selection = result . ctx . selection || null ;
@@ -1392,12 +1489,7 @@ export class AnycodeEditor {
13921489 this . code . setStateAfter ( this . offset , this . selection || undefined ) ;
13931490 this . code . commit ( ) ;
13941491
1395- if ( this . diffEnabled && this . originalCode !== undefined ) {
1396- const currentText = this . code . getContent ( ) ;
1397- this . diffs = computeGitChanges ( this . originalCode , currentText ) ;
1398- } else {
1399- this . diffs = undefined ;
1400- }
1492+ this . recomputeDiffs ( ) ;
14011493
14021494 this . renderer . renderChanges ( this . getEditorState ( ) , this . search ) ;
14031495 this . verifyDiffRendering ( ) ;
@@ -1411,29 +1503,44 @@ export class AnycodeEditor {
14111503 this . originalCode = this . code . getContent ( ) ;
14121504 }
14131505
1414- if ( enabled && this . originalCode !== undefined ) {
1415- const currentText = this . code . getContent ( ) ;
1416- this . diffs = computeGitChanges ( this . originalCode , currentText ) ;
1417- }
1506+ this . recomputeDiffs ( ) ;
14181507
14191508 if ( ! enabled ) {
14201509 this . renderer . clearAllDiffs ( ) ;
1510+ this . renderer . render ( this . getEditorState ( ) , this . search ) ;
14211511 } else {
14221512 this . renderer . render ( this . getEditorState ( ) , this . search ) ;
14231513 this . verifyDiffRendering ( ) ;
14241514 }
14251515 }
14261516
1517+ public setFocusedDiffMode ( enabled : boolean , contextLines : number = 3 ) : void {
1518+ this . focusedDiffEnabled = enabled ;
1519+ this . focusedDiffContextLines = Math . max ( 0 , contextLines ) ;
1520+ this . renderer . setFocusedDiffMode ( this . focusedDiffEnabled , this . focusedDiffContextLines ) ;
1521+ this . renderer . render ( this . getEditorState ( ) , this . search ) ;
1522+ if ( this . diffEnabled ) {
1523+ this . verifyDiffRendering ( ) ;
1524+ }
1525+ }
1526+
14271527 public setOriginalCode ( content : string ) : void {
14281528 this . originalCode = content ;
14291529 if ( this . diffEnabled ) {
1430- const currentText = this . code . getContent ( ) ;
1431- this . diffs = computeGitChanges ( this . originalCode , currentText ) ;
1530+ this . recomputeDiffs ( ) ;
14321531 this . renderer . render ( this . getEditorState ( ) , this . search ) ;
14331532 this . verifyDiffRendering ( ) ;
14341533 }
14351534 }
14361535
1536+ private recomputeDiffs ( ) : void {
1537+ if ( this . diffEnabled && this . originalCode !== undefined ) {
1538+ this . diffs = computeGitChanges ( this . originalCode , this . code . getContent ( ) ) ;
1539+ } else {
1540+ this . diffs = undefined ;
1541+ }
1542+ }
1543+
14371544 private verifyDiffRendering ( ) : void {
14381545 if ( ! this . diffEnabled || this . diffs === undefined ) {
14391546 return ;
0 commit comments