@@ -53,6 +53,13 @@ interface WrappedBlockContext {
5353 segmentBounds : Map < number , { lineStart : number ; lineEnd : number } > ;
5454}
5555
56+ type WrappedBlockCacheEntry = WrappedBlockContext | null ;
57+
58+ interface WrappedBlockScanCache {
59+ contexts : Map < number , WrappedBlockCacheEntry > ;
60+ cappedMiss : DirtyLineSegment | null ;
61+ }
62+
5663/** Shared empty array for non-matching lines to avoid per-call allocations. */
5764const EMPTY_RANGES : readonly CachedDecorationRange [ ] = Object . freeze ( [ ] ) ;
5865
@@ -1023,7 +1030,10 @@ export class KeywordHighlighter implements IDisposable {
10231030 private processLineRange ( start : number , end : number , cursorAbsoluteY : number ) {
10241031 if ( end < start ) return ;
10251032 const buffer = this . term . buffer . active ;
1026- const wrappedBlockCache = new Map < number , WrappedBlockContext > ( ) ;
1033+ const wrappedBlockCache : WrappedBlockScanCache = {
1034+ contexts : new Map < number , WrappedBlockCacheEntry > ( ) ,
1035+ cappedMiss : null ,
1036+ } ;
10271037 const pressure = getTerminalOutputPressure ( this . term ) ;
10281038 for ( let lineY = start ; lineY <= end ; lineY ++ ) {
10291039 const line = buffer . getLine ( lineY ) ;
@@ -1103,22 +1113,39 @@ export class KeywordHighlighter implements IDisposable {
11031113 return ! ! nextLine ?. isWrapped ;
11041114 }
11051115
1106- private findWrappedBlockStart ( buffer : IBuffer , lineY : number ) : number {
1116+ private findWrappedBlockStart ( buffer : IBuffer , lineY : number ) : { startY : number ; cappedRange ?: DirtyLineSegment } {
11071117 let startY = lineY ;
1118+ let scannedRows = 0 ;
1119+ const maxRows = this . getWrappedContextScanRowLimit ( ) ;
11081120 while ( startY > 0 ) {
1121+ scannedRows += 1 ;
1122+ if ( scannedRows > maxRows ) {
1123+ return { startY : - 1 , cappedRange : { start : startY , end : lineY } } ;
1124+ }
11091125 const current = buffer . getLine ( startY ) ;
11101126 if ( ! current ?. isWrapped ) break ;
11111127 startY -= 1 ;
11121128 }
1113- return startY ;
1129+ return { startY } ;
1130+ }
1131+
1132+ private getWrappedContextScanRowLimit ( ) : number {
1133+ const cols = Math . max ( 1 , this . term . cols || 1 ) ;
1134+ return Math . max ( 1 , Math . ceil ( TERMINAL_AUX_LONG_LINE_SCAN_LIMIT_CHARS / cols ) + 1 ) ;
11141135 }
11151136
11161137 private buildWrappedBlockContext ( buffer : IBuffer , startY : number ) : WrappedBlockContext | null {
11171138 let logicalLineText = "" ;
11181139 const segmentBounds = new Map < number , { lineStart : number ; lineEnd : number } > ( ) ;
11191140 let cursorY = startY ;
1141+ let scannedRows = 0 ;
1142+ const maxRows = this . getWrappedContextScanRowLimit ( ) ;
11201143
11211144 while ( true ) {
1145+ scannedRows += 1 ;
1146+ if ( scannedRows > maxRows ) {
1147+ return null ;
1148+ }
11221149 const segment = buffer . getLine ( cursorY ) ;
11231150 if ( ! segment ) break ;
11241151 const segmentText = segment . translateToString ( true ) ;
@@ -1142,16 +1169,22 @@ export class KeywordHighlighter implements IDisposable {
11421169 private getWrappedContext (
11431170 buffer : IBuffer ,
11441171 lineY : number ,
1145- cache : Map < number , WrappedBlockContext > ,
1172+ line : IBufferLine ,
1173+ cache : WrappedBlockScanCache ,
11461174 ) : { logicalLineText : string ; lineStart : number ; lineEnd : number } | null {
1147- const startY = this . findWrappedBlockStart ( buffer , lineY ) ;
1148- let block = cache . get ( startY ) ;
1149- if ( ! block ) {
1150- block = this . buildWrappedBlockContext ( buffer , startY ) ?? undefined ;
1151- if ( block ) {
1152- cache . set ( startY , block ) ;
1153- }
1175+ if ( this . isInCappedWrappedMiss ( lineY , line , cache ) ) {
1176+ return null ;
11541177 }
1178+
1179+ const { startY, cappedRange } = this . findWrappedBlockStart ( buffer , lineY ) ;
1180+ if ( startY < 0 ) {
1181+ cache . cappedMiss = cappedRange ?? { start : lineY , end : lineY } ;
1182+ return null ;
1183+ }
1184+ if ( ! cache . contexts . has ( startY ) ) {
1185+ cache . contexts . set ( startY , this . buildWrappedBlockContext ( buffer , startY ) ) ;
1186+ }
1187+ const block = cache . contexts . get ( startY ) ;
11551188 if ( ! block ) return null ;
11561189 const bounds = block . segmentBounds . get ( lineY ) ;
11571190 if ( ! bounds ) return null ;
@@ -1162,14 +1195,32 @@ export class KeywordHighlighter implements IDisposable {
11621195 } ;
11631196 }
11641197
1198+ private isInCappedWrappedMiss (
1199+ lineY : number ,
1200+ line : IBufferLine ,
1201+ cache : WrappedBlockScanCache ,
1202+ ) : boolean {
1203+ const miss = cache . cappedMiss ;
1204+ if ( ! miss ) return false ;
1205+ if ( lineY >= miss . start && lineY <= miss . end ) return true ;
1206+ if ( lineY === miss . end + 1 && line . isWrapped ) {
1207+ miss . end = lineY ;
1208+ return true ;
1209+ }
1210+ if ( lineY > miss . end ) {
1211+ cache . cappedMiss = null ;
1212+ }
1213+ return false ;
1214+ }
1215+
11651216 private scanWrappedLine (
11661217 buffer : IBuffer ,
11671218 lineY : number ,
11681219 line : IBufferLine ,
11691220 lineText : string ,
1170- wrappedBlockCache : Map < number , WrappedBlockContext > ,
1221+ wrappedBlockCache : WrappedBlockScanCache ,
11711222 ) : CachedDecorationRange [ ] {
1172- const context = this . getWrappedContext ( buffer , lineY , wrappedBlockCache ) ;
1223+ const context = this . getWrappedContext ( buffer , lineY , line , wrappedBlockCache ) ;
11731224 if ( ! context || context . logicalLineText === lineText ) {
11741225 return this . scanLine ( line , lineText ) ;
11751226 }
0 commit comments