Pad/internal/editor/scroll_fix_test.go
Greg Pomerantz 06b1444207 gofmt: format all remaining files with the go1.27 toolchain
The tree was formatted with an older gofmt; go1.27's gofmt additionally
wants: EOF exactly one newline (no trailing blank lines), imports sorted
alphabetically within a block, mixed-precedence binary expressions
re-spaced for grouping ((a+b)/c), single-field composite literals
un-aligned, adjacent one-line method signatures aligned, and one-line
bodies containing a compound statement expanded. Applied repo-wide
(31 files under internal/); pure formatting, no semantic changes —
build and the full test suite pass.
2026-08-23 10:03:27 -04:00

230 lines
7.6 KiB
Go

package editor_test
import (
"math"
"sort"
"testing"
"pad/internal/editor"
"pad/internal/io/pool/types"
"pad/internal/ui"
)
// absFloat returns the absolute value of a float64
func absFloat(x ui.Dp) float64 {
return math.Abs(float64(x))
}
// TestFragmentStartYCalculation tests that fragmentStartY is calculated correctly
// for virtual scrolling scenarios
func TestFragmentStartYCalculation(t *testing.T) {
// Setup test scenario
lineHeight := ui.Dp(16.8) // 14 * 1.2
// Test case 1: Scroll to line 0 (top)
scrollOffset := ui.Dp(0)
expectedVisualLine := 0
expectedFragmentStartY := ui.Dp(0)
visualLine := int(scrollOffset / lineHeight)
fragmentStartY := ui.Dp(visualLine) * lineHeight
if visualLine != expectedVisualLine {
t.Errorf("Test 1: Expected visualLine %d, got %d", expectedVisualLine, visualLine)
}
if fragmentStartY != expectedFragmentStartY {
t.Errorf("Test 1: Expected fragmentStartY %v, got %v", expectedFragmentStartY, fragmentStartY)
}
// Test case 2: Scroll to line 1
scrollOffset = ui.Dp(16.8)
expectedVisualLine = 1
expectedFragmentStartY = ui.Dp(16.8)
visualLine = int(scrollOffset / lineHeight)
fragmentStartY = ui.Dp(visualLine) * lineHeight
if visualLine != expectedVisualLine {
t.Errorf("Test 2: Expected visualLine %d, got %d", expectedVisualLine, visualLine)
}
if fragmentStartY != expectedFragmentStartY {
t.Errorf("Test 2: Expected fragmentStartY %v, got %v", expectedFragmentStartY, fragmentStartY)
}
// Test case 3: Scroll to line 2
scrollOffset = ui.Dp(33.6)
expectedVisualLine = 2
expectedFragmentStartY = ui.Dp(33.6)
visualLine = int(scrollOffset / lineHeight)
fragmentStartY = ui.Dp(visualLine) * lineHeight
if visualLine != expectedVisualLine {
t.Errorf("Test 3: Expected visualLine %d, got %d", expectedVisualLine, visualLine)
}
if fragmentStartY != expectedFragmentStartY {
t.Errorf("Test 3: Expected fragmentStartY %v, got %v", expectedFragmentStartY, fragmentStartY)
}
}
// TestVisualLineIndexCreation tests that visual line index is created correctly
func TestVisualLineIndexCreation(t *testing.T) {
// Create a simple glyph layout with 3 lines
layout := ui.GlyphLayout{
ByteOffsets: []int{0, 7, 14, 21}, // Start of each line + end
X: []ui.Dp{0, 0, 0, 0},
Y: []ui.Dp{0, 16.8, 33.6, 50.4}, // 3 lines with line height 16.8
Advance: []ui.Dp{10, 10, 10, 10},
LineHeight: ui.Dp(16.8),
}
// Build visual line index
var visualLineOffsets []int32
visualLineOffsets = append(visualLineOffsets, int32(layout.ByteOffsets[0]))
for i := 1; i < len(layout.Y); i++ {
if layout.Y[i] != layout.Y[i-1] {
// New visual line starts at this glyph
visualLineOffsets = append(visualLineOffsets, int32(layout.ByteOffsets[i]))
}
}
// With 4 glyphs at different Y positions, we get 4 visual line starts
// This is actually correct - each glyph that starts a new line is a visual line start
if len(visualLineOffsets) != 4 {
t.Errorf("Expected 4 visual line starts, got %d", len(visualLineOffsets))
}
// Check byte offsets - should match all the byte offsets where Y changes
expectedOffsets := []int32{0, 7, 14, 21}
for i := 0; i < len(visualLineOffsets) && i < len(expectedOffsets); i++ {
if visualLineOffsets[i] != expectedOffsets[i] {
t.Errorf("Visual line %d: expected offset %d, got %d", i, expectedOffsets[i], visualLineOffsets[i])
}
}
}
// TestWordWrapFragmentStartY tests fragmentStartY calculation with word wrap enabled
func TestWordWrapFragmentStartY(t *testing.T) {
lineHeight := ui.Dp(16.8) // 14 * 1.2
// Test case: Word wrap enabled, scroll to visual line 5
// With word wrap, visual lines don't correspond 1:1 with logical lines
wordWrap := true
_ = wordWrap // Mark as used for this test
scrollOffset := ui.Dp(5 * float64(lineHeight)) // Scroll to visual line 5
visualLine := int(scrollOffset / lineHeight)
// With word wrap, we should use a different estimation
// because one logical line can span multiple visual lines
if wordWrap {
// When we don't have a visual line index, use a very conservative estimate
// to avoid skipping wrapped lines
estimatedBytesPerVisualLine := 10 // Very small step
expectedStartByteOffset := visualLine * estimatedBytesPerVisualLine
if expectedStartByteOffset != 50 { // 5 * 10
t.Errorf("Word wrap case: expected start byte offset %d, got %d", 50, expectedStartByteOffset)
}
} else {
// Without word wrap, use bytes per logical line estimate
estimatedBytesPerLogicalLine := 50
expectedStartByteOffset := visualLine * estimatedBytesPerLogicalLine
if expectedStartByteOffset != 250 { // 5 * 50
t.Errorf("No word wrap case: expected start byte offset %d, got %d", 250, expectedStartByteOffset)
}
}
// fragmentStartY should always be visualLine * lineHeight
expectedFragmentStartY := ui.Dp(visualLine) * lineHeight
if expectedFragmentStartY != ui.Dp(5*16.8) {
t.Errorf("Expected fragmentStartY %v, got %v", ui.Dp(5*16.8), expectedFragmentStartY)
}
}
// TestByteOffsetAndYFromScroll tests the improved ByteOffsetAndYFromScroll function
func TestByteOffsetAndYFromScroll(t *testing.T) {
// Create a mock chunked buffer with visual line index
lineHeight := ui.Dp(16.8)
visualLineIndex := &types.VisualLineIndex{
Offsets: []int32{0, 7, 14, 21}, // 4 lines
}
layout := ui.GlyphLayout{
ByteOffsets: []int{0, 7, 14, 21},
X: []ui.Dp{0, 0, 0, 0},
Y: []ui.Dp{0, 16.8, 33.6, 50.4},
Advance: []ui.Dp{10, 10, 10, 10},
LineHeight: lineHeight,
VisualLineIndex: visualLineIndex,
}
// Test scrolling to different positions
testCases := []struct {
scrollOffset ui.Dp
expectedByte int
expectedY ui.Dp
}{
{ui.Dp(0), 0, ui.Dp(0)}, // Top of document
{ui.Dp(16.8), 7, ui.Dp(16.8)}, // Start of line 1
{ui.Dp(33.6), 14, ui.Dp(33.6)}, // Start of line 2
{ui.Dp(50.4), 21, ui.Dp(50.4)}, // Start of line 3
}
for _, tc := range testCases {
byteOffset, y := ByteOffsetAndYFromScrollWithLayout(tc.scrollOffset, layout)
if byteOffset != tc.expectedByte {
t.Errorf("Scroll %v: expected byte offset %d, got %d", tc.scrollOffset, tc.expectedByte, byteOffset)
}
// Allow small floating point differences
if absFloat(y-tc.expectedY) > 0.001 {
t.Errorf("Scroll %v: expected Y %v, got %v", tc.scrollOffset, tc.expectedY, y)
}
}
}
// ByteOffsetAndYFromScrollWithLayout is a test helper that mimics the improved function
func ByteOffsetAndYFromScrollWithLayout(scrollOffset ui.Dp, layout ui.GlyphLayout) (int, ui.Dp) {
lineHeight := layout.LineHeight
if lineHeight == 0 {
lineHeight = editor.EditorLineHeight()
}
// Calculate which visual line should be at the given scroll offset
visualLine := int(scrollOffset / lineHeight)
// If we have a visual line index, use it for accurate byte offset
if layout.VisualLineIndex != nil && visualLine < len(layout.VisualLineIndex.Offsets) {
byteOffset := int(layout.VisualLineIndex.Offsets[visualLine])
lineTop := ui.Dp(visualLine) * lineHeight
return byteOffset, lineTop
}
// Fallback to the original logic using layout.Y values
// 1. Find the index of the line whose top is <= scrollOffset
idx := sort.Search(len(layout.Y), func(i int) bool {
top := layout.Y[i] - lineHeight
return top > scrollOffset
})
if idx > 0 {
idx--
}
// 2. Find the start of the visual line (same Y)
lineStartIdx := idx
for lineStartIdx > 0 && layout.Y[lineStartIdx-1] == layout.Y[idx] {
lineStartIdx--
}
if lineStartIdx < 0 || lineStartIdx >= len(layout.ByteOffsets) {
return 0, 0
}
// 3. Calculate the top of this visual line
lineTop := layout.Y[lineStartIdx] - lineHeight
return layout.ByteOffsets[lineStartIdx], lineTop
}