package editor import ( "strings" "testing" tea "github.com/charmbracelet/bubbletea" ) func cacheDoc() *Editor { e := New() var b strings.Builder for i := 0; i < 200; i++ { b.WriteString("Some **prose** line with a [link](http://x) and `code` here.\n") } e.SetContent([]byte(b.String())) e.SetSize(80, 24) return e } // Scrolling and rendering must not re-scan the whole document: the visual model // depends only on content/width/theme/spell, none of which a scroll changes. func TestScrollReusesVisualCache(t *testing.T) { e := cacheDoc() e.buildCount = 0 for i := 0; i < 10; i++ { e.ScrollBy(1) _ = e.View() } if e.buildCount > 1 { t.Errorf("scroll+view rebuilt visual %d times, want <= 1", e.buildCount) } } // An edit must invalidate the cache so the next render reflects new content. func TestEditInvalidatesCache(t *testing.T) { e := cacheDoc() _ = e.View() // prime cache e.SetContent([]byte("brand new content\n")) out := e.View() if !strings.Contains(out, "brand new content") { t.Errorf("View after SetContent did not reflect new content:\n%s", out) } } // A bracketed-paste KeyMsg delivers the whole paste as one multi-rune // tea.KeyMsg. Each rune it inserts must not rebuild the visual cache off a // half-inserted buffer (that stale cache never gets invalidated again, so the // screen would freeze on the first pasted character even though e.Lines has // the rest of the text). func TestPasteKeyMsgReflectsFullTextImmediately(t *testing.T) { e := New() e.SetContent([]byte("")) e.SetSize(80, 24) e.HandleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("hello world"), Paste: true}) if e.Lines[0] != "hello world" { t.Fatalf("Lines[0] = %q, want %q", e.Lines[0], "hello world") } if out := e.View(); !strings.Contains(out, "hello world") { t.Errorf("View after paste did not reflect full pasted text, only:\n%s", out) } } // A multi-line paste's embedded newlines must split into separate logical // lines, not sit as literal '\n' runes inside one Lines[] entry. func TestPasteKeyMsgSplitsEmbeddedNewlines(t *testing.T) { e := New() e.SetContent([]byte("")) e.SetSize(80, 24) e.HandleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("line1\nline2"), Paste: true}) want := []string{"line1", "line2"} if len(e.Lines) != len(want) || e.Lines[0] != want[0] || e.Lines[1] != want[1] { t.Fatalf("Lines = %#v, want %#v", e.Lines, want) } } // InsertText (the explicit Ctrl+V clipboard-paste path) shares the same bulk // insert and must be equally immune to the stale-cache and embedded-newline // bugs. func TestInsertTextReflectsFullTextImmediately(t *testing.T) { e := New() e.SetContent([]byte("")) e.SetSize(80, 24) e.InsertText("line1\nline2") if len(e.Lines) != 2 || e.Lines[0] != "line1" || e.Lines[1] != "line2" { t.Fatalf("Lines = %#v, want [line1 line2]", e.Lines) } if out := e.View(); !strings.Contains(out, "line1") || !strings.Contains(out, "line2") { t.Errorf("View after InsertText did not reflect full text:\n%s", out) } }