@@ -2822,44 +2822,39 @@ private void RenderTextContent(DrawingContext dc)
28222822 ? y + _glyphRenderer . Baseline
28232823 : y + _charHeight * 0.8 ;
28242824
2825- // Base pass (external highlighter only): draw only the character spans NOT
2826- // covered by any syntax token in EditorForeground, so identifiers/punctuation
2827- // remain visible without double-drawing covered spans.
2828- // Double-drawing the same characters (base + token overdraw) causes ClearType
2829- // sub-pixel blurring, especially on long string literals in embedded zones.
2830- if ( hasExternalHighlighter )
2825+ // Build the final ordered token list for this line.
2826+ // For lines with an external highlighter, inject EditorForeground gap tokens
2827+ // for any character spans not covered by syntax tokens so every character is
2828+ // rendered exactly once through the same code path. This avoids the ClearType
2829+ // blur that results from drawing base-pass fragments and syntax tokens at
2830+ // independently-snapped x positions for the same character.
2831+ IReadOnlyList < Helpers . SyntaxHighlightToken > orderedTokens ;
2832+ if ( hasExternalHighlighter && renderTokens . Count > 0 )
28312833 {
2832- var dpi2 = VisualTreeHelper . GetDpi ( this ) . PixelsPerDip ;
2833- int gapCursor = 0 ;
2834+ var filled = new List < Helpers . SyntaxHighlightToken > ( renderTokens . Count + 4 ) ;
2835+ int gc = 0 ;
28342836 foreach ( var tok in renderTokens . OrderBy ( t => t . StartColumn ) )
28352837 {
2836- int tokStart = Math . Max ( 0 , tok . StartColumn ) ;
2837- int tokEnd = Math . Min ( line . Text . Length , tok . StartColumn + tok . Length ) ;
2838- if ( tokStart > gapCursor )
2839- {
2840- // Gap before this token — draw in default foreground.
2841- var gap = line . Text . Substring ( gapCursor , tokStart - gapCursor ) ;
2842- var gapTk = new Helpers . SyntaxHighlightToken ( gapCursor , gap . Length , gap , EditorForeground ) ;
2843- double gapX = _glyphRenderer ? . SnapToPixelPublic ( x + _glyphRenderer . ComputeVisualX ( line . Text , gapCursor ) )
2844- ?? x + gapCursor * _charWidth ;
2845- if ( _glyphRenderer != null ) _glyphRenderer . RenderToken ( dc , gapTk , gapX , y , baselineY ) ;
2846- else dc . DrawText ( new FormattedText ( gap , CultureInfo . CurrentCulture , FlowDirection . LeftToRight , _typeface , _fontSize , EditorForeground , dpi2 ) , new Point ( gapX , y ) ) ;
2847- }
2848- gapCursor = Math . Max ( gapCursor , tokEnd ) ;
2849- }
2850- // Trailing gap after last token.
2851- if ( gapCursor < line . Text . Length )
2852- {
2853- var tail = line . Text [ gapCursor ..] ;
2854- var tailTk = new Helpers . SyntaxHighlightToken ( gapCursor , tail . Length , tail , EditorForeground ) ;
2855- double tailX = _glyphRenderer ? . SnapToPixelPublic ( x + _glyphRenderer . ComputeVisualX ( line . Text , gapCursor ) )
2856- ?? x + gapCursor * _charWidth ;
2857- if ( _glyphRenderer != null ) _glyphRenderer . RenderToken ( dc , tailTk , tailX , y , baselineY ) ;
2858- else dc . DrawText ( new FormattedText ( tail , CultureInfo . CurrentCulture , FlowDirection . LeftToRight , _typeface , _fontSize , EditorForeground , dpi2 ) , new Point ( tailX , y ) ) ;
2838+ int ts = Math . Max ( 0 , tok . StartColumn ) ;
2839+ int te = Math . Min ( line . Text . Length , tok . StartColumn + tok . Length ) ;
2840+ if ( ts > gc )
2841+ filled . Add ( new Helpers . SyntaxHighlightToken ( gc , ts - gc ,
2842+ line . Text . Substring ( gc , ts - gc ) , EditorForeground ) ) ;
2843+ if ( te > ts )
2844+ filled . Add ( tok ) ;
2845+ gc = Math . Max ( gc , te ) ;
28592846 }
2847+ if ( gc < line . Text . Length )
2848+ filled . Add ( new Helpers . SyntaxHighlightToken ( gc , line . Text . Length - gc ,
2849+ line . Text [ gc ..] , EditorForeground ) ) ;
2850+ orderedTokens = filled ;
2851+ }
2852+ else
2853+ {
2854+ orderedTokens = renderTokens ;
28602855 }
28612856
2862- foreach ( var token in renderTokens )
2857+ foreach ( var token in orderedTokens )
28632858 {
28642859 // Use tab-aware X so tokens on tab-indented lines are not shifted left.
28652860 // Snap to physical pixel: fractional charWidth × column accumulates sub-pixel
@@ -2902,34 +2897,11 @@ private void RenderTextContent(DrawingContext dc)
29022897 }
29032898
29042899 // ── P1-CE-05: Build GlyphRun cache after first render ─────────────
2905- // Cache gap tokens + syntax tokens so the stale-cache path (DrawGlyphRun)
2906- // renders each character exactly once — matching the gap-only live pass .
2900+ // orderedTokens already contains gap-fill tokens, so use it directly
2901+ // for the cache build — every character is covered exactly once .
29072902 if ( _glyphRenderer != null )
29082903 {
2909- IEnumerable < Helpers . SyntaxHighlightToken > allCacheTokens ;
2910- if ( hasExternalHighlighter )
2911- {
2912- // Build gap segments (chars not covered by any syntax token).
2913- var gapTokens = new List < Helpers . SyntaxHighlightToken > ( ) ;
2914- int gc = 0 ;
2915- foreach ( var tok in renderTokens . OrderBy ( t => t . StartColumn ) )
2916- {
2917- int ts2 = Math . Max ( 0 , tok . StartColumn ) ;
2918- int te2 = Math . Min ( line . Text . Length , tok . StartColumn + tok . Length ) ;
2919- if ( ts2 > gc )
2920- gapTokens . Add ( new Helpers . SyntaxHighlightToken ( gc , ts2 - gc , line . Text . Substring ( gc , ts2 - gc ) , EditorForeground ) ) ;
2921- gc = Math . Max ( gc , te2 ) ;
2922- }
2923- if ( gc < line . Text . Length )
2924- gapTokens . Add ( new Helpers . SyntaxHighlightToken ( gc , line . Text . Length - gc , line . Text [ gc ..] , EditorForeground ) ) ;
2925- allCacheTokens = Enumerable . Concat ( gapTokens , renderTokens ) ;
2926- }
2927- else
2928- {
2929- allCacheTokens = renderTokens ;
2930- }
2931-
2932- line . GlyphRunCache = _glyphRenderer . BuildLineGlyphRuns ( allCacheTokens , SyntaxUrlColor , line . Text ) ;
2904+ line . GlyphRunCache = _glyphRenderer . BuildLineGlyphRuns ( orderedTokens , SyntaxUrlColor , line . Text ) ;
29332905 line . IsGlyphCacheDirty = false ;
29342906
29352907 // Cache link zones for GlyphRun-hit renders (no re-run of OverlayUrlTokens).
0 commit comments