package core import "testing" // -------------------------------------------------- // Window Tests (generated by ClaudeCode) // -------------------------------------------------- func TestWindow_SetCursorLine(t *testing.T) { t.Run("clamps cursor below zero", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"line 1", "line 2", "line 3"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorLine(-5) if win.Cursor.Line != 0 { t.Errorf("expected cursor at line 0, got %d", win.Cursor.Line) } }) t.Run("clamps cursor past end", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"line 1", "line 2", "line 3"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorLine(999) if win.Cursor.Line != 2 { // 3 lines, max index is 2 t.Errorf("expected cursor at line 2, got %d", win.Cursor.Line) } }) t.Run("allows valid position", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"line 1", "line 2", "line 3"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorLine(1) if win.Cursor.Line != 1 { t.Errorf("expected cursor at line 1, got %d", win.Cursor.Line) } }) t.Run("handles empty buffer", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorLine(5) if win.Cursor.Line != 0 { t.Errorf("expected cursor at line 0 for empty buffer, got %d", win.Cursor.Line) } }) } func TestWindow_SetCursorCol(t *testing.T) { t.Run("clamps to line length", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"hello"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorCol(999) // "hello" is 5 chars, max col should be 5 (after last char for insert mode) if win.Cursor.Col > 5 { t.Errorf("expected cursor col <= 5, got %d", win.Cursor.Col) } }) t.Run("clamps below zero", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"hello"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorCol(-10) if win.Cursor.Col != 0 { t.Errorf("expected cursor col 0, got %d", win.Cursor.Col) } }) t.Run("handles empty line", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{""}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorCol(5) if win.Cursor.Col != 0 { t.Errorf("expected cursor at col 0 on empty line, got %d", win.Cursor.Col) } }) t.Run("allows cursor at end of line", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"hello"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetCursorCol(5) // After last char if win.Cursor.Col != 5 { t.Errorf("expected cursor at col 5, got %d", win.Cursor.Col) } }) } func TestWindow_AdjustScroll(t *testing.T) { t.Run("scrolls down when cursor goes below viewport", func(t *testing.T) { // Create buffer with many lines lines := make([]string, 100) for i := range lines { lines[i] = "line" } buf := NewBufferBuilder().WithLines(lines).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(24). Build() // Start at top win.SetCursorLine(0) win.AdjustScroll() initialScroll := win.ScrollY // Move cursor way down win.SetCursorLine(50) win.AdjustScroll() // Scroll should have increased if win.ScrollY <= initialScroll { t.Errorf("expected scroll to increase, was %d, now %d", initialScroll, win.ScrollY) } // Cursor should be visible viewport := win.ViewportHeight() if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport { t.Errorf("cursor at %d not visible in scroll range [%d, %d)", win.Cursor.Line, win.ScrollY, win.ScrollY+viewport) } }) t.Run("scrolls up when cursor goes above viewport", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = "line" } buf := NewBufferBuilder().WithLines(lines).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(24). Build() // Start at bottom win.SetCursorLine(80) win.AdjustScroll() initialScroll := win.ScrollY // Move cursor to top win.SetCursorLine(5) win.AdjustScroll() // Scroll should have decreased if win.ScrollY >= initialScroll { t.Errorf("expected scroll to decrease, was %d, now %d", initialScroll, win.ScrollY) } // Cursor should be visible viewport := win.ViewportHeight() if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport { t.Errorf("cursor at %d not visible in scroll range [%d, %d)", win.Cursor.Line, win.ScrollY, win.ScrollY+viewport) } }) t.Run("respects scrolloff margin", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = "line" } buf := NewBufferBuilder().WithLines(lines).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(24). Build() // Set scrolloff to 5 opts := win.Options opts.ScrollOff = 5 win.SetOptions(opts) // Move to line 20 and adjust win.SetCursorLine(20) win.AdjustScroll() viewport := win.ViewportHeight() distFromTop := win.Cursor.Line - win.ScrollY distFromBottom := (win.ScrollY + viewport - 1) - win.Cursor.Line // At least one should respect scrolloff if distFromTop < opts.ScrollOff && distFromBottom < opts.ScrollOff { t.Errorf("scrolloff %d not respected: top=%d, bottom=%d", opts.ScrollOff, distFromTop, distFromBottom) } }) t.Run("handles scrolloff larger than half viewport", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = "line" } buf := NewBufferBuilder().WithLines(lines).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(24). Build() // Set scrolloff larger than half viewport opts := win.Options opts.ScrollOff = 999 win.SetOptions(opts) win.SetCursorLine(50) win.AdjustScroll() // Should not panic or error viewport := win.ViewportHeight() if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport { t.Error("cursor should still be visible with large scrolloff") } }) t.Run("handles small viewport", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = "line" } buf := NewBufferBuilder().WithLines(lines).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(5). // Very small Build() win.SetCursorLine(50) win.AdjustScroll() // Should not panic viewport := win.ViewportHeight() if viewport > 0 && (win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport) { t.Error("cursor should be visible in small viewport") } }) t.Run("scrolls right when cursor moves past visible width", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"0123456789abcdefghij"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). WithWidth(12). WithHeight(10). Build() win.SetCursorCol(10) win.AdjustScroll() if win.ScrollX == 0 { t.Fatal("expected horizontal scroll to move right") } viewport := win.ViewportWidth() if win.Cursor.Col < win.ScrollX || win.Cursor.Col >= win.ScrollX+viewport { t.Errorf("cursor at %d not visible in scroll range [%d, %d)", win.Cursor.Col, win.ScrollX, win.ScrollX+viewport) } }) t.Run("scrolls left when cursor moves back into hidden content", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"0123456789abcdefghij"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). WithWidth(12). WithHeight(10). Build() win.SetCursorCol(14) win.AdjustScroll() if win.ScrollX == 0 { t.Fatal("expected initial horizontal scroll to move right") } win.SetCursorCol(2) win.AdjustScroll() if win.ScrollX != 2 { t.Errorf("expected horizontal scroll to follow cursor left, got %d", win.ScrollX) } }) } func TestWindow_AdjustScrollHorizontalRuneAware(t *testing.T) { tests := []struct { name string line string width int cursorCol int initialScroll int expected int }{ { name: "ascii line scrolls using visible columns", line: "0123456789abcdef", width: 12, cursorCol: 10, expected: 4, }, { name: "multibyte rune line uses rune length not bytes", line: "abécdefghij", width: 10, cursorCol: 10, expected: 6, }, { name: "moving left pulls scroll back toward cursor", line: "abécdefghij", width: 10, cursorCol: 2, initialScroll: 6, expected: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := NewBufferBuilder().WithLines([]string{tt.line}).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithWidth(tt.width). WithHeight(10). Build() win.ScrollX = tt.initialScroll win.SetCursorCol(tt.cursorCol) win.AdjustScroll() if win.ScrollX != tt.expected { t.Errorf("ScrollX() = %d, want %d", win.ScrollX, tt.expected) } viewport := win.ViewportWidth() if viewport > 0 && (win.Cursor.Col < win.ScrollX || win.Cursor.Col >= win.ScrollX+viewport) { t.Errorf("cursor at %d not visible in scroll range [%d, %d)", win.Cursor.Col, win.ScrollX, win.ScrollX+viewport) } }) } } func TestWindow_ViewportHeight(t *testing.T) { t.Run("calculates viewport height correctly", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(24). Build() // Height - 2 (status bar + command bar) expected := 22 if win.ViewportHeight() != expected { t.Errorf("expected viewport height %d, got %d", expected, win.ViewportHeight()) } }) t.Run("handles small window", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(3). Build() // 3 - 2 = 1 expected := 1 if win.ViewportHeight() != expected { t.Errorf("expected viewport height %d, got %d", expected, win.ViewportHeight()) } }) t.Run("handles zero height", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). WithHeight(0). Build() // With height 0, viewport is 0 - 2 (status + command bars) = -2 // This is an edge case that shouldn't occur in practice, but shouldn't panic result := win.ViewportHeight() expected := -2 if result != expected { t.Errorf("expected viewport height %d for zero height window, got %d", expected, result) } }) } func TestWindow_ViewportWidth(t *testing.T) { t.Run("subtracts gutter width", func(t *testing.T) { buf := NewBufferBuilder().WithLines([]string{"line"}).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithWidth(20). Build() expected := 15 if win.ViewportWidth() != expected { t.Errorf("expected viewport width %d, got %d", expected, win.ViewportWidth()) } }) t.Run("returns full width when gutter disabled", func(t *testing.T) { buf := NewBufferBuilder().WithLines([]string{"line"}).Build() win := NewWindowBuilder(). WithBuffer(&buf). WithWidth(20). WithOptions(WinOptions{Number: false, RelativeNumber: false, GutterSize: 5, ScrollOff: 8}). Build() if win.ViewportWidth() != 20 { t.Errorf("expected viewport width 20, got %d", win.ViewportWidth()) } }) } func TestWindow_SetOptions(t *testing.T) { t.Run("updates options", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() newOpts := WinOptions{ Number: false, RelativeNumber: false, GutterSize: 10, ScrollOff: 3, } win.SetOptions(newOpts) if win.Options.Number != false { t.Error("expected Number to be false") } if win.Options.RelativeNumber != false { t.Error("expected RelativeNumber to be false") } if win.Options.GutterSize != 10 { t.Errorf("expected GutterSize 10, got %d", win.Options.GutterSize) } if win.Options.ScrollOff != 3 { t.Errorf("expected ScrollOff 3, got %d", win.Options.ScrollOff) } }) t.Run("can toggle individual options", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() // Get current options opts := win.Options originalNumber := opts.Number // Toggle number opts.Number = !opts.Number win.SetOptions(opts) if win.Options.Number == originalNumber { t.Error("Number option should have toggled") } }) } func TestWindow_SetAnchor(t *testing.T) { t.Run("sets anchor line", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"a", "b", "c"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetAnchorLine(2) if win.Anchor.Line != 2 { t.Errorf("expected anchor line 2, got %d", win.Anchor.Line) } }) t.Run("sets anchor col", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetAnchorCol(5) if win.Anchor.Col != 5 { t.Errorf("expected anchor col 5, got %d", win.Anchor.Col) } }) } func TestWindow_SetDimensions(t *testing.T) { t.Run("updates width and height", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() win.SetDimensions(100, 50) if win.Width != 100 { t.Errorf("expected width 100, got %d", win.Width) } if win.Height != 50 { t.Errorf("expected height 50, got %d", win.Height) } }) } func TestWindowBuilder(t *testing.T) { t.Run("builds with defaults", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). Build() // Should have default options if win.Options.Number != true { t.Error("expected default Number to be true") } if win.Options.RelativeNumber != true { t.Error("expected default RelativeNumber to be true") } if win.Options.ScrollOff != 8 { t.Errorf("expected default ScrollOff 8, got %d", win.Options.ScrollOff) } if win.Options.GutterSize != 5 { t.Errorf("expected default GutterSize 5, got %d", win.Options.GutterSize) } }) t.Run("builds with custom cursor position", func(t *testing.T) { buf := NewBufferBuilder(). WithLines([]string{"a", "b", "c"}). Build() win := NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(2, 0). Build() if win.Cursor.Line != 2 { t.Errorf("expected cursor line 2, got %d", win.Cursor.Line) } if win.Cursor.Col != 0 { t.Errorf("expected cursor col 0, got %d", win.Cursor.Col) } }) t.Run("builds with custom dimensions", func(t *testing.T) { buf := NewBufferBuilder().Build() win := NewWindowBuilder(). WithBuffer(&buf). WithDimensions(120, 40). Build() if win.Width != 120 { t.Errorf("expected width 120, got %d", win.Width) } if win.Height != 40 { t.Errorf("expected height 40, got %d", win.Height) } }) t.Run("assigns unique IDs", func(t *testing.T) { buf := NewBufferBuilder().Build() win1 := NewWindowBuilder().WithBuffer(&buf).Build() win2 := NewWindowBuilder().WithBuffer(&buf).Build() if win1.Id == win2.Id { t.Errorf("expected unique IDs, both were %d", win1.Id) } }) }