11import { AnycodeLine } from "../utils" ;
22import { EditorSettings } from "../editor" ;
33import { DiffInfo , ChangeType } from "../diff" ;
4+ import { GhostRow } from "./Renderer" ;
45
56export interface GhostLine {
67 code : HTMLElement ;
@@ -46,78 +47,110 @@ export class DiffRenderer {
4647
4748 // ========== Ghost Lines Rendering ==========
4849
50+ private hasGhostContent ( diffInfo : DiffInfo ) : boolean {
51+ return (
52+ ( diffInfo . changeType === 'modified' || diffInfo . changeType === 'deleted' ) &&
53+ ! ! diffInfo . oldLines &&
54+ diffInfo . oldLines . length > 0
55+ ) ;
56+ }
57+
58+ private getGhostAnchorLine ( lineNumber : number , diffInfo : DiffInfo ) : number {
59+ return diffInfo . ghostAnchorLine ?? lineNumber ;
60+ }
61+
62+ /**
63+ * @deprecated Use buildVisualRows + createGhostRowElements instead.
64+ * This method was used for dynamic ghost line insertion during render.
65+ */
4966 public renderGhostsForLine (
5067 lineIndex : number ,
5168 diffResult : Map < number , DiffInfo > ,
5269 renderedHunks : Set < number > ,
5370 settings : EditorSettings
5471 ) : GhostLine [ ] | null {
55- const diffInfo = diffResult . get ( lineIndex + 1 ) ;
56- if ( diffInfo ?. changeType === 'modified' && diffInfo . oldLines && diffInfo . oldLines . length > 0 ) {
57- const hunkId = diffInfo . hunkId ;
72+ const anchorLine = lineIndex + 1 ;
73+ const lines : GhostLine [ ] = [ ] ;
5874
59- // Check if this is the first line in the hunk within the visible range
60- // And we haven't rendered this hunk yet
61- if ( ! renderedHunks . has ( hunkId ) ) {
62- renderedHunks . add ( hunkId ) ;
75+ for ( const [ lineNumber , diffInfo ] of diffResult ) {
76+ if ( ! this . hasGhostContent ( diffInfo ) ) {
77+ continue ;
78+ }
79+ if ( this . getGhostAnchorLine ( lineNumber , diffInfo ) !== anchorLine ) {
80+ continue ;
81+ }
6382
64- const lines : GhostLine [ ] = [ ] ;
83+ const hunkId = diffInfo . hunkId ;
84+ if ( renderedHunks . has ( hunkId ) ) {
85+ continue ;
86+ }
87+ renderedHunks . add ( hunkId ) ;
6588
66- // Add ghost lines for deleted content
67- for ( const oldLine of diffInfo . oldLines ) {
68- const ghostLine = this . createDeletedGhostLine ( oldLine , settings , hunkId ) ;
89+ for ( const oldLine of diffInfo . oldLines ! ) {
90+ const ghostLine = this . createDeletedGhostLine ( oldLine , settings , hunkId ) ;
6991
70- // Add empty gutter and button elements to keep alignment
71- const emptyGutter = document . createElement ( 'div' ) ;
72- emptyGutter . className = 'ln' ;
73- emptyGutter . style . height = `${ settings . lineHeight } px` ;
74- emptyGutter . setAttribute ( 'data-ghost' , 'true' ) ;
75- emptyGutter . setAttribute ( 'data-hunk-id' , hunkId . toString ( ) ) ;
92+ // Add empty gutter and button elements to keep alignment
93+ const emptyGutter = document . createElement ( 'div' ) ;
94+ emptyGutter . className = 'ln' ;
95+ emptyGutter . style . height = `${ settings . lineHeight } px` ;
96+ emptyGutter . setAttribute ( 'data-ghost' , 'true' ) ;
97+ emptyGutter . setAttribute ( 'data-hunk-id' , hunkId . toString ( ) ) ;
7698
77- const emptyButton = document . createElement ( 'div' ) ;
78- emptyButton . className = 'bt' ;
79- emptyButton . style . height = `${ settings . lineHeight } px` ;
80- emptyButton . setAttribute ( 'data-ghost' , 'true' ) ;
81- emptyButton . setAttribute ( 'data-hunk-id' , hunkId . toString ( ) ) ;
99+ const emptyButton = document . createElement ( 'div' ) ;
100+ emptyButton . className = 'bt' ;
101+ emptyButton . style . height = `${ settings . lineHeight } px` ;
102+ emptyButton . setAttribute ( 'data-ghost' , 'true' ) ;
103+ emptyButton . setAttribute ( 'data-hunk-id' , hunkId . toString ( ) ) ;
82104
83- lines . push ( { code : ghostLine , gutter : emptyGutter , btn : emptyButton } ) ;
84- }
85- return lines ;
105+ lines . push ( { code : ghostLine , gutter : emptyGutter , btn : emptyButton } ) ;
86106 }
87107 }
88- return null ;
108+
109+ return lines . length > 0 ? lines : null ;
89110 }
90111
112+ /**
113+ * @deprecated No longer needed with visual rows model.
114+ * Ghost lines are now part of the unified visual rows and rendered with stable indices.
115+ */
91116 public syncVisibleGhosts (
92117 startLine : number ,
93118 endLine : number ,
94119 diffResult : Map < number , DiffInfo > ,
95120 settings : EditorSettings ,
96- lines : AnycodeLine [ ]
121+ lines : AnycodeLine [ ] ,
122+ totalLines : number
97123 ) : void {
98124 if ( ! diffResult || diffResult . size === 0 ) return ;
99125
100126 const visibleHunks = new Set < number > ( ) ;
127+ const includeEofAnchor = endLine === totalLines ;
128+
129+ // Collect all hunks whose ghost anchor is visible.
130+ for ( const [ lineNumber , diffInfo ] of diffResult ) {
131+ if ( ! this . hasGhostContent ( diffInfo ) ) {
132+ continue ;
133+ }
134+ const anchorLine = this . getGhostAnchorLine ( lineNumber , diffInfo ) ;
135+ const inVisibleRange = anchorLine >= startLine + 1 && anchorLine <= endLine ;
136+ const atVisibleEof = includeEofAnchor && anchorLine === totalLines + 1 ;
101137
102- // Collect all hunks in visible range
103- for ( let i = startLine ; i < endLine ; i ++ ) {
104- const diffInfo = diffResult . get ( i + 1 ) ;
105- if ( diffInfo ?. changeType === 'modified' && diffInfo . oldLines && diffInfo . oldLines . length > 0 ) {
138+ if ( inVisibleRange || atVisibleEof ) {
106139 visibleHunks . add ( diffInfo . hunkId ) ;
107140 }
108141 }
109142
110143 // Update ghost lines for each visible hunk
111144 for ( const hunkId of visibleHunks ) {
112- let oldLines : string [ ] | undefined ;
145+ let oldLinesForHunk : string [ ] | undefined ;
113146 for ( const [ _ , info ] of diffResult ) {
114- if ( info . hunkId === hunkId && info . oldLines ) {
115- oldLines = info . oldLines ;
147+ if ( info . hunkId === hunkId && info . oldLines && info . oldLines . length > 0 ) {
148+ oldLinesForHunk = info . oldLines ;
116149 break ;
117150 }
118151 }
119- if ( oldLines ) {
120- this . updateGhostLinesForHunk ( hunkId , oldLines , settings , diffResult , lines ) ;
152+ if ( oldLinesForHunk ) {
153+ this . updateGhostLinesForHunk ( hunkId , oldLinesForHunk , settings , diffResult , lines ) ;
121154 }
122155 }
123156
@@ -163,13 +196,12 @@ export class DiffRenderer {
163196 // Remove old ghost lines and corresponding gutter/button elements
164197 this . removeGhostLinesForHunk ( hunkId ) ;
165198
166- // Find the first line in this hunk
167- const firstLineNum = this . getFirstLineInHunk ( hunkId , diffResult ) ;
168- if ( firstLineNum === null ) return ;
199+ // Find ghost anchor for this hunk.
200+ const anchorLineNum = this . getGhostAnchorLineInHunk ( hunkId , diffResult ) ;
201+ if ( anchorLineNum === null ) return ;
169202
170- // Find firstLine from lines array instead of DOM query
171- const firstLine = lines . find ( line => line . lineNumber === firstLineNum - 1 ) ;
172- if ( ! firstLine ) return ;
203+ // Find anchor line from lines array (can be absent for EOF anchors).
204+ const anchorLine = lines . find ( line => line . lineNumber === anchorLineNum - 1 ) ;
173205
174206 // Insert new ghost lines before the first line
175207 const codeFrag = document . createDocumentFragment ( ) ;
@@ -195,19 +227,18 @@ export class DiffRenderer {
195227 btnFrag . appendChild ( emptyButton ) ;
196228 }
197229
198- // Find corresponding gutter and button elements by data-line attribute
199- const firstGutterEl = this . gutter . querySelector ( `.ln[data-line="${ firstLineNum - 1 } "]` ) ;
200- const firstBtnEl = this . buttonsColumn . querySelector ( `.bt[data-line="${ firstLineNum - 1 } "]` ) ;
230+ // Find corresponding gutter and button elements by data-line attribute.
231+ const anchorGutterEl = this . gutter . querySelector ( `.ln[data-line="${ anchorLineNum - 1 } "]` ) ;
232+ const anchorBtnEl = this . buttonsColumn . querySelector ( `.bt[data-line="${ anchorLineNum - 1 } "]` ) ;
201233
202- // Insert at the correct positions in all three containers
203- this . codeContent . insertBefore ( codeFrag , firstLine ) ;
234+ // Insert at the correct positions in all three containers.
235+ const codeInsertBefore = anchorLine ?? this . codeContent . lastElementChild ;
236+ const gutterInsertBefore = anchorGutterEl ?? this . gutter . lastElementChild ;
237+ const btnInsertBefore = anchorBtnEl ?? this . buttonsColumn . lastElementChild ;
204238
205- if ( firstGutterEl ) {
206- this . gutter . insertBefore ( gutterFrag , firstGutterEl ) ;
207- }
208- if ( firstBtnEl ) {
209- this . buttonsColumn . insertBefore ( btnFrag , firstBtnEl ) ;
210- }
239+ this . codeContent . insertBefore ( codeFrag , codeInsertBefore ) ;
240+ this . gutter . insertBefore ( gutterFrag , gutterInsertBefore ) ;
241+ this . buttonsColumn . insertBefore ( btnFrag , btnInsertBefore ) ;
211242 }
212243
213244 private createDeletedGhostLine (
@@ -228,6 +259,33 @@ export class DiffRenderer {
228259 return ghostLine ;
229260 }
230261
262+ /**
263+ * Create DOM elements for a ghost row (from visual rows model)
264+ * Returns code, gutter, and button elements for the ghost line
265+ */
266+ public createGhostRowElements (
267+ ghostRow : GhostRow ,
268+ settings : EditorSettings
269+ ) : GhostLine {
270+ const { hunkId, text } = ghostRow ;
271+
272+ const ghostLine = this . createDeletedGhostLine ( text , settings , hunkId ) ;
273+
274+ const emptyGutter = document . createElement ( 'div' ) ;
275+ emptyGutter . className = 'ln' ;
276+ emptyGutter . style . height = `${ settings . lineHeight } px` ;
277+ emptyGutter . setAttribute ( 'data-ghost' , 'true' ) ;
278+ emptyGutter . setAttribute ( 'data-hunk-id' , hunkId . toString ( ) ) ;
279+
280+ const emptyButton = document . createElement ( 'div' ) ;
281+ emptyButton . className = 'bt' ;
282+ emptyButton . style . height = `${ settings . lineHeight } px` ;
283+ emptyButton . setAttribute ( 'data-ghost' , 'true' ) ;
284+ emptyButton . setAttribute ( 'data-hunk-id' , hunkId . toString ( ) ) ;
285+
286+ return { code : ghostLine , gutter : emptyGutter , btn : emptyButton } ;
287+ }
288+
231289 private findGhostLinesForHunk ( hunkId : number ) : HTMLElement [ ] {
232290 const ghostLines : HTMLElement [ ] = [ ] ;
233291 const allGhostLines = this . codeContent . querySelectorAll ( '[data-ghost="true"]' ) ;
@@ -250,18 +308,19 @@ export class DiffRenderer {
250308 btnGhosts . forEach ( ghost => ghost . remove ( ) ) ;
251309 }
252310
253- private getFirstLineInHunk (
311+ private getGhostAnchorLineInHunk (
254312 hunkId : number , diffResult : Map < number , DiffInfo >
255313 ) : number | null {
256- let minLine : number | null = null ;
314+ let minAnchorLine : number | null = null ;
257315 for ( const [ lineNum , info ] of diffResult ) {
258- if ( info . hunkId === hunkId && info . changeType === 'modified' ) {
259- if ( minLine === null || lineNum < minLine ) {
260- minLine = lineNum ;
316+ if ( info . hunkId === hunkId && this . hasGhostContent ( info ) ) {
317+ const anchorLine = this . getGhostAnchorLine ( lineNum , info ) ;
318+ if ( minAnchorLine === null || anchorLine < minAnchorLine ) {
319+ minAnchorLine = anchorLine ;
261320 }
262321 }
263322 }
264- return minLine ;
323+ return minAnchorLine ;
265324 }
266325
267326 public clearAllGhostLines ( ) : void {
0 commit comments