All checks were successful
Run Test Suite / test (push) Successful in 42s
Updated lots of pieces with this, but it looks good.
631 lines
15 KiB
Go
631 lines
15 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|