Fix redundant file loading and buffer insertion bugs

- Fix ChunkedBuffer.Insert to always update file length.
- Fix UpdateLineIndexAfterEdit to not shift the first line offset.
- Remove redundant ReadFileTask dispatch in HandleBrowserTap.
- Add end-to-end test for typing at start of buffer.
This commit is contained in:
Greg Pomerantz 2026-06-05 10:45:48 -04:00
parent 615957196e
commit be123cf48d
3 changed files with 164 additions and 9 deletions

View File

@ -5,7 +5,6 @@ import (
"path/filepath"
"strings"
"pad/internal/io/pool"
"pad/internal/ui"
)
@ -98,8 +97,8 @@ func HandleBrowserTap(bm *BrowserManager, s *BrowserState, index int) {
ui.OpenFile(entry.Path)
}
// Dispatch ReadFileTask to the worker pool
task := pool.NewReadFileTask(entry.Path, bm.fs)
fmt.Printf("HandleBrowserTap: Dispatching ReadFileTask for %s\n", entry.Path)
bm.workerPool.Dispatch(task)
// task := pool.NewReadFileTask(entry.Path, bm.fs)
// fmt.Printf("HandleBrowserTap: Dispatching ReadFileTask for %s\n", entry.Path)
// bm.workerPool.Dispatch(task)
}
}

View File

@ -59,8 +59,10 @@ func NewChunkedBuffer(filename string, chunkSize int, fs pool.FileSystem, basePa
// SetFileSize sets the total file length.
func (cb *ChunkedBuffer) SetFileSize(length int64) {
if !cb.dirty {
cb.fileLen = length
}
}
// FileLen returns the total file length.
func (cb *ChunkedBuffer) FileLen() int64 {
@ -288,10 +290,7 @@ func (cb *ChunkedBuffer) Insert(pos int, text string) {
copy(newChunk[offsetInChunk+len(text):], chunk[offsetInChunk:])
cb.chunks[chunkIdx] = newChunk
// Update file length if insertion extends beyond current length
if pos+len(text) > int(cb.fileLen) {
cb.fileLen = int64(pos + len(text))
}
cb.fileLen += int64(len(text))
cb.dirty = true
cb.dirtyChunks[chunkIdx] = true
@ -538,6 +537,9 @@ func (cb *ChunkedBuffer) UpdateLineIndexAfterEdit(editPos int, offsetShift int)
idx := sort.Search(len(cb.LineIndex.Offsets), func(i int) bool {
return int(cb.LineIndex.Offsets[i]) >= editPos
})
if idx == 0 {
idx = 1
}
for i := idx; i < len(cb.LineIndex.Offsets); i++ {
cb.LineIndex.Offsets[i] += int32(offsetShift)
}

View File

@ -0,0 +1,154 @@
package e2e_test
import (
"strings"
"testing"
"time"
"pad/internal/editor"
"pad/internal/test/e2e"
"pad/internal/ui"
)
// TestTypeAtStartOfBuffer verifies that typing at cursor position 0 inserts
// characters at the start of the buffer without deleting characters from the end,
// and that they actually appear on screen (in the visible TextField).
func TestTypeAtStartOfBuffer(t *testing.T) {
h := e2e.NewHarnessWithDefaults()
h.Run()
defer h.Cleanup()
// 1. Navigate to browser, then open a file.
editor.GoToBrowser(nil)
time.Sleep(200 * time.Millisecond)
filename := "/notes.txt"
editor.OpenFile(filename)
// 2. Wait for the file content to be loaded into the chunked buffer and line index built.
loaded := false
for i := 0; i < 30; i++ {
cb := h.State().Editor.ChunkedBuffer
if cb != nil && cb.FileLen() > 0 && cb.LineIndex != nil {
loaded = true
break
}
time.Sleep(100 * time.Millisecond)
}
if !loaded {
t.Fatal("timed out waiting for file to load and line index to build")
}
// Record original file length and content
cb := h.State().Editor.ChunkedBuffer
originalLen := int(cb.FileLen())
originalContent, err := cb.FullContent()
if err != nil {
t.Fatalf("failed to read original content: %v", err)
}
originalTail := originalContent[max(0, originalLen-20):]
// Confirm cursor is at position 0 (top of file).
if h.State().Editor.CursorPosition != 0 {
t.Fatalf("expected cursor at 0, got %d", h.State().Editor.CursorPosition)
}
// Get initial frame to verify the text field content before typing.
initialFrame := e2e.GetLastFrame(h)
var initialTextField *ui.TextField
for _, elem := range initialFrame {
if tf, ok := elem.(ui.TextField); ok && tf.ID() == "editor_text" {
initialTextField = &tf
break
}
}
if initialTextField == nil {
t.Fatal("could not find editor text field in initial frame")
}
if initialTextField.Value != originalContent {
t.Fatalf("expected initial text field to contain full original content, got length %d vs %d",
len(initialTextField.Value), len(originalContent))
}
// 3. Type some text at the start of the buffer.
typedText := "AAA "
h.SendInput([]ui.InputEvent{
{
Handler: func(data any) {
editor.HandleInsert(typedText)
},
Data: nil,
},
})
// 4. Wait for a new frame to be captured.
time.Sleep(200 * time.Millisecond)
// 5. Assert on the buffer content.
newContent, err := h.State().Editor.ChunkedBuffer.FullContent()
if err != nil {
t.Fatalf("failed to read content after insert: %v", err)
}
// 5a. The typed text must be at the very start of full content.
if !strings.HasPrefix(newContent, typedText) {
t.Errorf("expected full content to start with %q, got prefix %q",
typedText, newContent[:min(len(typedText), len(newContent))])
}
// 5b. The original tail must still be present at the end of full content.
if !strings.HasSuffix(newContent, originalTail) {
t.Errorf("expected full content to end with original tail %q, got suffix %q",
originalTail, newContent[len(newContent)-len(originalTail):])
}
// 5c. The total length of full content should be originalLen + len(typedText).
expectedLen := originalLen + len(typedText)
if len(newContent) != expectedLen {
t.Errorf("expected full content length %d, got %d", expectedLen, len(newContent))
}
// 6. Assert on the RENDERED TextField (visible content on screen).
finalFrame := e2e.GetLastFrame(h)
var finalTextField *ui.TextField
for _, elem := range finalFrame {
if tf, ok := elem.(ui.TextField); ok && tf.ID() == "editor_text" {
finalTextField = &tf
break
}
}
if finalTextField == nil {
t.Fatal("could not find editor text field in final frame")
}
// 6a. The typed characters must appear at the start of the text field.
if !strings.HasPrefix(finalTextField.Value, typedText) {
t.Errorf("BUG: typed characters do not appear at the start of the text field. Expected prefix %q, got %q",
typedText, finalTextField.Value[:min(len(typedText), len(finalTextField.Value))])
}
// 6b. The end of the file must not be truncated in the text field.
if !strings.HasSuffix(finalTextField.Value, originalTail) {
t.Errorf("BUG: characters at the end of the text field disappeared. Expected suffix %q, got %q",
originalTail, finalTextField.Value[max(0, len(finalTextField.Value)-len(originalTail)):])
}
// 6c. The visible text field length must be correct.
if len(finalTextField.Value) != expectedLen {
t.Errorf("expected visible text field length %d, got %d", expectedLen, len(finalTextField.Value))
}
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}