package editor import ( "bytes" "strings" "testing" ) // newTestBuffer builds a fully-loaded (in-range) buffer from content using the // on-open path (SetContent), so all chunks are resident. func newTestBuffer(t *testing.T, content []byte) *ChunkedBuffer { t.Helper() cb := NewChunkedBuffer("/test.txt", DefaultChunkSize, nil, "") cb.SetContent(content) return cb } // TestSetContent_MultipleChunks verifies the whole-file load splits content // into ordered chunks and Content reconstructs it exactly. func TestSetContent_MultipleChunks(t *testing.T) { content := make([]byte, 200*1024) for i := range content { content[i] = byte(i % 251) } cb := newTestBuffer(t, content) // 200KB / 64KB = 3 full chunks (64KB each) + 1 partial (8KB) = 4 chunks. if got := len(cb.chunks); got != 4 { t.Fatalf("expected 4 chunks, got %d", got) } if cb.FileLen() != int64(len(content)) { t.Fatalf("FileLen=%d, want %d", cb.FileLen(), len(content)) } full, err := cb.FullContent() if err != nil { t.Fatalf("FullContent error: %v", err) } if full != string(content) { t.Fatalf("FullContent mismatch after SetContent") } // Spot-check a range that spans the chunk-0/chunk-1 boundary. seg := cb.Content(64*1024-10, 64*1024+10) if seg != string(content[64*1024-10:64*1024+10]) { t.Fatalf("Content across chunk boundary mismatch: got %q", seg) } } // TestInsertShiftsLaterChunks is the core drift-fix test: an insert in chunk 0 // must shift every later byte, and Content must still return the correct byte // at the shifted offset (the old fixed-slot i*chunkSize model failed here). func TestInsertShiftsLaterChunks(t *testing.T) { content := make([]byte, 200*1024) for i := range content { content[i] = byte(i % 251) } cb := newTestBuffer(t, content) const insLen = 3 cb.Insert(0, strings.Repeat("X", insLen)) // A byte originally at position 100000 (chunk 1) is now at 100003. want := content[100000] if got := cb.Content(100000+insLen, 100001+insLen); got != string(want) { t.Fatalf("byte at shifted offset 100003 = %q, want %q", got, string(want)) } // The inserted prefix is present. if got := cb.Content(0, insLen); got != strings.Repeat("X", insLen) { t.Fatalf("prefix = %q, want %q", got, strings.Repeat("X", insLen)) } // Full content is the insert + original. full, _ := cb.FullContent() if full != strings.Repeat("X", insLen)+string(content) { t.Fatalf("FullContent after insert mismatch (len=%d, want %d)", len(full), insLen+len(content)) } } // TestDeleteShiftsLaterChunks verifies a deletion in an early chunk shifts // later bytes left and Content/FullContent stay exact. func TestDeleteShiftsLaterChunks(t *testing.T) { content := make([]byte, 200*1024) for i := range content { content[i] = byte(i % 251) } cb := newTestBuffer(t, content) const delLen = 100 cb.Delete(0, delLen) // A byte originally at 100000 is now at 99900. want := content[100000] if got := cb.Content(100000-delLen, 100001-delLen); got != string(want) { t.Fatalf("byte at shifted offset = %q, want %q", got, string(want)) } full, _ := cb.FullContent() if full != string(content[delLen:]) { t.Fatalf("FullContent after delete mismatch (len=%d, want %d)", len(full), len(content)-delLen) } } // TestDeleteSpanningChunks verifies a deletion that spans multiple chunk // boundaries is exact. func TestDeleteSpanningChunks(t *testing.T) { content := make([]byte, 200*1024) for i := range content { content[i] = byte(i % 251) } cb := newTestBuffer(t, content) // Delete 64KB+50 bytes starting at 30KB (spans chunks 0,1,2). const start = 30 * 1024 const n = 64*1024 + 50 cb.Delete(start, n) expected := make([]byte, 0, len(content)-n) expected = append(expected, content[:start]...) expected = append(expected, content[start+n:]...) full, _ := cb.FullContent() if !bytes.Equal([]byte(full), expected) { t.Fatalf("FullContent after spanning delete mismatch (len=%d, want %d)", len(full), len(expected)) } if cb.FileLen() != int64(len(expected)) { t.Fatalf("FileLen=%d, want %d", cb.FileLen(), len(expected)) } } // TestRuneIndexToByteAfterEdit verifies the IME rune->byte bridge stays correct // after a length-changing edit (ASCII + multibyte). func TestRuneIndexToByteAfterEdit(t *testing.T) { // 2 chunks of ASCII so the edit crosses into chunk arithmetic. var b bytes.Buffer for i := 0; i < 100*1024; i++ { b.WriteByte(byte('a' + i%26)) } cb := newTestBuffer(t, b.Bytes()) // Insert a multibyte rune ("é" = 2 bytes) at rune 0. cb.Insert(0, "é") // Rune 0 is now 'é' (bytes 0..1). Rune 1 is the original first 'a' at byte 2. if got := cb.RuneIndexToByte(0); got != 0 { t.Fatalf("RuneIndexToByte(0)=%d, want 0", got) } if got := cb.RuneIndexToByte(1); got != 2 { t.Fatalf("RuneIndexToByte(1)=%d, want 2", got) } // New rune R (R>0) is the original rune R-1 (originally at byte R-1), now // at byte (R-1)+2 = R+1 (é is 1 rune, 2 bytes). Check one deep in chunk 1 // (past the 64KB boundary) to confirm the shift survives chunk boundaries. deep := 70000 if got := cb.RuneIndexToByte(deep); got != deep+1 { t.Fatalf("RuneIndexToByte(%d)=%d, want %d", deep, got, deep+1) } } // TestContent_BoundsAndEmpty guards edge cases in the rewritten Content. func TestContent_BoundsAndEmpty(t *testing.T) { cb := newTestBuffer(t, []byte("hello world")) if got := cb.Content(0, 5); got != "hello" { t.Fatalf("Content(0,5)=%q, want hello", got) } if got := cb.Content(6, 11); got != "world" { t.Fatalf("Content(6,11)=%q, want world", got) } // Out-of-range end clamps to fileLen. if got := cb.Content(0, 1000); got != "hello world" { t.Fatalf("Content(0,1000)=%q, want full", got) } // start >= end is empty. if got := cb.Content(5, 5); got != "" { t.Fatalf("Content(5,5)=%q, want empty", got) } // Empty buffer. empty := newTestBuffer(t, nil) if got := empty.Content(0, 10); got != "" { t.Fatalf("empty Content=%q, want empty", got) } if got, _ := empty.FullContent(); got != "" { t.Fatalf("empty FullContent=%q, want empty", got) } } // TestAppendAtEnd inserts at the very end of the buffer (pos == fileLen), which // maps to the last chunk's end. func TestAppendAtEnd(t *testing.T) { cb := newTestBuffer(t, []byte("abcdef")) cb.Insert(6, "XYZ") full, _ := cb.FullContent() if full != "abcdefXYZ" { t.Fatalf("append at end = %q, want abcdefXYZ", full) } if cb.FileLen() != 9 { t.Fatalf("FileLen=%d, want 9", cb.FileLen()) } }