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.
230 lines
7.6 KiB
Go
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
|
|
}
|