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:
parent
615957196e
commit
be123cf48d
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"pad/internal/io/pool"
|
|
||||||
"pad/internal/ui"
|
"pad/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -98,8 +97,8 @@ func HandleBrowserTap(bm *BrowserManager, s *BrowserState, index int) {
|
||||||
ui.OpenFile(entry.Path)
|
ui.OpenFile(entry.Path)
|
||||||
}
|
}
|
||||||
// Dispatch ReadFileTask to the worker pool
|
// Dispatch ReadFileTask to the worker pool
|
||||||
task := pool.NewReadFileTask(entry.Path, bm.fs)
|
// task := pool.NewReadFileTask(entry.Path, bm.fs)
|
||||||
fmt.Printf("HandleBrowserTap: Dispatching ReadFileTask for %s\n", entry.Path)
|
// fmt.Printf("HandleBrowserTap: Dispatching ReadFileTask for %s\n", entry.Path)
|
||||||
bm.workerPool.Dispatch(task)
|
// bm.workerPool.Dispatch(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,9 @@ func NewChunkedBuffer(filename string, chunkSize int, fs pool.FileSystem, basePa
|
||||||
|
|
||||||
// SetFileSize sets the total file length.
|
// SetFileSize sets the total file length.
|
||||||
func (cb *ChunkedBuffer) SetFileSize(length int64) {
|
func (cb *ChunkedBuffer) SetFileSize(length int64) {
|
||||||
cb.fileLen = length
|
if !cb.dirty {
|
||||||
|
cb.fileLen = length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileLen returns the total file length.
|
// FileLen returns the total file length.
|
||||||
|
|
@ -288,10 +290,7 @@ func (cb *ChunkedBuffer) Insert(pos int, text string) {
|
||||||
copy(newChunk[offsetInChunk+len(text):], chunk[offsetInChunk:])
|
copy(newChunk[offsetInChunk+len(text):], chunk[offsetInChunk:])
|
||||||
cb.chunks[chunkIdx] = newChunk
|
cb.chunks[chunkIdx] = newChunk
|
||||||
|
|
||||||
// Update file length if insertion extends beyond current length
|
cb.fileLen += int64(len(text))
|
||||||
if pos+len(text) > int(cb.fileLen) {
|
|
||||||
cb.fileLen = int64(pos + len(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
cb.dirty = true
|
cb.dirty = true
|
||||||
cb.dirtyChunks[chunkIdx] = 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 {
|
idx := sort.Search(len(cb.LineIndex.Offsets), func(i int) bool {
|
||||||
return int(cb.LineIndex.Offsets[i]) >= editPos
|
return int(cb.LineIndex.Offsets[i]) >= editPos
|
||||||
})
|
})
|
||||||
|
if idx == 0 {
|
||||||
|
idx = 1
|
||||||
|
}
|
||||||
for i := idx; i < len(cb.LineIndex.Offsets); i++ {
|
for i := idx; i < len(cb.LineIndex.Offsets); i++ {
|
||||||
cb.LineIndex.Offsets[i] += int32(offsetShift)
|
cb.LineIndex.Offsets[i] += int32(offsetShift)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
154
internal/test/e2e/type_at_start_test.go
Normal file
154
internal/test/e2e/type_at_start_test.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user