@@ -690,6 +690,123 @@ func TestDiagCounts(t *testing.T) {
690690 }
691691}
692692
693+ // diagJumpModel builds a hermetic model with a single buffer open and three
694+ // diagnostics seeded on content lines (rows 2, 4, 6, cols within the line
695+ // length so JumpTo does not clamp), returned unsorted to prove the jump path
696+ // sorts them. The cursor starts at row 0, col 0.
697+ func diagJumpModel (t * testing.T ) (model , string ) {
698+ t .Helper ()
699+ root := t .TempDir ()
700+ path := filepath .Join (root , "a.go" )
701+ body := "package main\n \n func a() {}\n \n func b() {}\n \n func c() {}\n "
702+ if err := os .WriteFile (path , []byte (body ), 0o644 ); err != nil {
703+ t .Fatal (err )
704+ }
705+ m := newModel (root )
706+ m .width , m .height = 100 , 30
707+ m = m .resize ()
708+ m .bufs .OpenOrSwitch (path )
709+ m .diagnostics [path ] = []protocol.Diagnostic {
710+ {Range : protocol.Range {Start : protocol.Position {Line : 6 , Character : 2 }}, Severity : protocol .DiagnosticSeverityWarning , Message : "c" },
711+ {Range : protocol.Range {Start : protocol.Position {Line : 2 , Character : 0 }}, Severity : protocol .DiagnosticSeverityError , Message : "a" },
712+ {Range : protocol.Range {Start : protocol.Position {Line : 4 , Character : 5 }}, Severity : protocol .DiagnosticSeverityWarning , Message : "b" },
713+ }
714+ return m , path
715+ }
716+
717+ // TestJumpDiagnosticForwardWraps walks F8 through all three diagnostics in
718+ // sorted order and confirms the fourth press wraps to the first.
719+ func TestJumpDiagnosticForwardWraps (t * testing.T ) {
720+ m , _ := diagJumpModel (t )
721+ want := []struct { row , col int }{{2 , 0 }, {4 , 5 }, {6 , 2 }, {2 , 0 }}
722+ for i , w := range want {
723+ updated , _ := m .jumpDiagnostic (true )
724+ m = updated .(model )
725+ p := m .bufs .Active ()
726+ if p .CursorRow () != w .row || p .CursorCol () != w .col {
727+ t .Fatalf ("forward step %d: cursor at (%d,%d), want (%d,%d)" , i , p .CursorRow (), p .CursorCol (), w .row , w .col )
728+ }
729+ }
730+ }
731+
732+ // TestJumpDiagnosticBackwardWraps confirms Alt+F8 from the top of the buffer
733+ // wraps to the last diagnostic, then walks upward in order.
734+ func TestJumpDiagnosticBackwardWraps (t * testing.T ) {
735+ m , _ := diagJumpModel (t )
736+ want := []struct { row , col int }{{6 , 2 }, {4 , 5 }, {2 , 0 }, {6 , 2 }}
737+ for i , w := range want {
738+ updated , _ := m .jumpDiagnostic (false )
739+ m = updated .(model )
740+ p := m .bufs .Active ()
741+ if p .CursorRow () != w .row || p .CursorCol () != w .col {
742+ t .Fatalf ("backward step %d: cursor at (%d,%d), want (%d,%d)" , i , p .CursorRow (), p .CursorCol (), w .row , w .col )
743+ }
744+ }
745+ }
746+
747+ // TestJumpDiagnosticF8KeyWiring proves the F8 / Alt+F8 keys are routed into
748+ // jumpDiagnostic through the real Update path, not just the helper.
749+ func TestJumpDiagnosticF8KeyWiring (t * testing.T ) {
750+ m , _ := diagJumpModel (t )
751+ updated , _ := m .Update (tea.KeyMsg {Type : tea .KeyF8 })
752+ m = updated .(model )
753+ if p := m .bufs .Active (); p .CursorRow () != 2 {
754+ t .Fatalf ("F8 should move cursor to first diagnostic row 2, got %d" , p .CursorRow ())
755+ }
756+ updated , _ = m .Update (tea.KeyMsg {Type : tea .KeyF8 , Alt : true })
757+ m = updated .(model )
758+ if p := m .bufs .Active (); p .CursorRow () != 6 {
759+ t .Fatalf ("Alt+F8 from row 2 should wrap back to last diagnostic row 6, got %d" , p .CursorRow ())
760+ }
761+ }
762+
763+ // TestJumpDiagnosticNoDiagnostics confirms the jump is a no-op with a status
764+ // hint when the buffer carries no diagnostics — the mutation-proof: strip the
765+ // seeded map and the cursor must not move.
766+ func TestJumpDiagnosticNoDiagnostics (t * testing.T ) {
767+ m , path := diagJumpModel (t )
768+ delete (m .diagnostics , path )
769+ if got := m .diagnosticPositions (); got != nil {
770+ t .Fatalf ("diagnosticPositions should be nil with no diagnostics, got %v" , got )
771+ }
772+ before := m .bufs .Active ().CursorRow ()
773+ updated , _ := m .jumpDiagnostic (true )
774+ m = updated .(model )
775+ if p := m .bufs .Active (); p .CursorRow () != before {
776+ t .Fatalf ("cursor moved from %d to %d despite no diagnostics" , before , p .CursorRow ())
777+ }
778+ if m .status != "no diagnostics in buffer" {
779+ t .Fatalf ("status = %q, want no-diagnostics hint" , m .status )
780+ }
781+ }
782+
783+ // TestDiagnosticPositionsSortedDeduped proves the position list is sorted and
784+ // collapses duplicate diagnostics sharing one (row,col).
785+ func TestDiagnosticPositionsSortedDeduped (t * testing.T ) {
786+ root := t .TempDir ()
787+ path := filepath .Join (root , "a.go" )
788+ if err := os .WriteFile (path , []byte ("package main\n \n func a() {}\n " ), 0o644 ); err != nil {
789+ t .Fatal (err )
790+ }
791+ m := newModel (root )
792+ m .bufs .OpenOrSwitch (path )
793+ m .diagnostics [path ] = []protocol.Diagnostic {
794+ {Range : protocol.Range {Start : protocol.Position {Line : 2 , Character : 5 }}, Message : "dup2" },
795+ {Range : protocol.Range {Start : protocol.Position {Line : 0 , Character : 0 }}, Message : "first" },
796+ {Range : protocol.Range {Start : protocol.Position {Line : 2 , Character : 5 }}, Message : "dup1" },
797+ }
798+ got := m .diagnosticPositions ()
799+ want := []diagPos {{0 , 0 }, {2 , 5 }}
800+ if len (got ) != len (want ) {
801+ t .Fatalf ("positions len=%d want %d (%v)" , len (got ), len (want ), got )
802+ }
803+ for i := range want {
804+ if got [i ] != want [i ] {
805+ t .Fatalf ("position %d = %v, want %v" , i , got [i ], want [i ])
806+ }
807+ }
808+ }
809+
693810// TestTabFlowMultipleBuffers exercises the full v0.4.0 tab story end-to-end:
694811// open three buffers, walk forward/back with Alt+]/Alt+[, close with Ctrl+W,
695812// and verify the welcome card returns when the last buffer closes.
0 commit comments