diff --git a/internal/browser/handlers.go b/internal/browser/handlers.go index 3e62fbc..f6dc03a 100644 --- a/internal/browser/handlers.go +++ b/internal/browser/handlers.go @@ -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) } } diff --git a/internal/editor/chunked_buffer.go b/internal/editor/chunked_buffer.go index c378b78..97a7a06 100644 --- a/internal/editor/chunked_buffer.go +++ b/internal/editor/chunked_buffer.go @@ -59,7 +59,9 @@ func NewChunkedBuffer(filename string, chunkSize int, fs pool.FileSystem, basePa // SetFileSize sets the total file length. func (cb *ChunkedBuffer) SetFileSize(length int64) { - cb.fileLen = length + if !cb.dirty { + cb.fileLen = 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:]) 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) } diff --git a/internal/test/e2e/type_at_start_test.go b/internal/test/e2e/type_at_start_test.go new file mode 100644 index 0000000..595d22f --- /dev/null +++ b/internal/test/e2e/type_at_start_test.go @@ -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 +}