@@ -30,6 +30,10 @@ type IncrementalRenderer struct {
3030 inputPrefix string
3131 outputPrefix string
3232
33+ // codeBlocksPrefix is the list of code blocks emitted while rendering the
34+ // cached prefix, with Line indices relative to outputPrefix.
35+ codeBlocksPrefix []CodeBlock
36+
3337 // fallback is used for the actual rendering work; it is reused across calls
3438 // so its parser pool (and chroma caches) stay warm.
3539 fallback * FastRenderer
@@ -48,10 +52,20 @@ func NewIncrementalRenderer(width int) *IncrementalRenderer {
4852// inputs that share a long common prefix (the streaming case), only the suffix
4953// is parsed and rendered; the rest is served from the cached output.
5054func (r * IncrementalRenderer ) Render (input string ) (string , error ) {
55+ out , _ , err := r .RenderWithCodeBlocks (input )
56+ return out , err
57+ }
58+
59+ // RenderWithCodeBlocks behaves like Render but additionally returns the list
60+ // of fenced code blocks in the rendered output. Each entry's Line is the
61+ // 0-indexed line within the returned string where the block's copy label is
62+ // drawn.
63+ func (r * IncrementalRenderer ) RenderWithCodeBlocks (input string ) (string , []CodeBlock , error ) {
5164 if input == "" {
5265 r .inputPrefix = ""
5366 r .outputPrefix = ""
54- return "" , nil
67+ r .codeBlocksPrefix = nil
68+ return "" , nil , nil
5569 }
5670
5771 // If the new input no longer starts with our cached prefix, the user (or a
@@ -70,33 +84,37 @@ func (r *IncrementalRenderer) Render(input string) (string, error) {
7084 if boundary <= 0 {
7185 // No new block boundary in the tail yet — render only the tail and
7286 // concatenate. Cached prefix is unchanged.
73- renderedTail , err := r .fallback .Render (tail )
87+ renderedTail , tailBlocks , err := r .fallback .RenderWithCodeBlocks (tail )
7488 if err != nil {
7589 return r .fullRender (input )
7690 }
77- return r .joinPrefixAndTail (r .outputPrefix , renderedTail ), nil
91+ out := r .joinPrefixAndTail (r .outputPrefix , renderedTail )
92+ return out , r .mergeCodeBlocks (r .outputPrefix , r .codeBlocksPrefix , tailBlocks ), nil
7893 }
7994
8095 // We have a new boundary inside the tail. Render the new stable region
8196 // (inputPrefix + tail[:boundary]) once, append it to the cache, then render
8297 // the new tail.
8398 newStableTail := tail [:boundary ]
84- renderedStableTail , err := r .fallback .Render (newStableTail )
99+ renderedStableTail , stableBlocks , err := r .fallback .RenderWithCodeBlocks (newStableTail )
85100 if err != nil {
86101 return r .fullRender (input )
87102 }
103+ newBlocks := r .mergeCodeBlocks (r .outputPrefix , r .codeBlocksPrefix , stableBlocks )
88104 r .inputPrefix += newStableTail
89105 r .outputPrefix = r .joinPrefixAndTail (r .outputPrefix , renderedStableTail )
106+ r .codeBlocksPrefix = newBlocks
90107
91108 rest := tail [boundary :]
92109 if rest == "" {
93- return r .outputPrefix , nil
110+ return r .outputPrefix , cloneCodeBlocks ( r . codeBlocksPrefix ), nil
94111 }
95- renderedRest , err := r .fallback .Render (rest )
112+ renderedRest , restBlocks , err := r .fallback .RenderWithCodeBlocks (rest )
96113 if err != nil {
97114 return r .fullRender (input )
98115 }
99- return r .joinPrefixAndTail (r .outputPrefix , renderedRest ), nil
116+ out := r .joinPrefixAndTail (r .outputPrefix , renderedRest )
117+ return out , r .mergeCodeBlocks (r .outputPrefix , r .codeBlocksPrefix , restBlocks ), nil
100118}
101119
102120// SetWidth updates the renderer width. Width changes invalidate the cache
@@ -109,13 +127,15 @@ func (r *IncrementalRenderer) SetWidth(width int) {
109127 r .fallback = NewFastRenderer (width )
110128 r .inputPrefix = ""
111129 r .outputPrefix = ""
130+ r .codeBlocksPrefix = nil
112131}
113132
114133// Reset drops the cached prefix without changing the width. Use when the
115134// underlying message is replaced by an unrelated new one.
116135func (r * IncrementalRenderer ) Reset () {
117136 r .inputPrefix = ""
118137 r .outputPrefix = ""
138+ r .codeBlocksPrefix = nil
119139}
120140
121141// fullRender renders input from scratch, refreshes the cache, and returns the
@@ -124,36 +144,40 @@ func (r *IncrementalRenderer) Reset() {
124144// pieces separately, then join. The two render calls on smaller inputs are
125145// faster than one big render plus a separate prefix render, and the prefix
126146// piece can be reused as outputPrefix.
127- func (r * IncrementalRenderer ) fullRender (input string ) (string , error ) {
147+ func (r * IncrementalRenderer ) fullRender (input string ) (string , [] CodeBlock , error ) {
128148 boundary := stableBoundary (input )
129149 if boundary <= 0 {
130- out , err := r .fallback .Render (input )
150+ out , blocks , err := r .fallback .RenderWithCodeBlocks (input )
131151 if err != nil {
132- return "" , err
152+ return "" , nil , err
133153 }
134154 r .inputPrefix = ""
135155 r .outputPrefix = ""
136- return out , nil
156+ r .codeBlocksPrefix = nil
157+ return out , blocks , nil
137158 }
138159
139160 prefix := input [:boundary ]
140161 rest := input [boundary :]
141- renderedPrefix , err := r .fallback .Render (prefix )
162+ renderedPrefix , prefixBlocks , err := r .fallback .RenderWithCodeBlocks (prefix )
142163 if err != nil {
143- return "" , err
164+ return "" , nil , err
144165 }
145166 if rest == "" {
146167 r .inputPrefix = prefix
147168 r .outputPrefix = renderedPrefix
148- return renderedPrefix , nil
169+ r .codeBlocksPrefix = prefixBlocks
170+ return renderedPrefix , cloneCodeBlocks (prefixBlocks ), nil
149171 }
150- renderedRest , err := r .fallback .Render (rest )
172+ renderedRest , restBlocks , err := r .fallback .RenderWithCodeBlocks (rest )
151173 if err != nil {
152- return "" , err
174+ return "" , nil , err
153175 }
154176 r .inputPrefix = prefix
155177 r .outputPrefix = renderedPrefix
156- return r .joinPrefixAndTail (renderedPrefix , renderedRest ), nil
178+ r .codeBlocksPrefix = prefixBlocks
179+ out := r .joinPrefixAndTail (renderedPrefix , renderedRest )
180+ return out , r .mergeCodeBlocks (renderedPrefix , prefixBlocks , restBlocks ), nil
157181}
158182
159183// joinPrefixAndTail concatenates a previously rendered prefix and a freshly
@@ -193,6 +217,43 @@ func (r *IncrementalRenderer) joinPrefixAndTail(prefix, tail string) string {
193217 return b .String ()
194218}
195219
220+ // joinSeparatorLines is the number of extra rendered lines that
221+ // joinPrefixAndTail inserts between the prefix output and the tail output:
222+ // one to terminate the prefix's final (untrailing-newlined) line, and one
223+ // blank-padded separator line. Keep this in sync with joinPrefixAndTail.
224+ const joinSeparatorLines = 2
225+
226+ // mergeCodeBlocks returns the union of code blocks from a cached prefix output
227+ // and a freshly rendered tail. Tail block line indices are shifted past the
228+ // prefix's lines and the separator that joinPrefixAndTail inserts.
229+ func (r * IncrementalRenderer ) mergeCodeBlocks (prefixOut string , prefixBlocks , tailBlocks []CodeBlock ) []CodeBlock {
230+ if len (prefixBlocks ) == 0 && len (tailBlocks ) == 0 {
231+ return nil
232+ }
233+ out := make ([]CodeBlock , 0 , len (prefixBlocks )+ len (tailBlocks ))
234+ out = append (out , prefixBlocks ... )
235+ if len (tailBlocks ) == 0 {
236+ return out
237+ }
238+ offset := 0
239+ if prefixOut != "" {
240+ offset = strings .Count (prefixOut , "\n " ) + joinSeparatorLines
241+ }
242+ for _ , b := range tailBlocks {
243+ out = append (out , CodeBlock {Content : b .Content , Line : b .Line + offset })
244+ }
245+ return out
246+ }
247+
248+ func cloneCodeBlocks (in []CodeBlock ) []CodeBlock {
249+ if len (in ) == 0 {
250+ return nil
251+ }
252+ out := make ([]CodeBlock , len (in ))
253+ copy (out , in )
254+ return out
255+ }
256+
196257// stableBoundary returns the byte index just after the last "safe" block
197258// boundary in input, or 0 if no safe boundary exists. A safe boundary is a
198259// blank line that the FastRenderer treats as a hard break between top-level
0 commit comments