@@ -444,4 +444,109 @@ public void Write_EmitsSixelDataAndPositionsCursor (bool isLegacyConsole)
444444
445445 app . Dispose ( ) ;
446446 }
447+
448+ // Claude - Opus 4.7
449+ // Regression test for https://github.com/gui-cs/Terminal.Gui/issues/4892
450+ // When dirty cells with a URL are flushed mid-row because a clean cell follows,
451+ // the OSC 8 hyperlink remains open in the terminal. If no more dirty cells appear
452+ // on the row, the end-of-row code must still emit the OSC 8 close sequence so the
453+ // hyperlink does not bleed into the next row.
454+ [ Fact ]
455+ public void Write_UrlFollowedByCleanCells_ClosesHyperlinkAtRowEnd ( )
456+ {
457+ // Arrange: 5-col row. URL at cols 0-1, clean cells at cols 2-4.
458+ AnsiOutput output = new ( ) ;
459+ IOutputBuffer buffer = output . GetLastBuffer ( ) ! ;
460+ buffer . SetSize ( 5 , 1 ) ;
461+
462+ // First frame: write URL cells then clear dirty by flushing
463+ buffer . Move ( 0 , 0 ) ;
464+ buffer . CurrentUrl = "https://example.com" ;
465+ buffer . AddStr ( "AB" ) ;
466+ buffer . CurrentUrl = null ;
467+ output . Write ( buffer ) ;
468+
469+ // Second frame: only re-mark the URL cells dirty so cols 2-4 stay clean.
470+ buffer . Contents ! [ 0 , 0 ] . IsDirty = true ;
471+ buffer . Contents ! [ 0 , 1 ] . IsDirty = true ;
472+ buffer . DirtyLines [ 0 ] = true ;
473+
474+ // Act
475+ output . Write ( buffer ) ;
476+ string result = output . GetLastOutput ( ) ;
477+
478+ // Assert: every OSC 8 start sequence is followed by an OSC 8 close before the row ends.
479+ string start = EscSeqUtils . OSC_StartHyperlink ( "https://example.com" ) ;
480+ string end = EscSeqUtils . OSC_EndHyperlink ( ) ;
481+ int startIdx = result . IndexOf ( start , StringComparison . Ordinal ) ;
482+ Assert . True ( startIdx >= 0 , "OSC 8 start sequence not emitted" ) ;
483+
484+ int endIdx = result . IndexOf ( end , startIdx + start . Length , StringComparison . Ordinal ) ;
485+ Assert . True ( endIdx > startIdx , "OSC 8 hyperlink was not closed before the row ended" ) ;
486+ }
487+
488+ // Claude - Opus 4.7
489+ // Regression test for https://github.com/gui-cs/Terminal.Gui/issues/4892
490+ // When a Link's display area shrinks (or a Link is replaced), the cells previously
491+ // associated with a URL may be overdrawn by content that has no URL. Those cells
492+ // must be removed from the URL map so OSC 8 sequences are not re-emitted for them.
493+ [ Fact ]
494+ public void AddStr_NoCurrentUrl_ClearsStaleUrlMapping ( )
495+ {
496+ // Arrange
497+ AnsiOutput output = new ( ) ;
498+ IOutputBuffer buffer = output . GetLastBuffer ( ) ! ;
499+ buffer . SetSize ( 5 , 1 ) ;
500+
501+ // First write: cells get associated with a URL
502+ buffer . Move ( 0 , 0 ) ;
503+ buffer . CurrentUrl = "https://example.com" ;
504+ buffer . AddStr ( "HELLO" ) ;
505+ buffer . CurrentUrl = null ;
506+
507+ Assert . Equal ( "https://example.com" , buffer . GetCellUrl ( 0 , 0 ) ) ;
508+ Assert . Equal ( "https://example.com" , buffer . GetCellUrl ( 4 , 0 ) ) ;
509+
510+ // Act: overwrite cells with no CurrentUrl set (simulates a non-link view redrawing)
511+ buffer . Move ( 0 , 0 ) ;
512+ buffer . AddStr ( "WORLD" ) ;
513+
514+ // Assert: stale URL associations are cleared
515+ Assert . Null ( buffer . GetCellUrl ( 0 , 0 ) ) ;
516+ Assert . Null ( buffer . GetCellUrl ( 1 , 0 ) ) ;
517+ Assert . Null ( buffer . GetCellUrl ( 2 , 0 ) ) ;
518+ Assert . Null ( buffer . GetCellUrl ( 3 , 0 ) ) ;
519+ Assert . Null ( buffer . GetCellUrl ( 4 , 0 ) ) ;
520+
521+ // And the rendered output for the second frame contains no OSC 8 sequences
522+ string result = output . ToAnsi ( buffer ) ;
523+ Assert . DoesNotContain ( EscSeqUtils . OSC_StartHyperlink ( "https://example.com" ) , result ) ;
524+ }
525+
526+ // Claude - Opus 4.7
527+ // Regression test for https://github.com/gui-cs/Terminal.Gui/issues/4892
528+ // When CurrentUrl changes from one URL to another for the same cell, the URL map
529+ // should reflect the new URL (verifying the URL clear path does not break re-assignment).
530+ [ Fact ]
531+ public void AddStr_DifferentUrl_OverwritesUrlMapping ( )
532+ {
533+ // Arrange
534+ AnsiOutput output = new ( ) ;
535+ IOutputBuffer buffer = output . GetLastBuffer ( ) ! ;
536+ buffer . SetSize ( 3 , 1 ) ;
537+
538+ buffer . Move ( 0 , 0 ) ;
539+ buffer . CurrentUrl = "https://one.com" ;
540+ buffer . AddStr ( "ABC" ) ;
541+
542+ // Act: rewrite same cells with a different URL
543+ buffer . Move ( 0 , 0 ) ;
544+ buffer . CurrentUrl = "https://two.com" ;
545+ buffer . AddStr ( "ABC" ) ;
546+
547+ // Assert: cells now report the new URL
548+ Assert . Equal ( "https://two.com" , buffer . GetCellUrl ( 0 , 0 ) ) ;
549+ Assert . Equal ( "https://two.com" , buffer . GetCellUrl ( 1 , 0 ) ) ;
550+ Assert . Equal ( "https://two.com" , buffer . GetCellUrl ( 2 , 0 ) ) ;
551+ }
447552}
0 commit comments