package editor import ( "testing" tea "github.com/charmbracelet/bubbletea" ) // enter sends an Enter key through the full HandleKey path (undo-aware), the // same way the app drives the editor. func enter(e *Editor) { e.HandleKey(tea.KeyMsg{Type: tea.KeyEnter}) } func tab(e *Editor) { e.HandleKey(tea.KeyMsg{Type: tea.KeyTab}) } func shiftTab(e *Editor) { e.HandleKey(tea.KeyMsg{Type: tea.KeyShiftTab}) } func TestEnterContinuesBulletMarker(t *testing.T) { e := New() e.SetContent([]byte("- first")) e.Cursor = Position{Row: 0, Col: len("- first")} enter(e) if got := e.Lines[1]; got != "- " { t.Fatalf("new line = %q, want %q", got, "- ") } if e.Cursor.Row != 1 || e.Cursor.Col != 2 { t.Fatalf("cursor = %+v, want row 1 col 2", e.Cursor) } } func TestEnterContinuesStarAndPlus(t *testing.T) { for _, m := range []string{"* ", "+ "} { e := New() e.SetContent([]byte(m + "x")) e.Cursor = Position{Row: 0, Col: len(m + "x")} enter(e) if got := e.Lines[1]; got != m { t.Fatalf("marker %q: new line = %q, want %q", m, got, m) } } } func TestEnterIncrementsOrderedNumber(t *testing.T) { e := New() e.SetContent([]byte("1. first")) e.Cursor = Position{Row: 0, Col: len("1. first")} enter(e) if got := e.Lines[1]; got != "2. " { t.Fatalf("new line = %q, want %q", got, "2. ") } } func TestEnterOrderedParenDelimiter(t *testing.T) { e := New() e.SetContent([]byte("3) third")) e.Cursor = Position{Row: 0, Col: len("3) third")} enter(e) if got := e.Lines[1]; got != "4) " { t.Fatalf("new line = %q, want %q", got, "4) ") } } func TestEnterContinuesCheckboxUnchecked(t *testing.T) { e := New() e.SetContent([]byte("- [x] done")) e.Cursor = Position{Row: 0, Col: len("- [x] done")} enter(e) if got := e.Lines[1]; got != "- [ ] " { t.Fatalf("new line = %q, want %q", got, "- [ ] ") } } func TestEnterPreservesIndentation(t *testing.T) { e := New() e.SetContent([]byte(" - nested")) e.Cursor = Position{Row: 0, Col: len(" - nested")} enter(e) if got := e.Lines[1]; got != " - " { t.Fatalf("new line = %q, want %q", got, " - ") } } func TestEnterSplitsContentMidItem(t *testing.T) { e := New() e.SetContent([]byte("- foobar")) e.Cursor = Position{Row: 0, Col: len("- foo")} enter(e) if e.Lines[0] != "- foo" || e.Lines[1] != "- bar" { t.Fatalf("lines = %q / %q, want %q / %q", e.Lines[0], e.Lines[1], "- foo", "- bar") } } func TestEnterOnEmptyItemExitsList(t *testing.T) { e := New() e.SetContent([]byte("- ")) e.Cursor = Position{Row: 0, Col: 2} enter(e) if len(e.Lines) != 1 { t.Fatalf("line count = %d, want 1 (no new line)", len(e.Lines)) } if e.Lines[0] != "" { t.Fatalf("line = %q, want empty (marker removed)", e.Lines[0]) } if e.Cursor.Col != 0 { t.Fatalf("cursor col = %d, want 0", e.Cursor.Col) } } func TestEnterOnEmptyCheckboxExitsList(t *testing.T) { e := New() e.SetContent([]byte("- [ ] ")) e.Cursor = Position{Row: 0, Col: len("- [ ] ")} enter(e) if len(e.Lines) != 1 || e.Lines[0] != "" { t.Fatalf("lines = %v, want one empty line", e.Lines) } } func TestEnterOnNonListInsertsPlainNewline(t *testing.T) { e := New() e.SetContent([]byte("hello")) e.Cursor = Position{Row: 0, Col: 5} enter(e) if len(e.Lines) != 2 || e.Lines[1] != "" { t.Fatalf("lines = %v, want plain split", e.Lines) } } func TestTabIndentsListItem(t *testing.T) { e := New() e.SetContent([]byte("- item")) e.Cursor = Position{Row: 0, Col: 2} tab(e) if e.Lines[0] != " - item" { t.Fatalf("line = %q, want %q", e.Lines[0], " - item") } if e.Cursor.Col != 4 { t.Fatalf("cursor col = %d, want 4", e.Cursor.Col) } } func TestTabOnNonListInsertsTab(t *testing.T) { e := New() e.SetContent([]byte("plain")) e.Cursor = Position{Row: 0, Col: 0} tab(e) if e.Lines[0] != "\tplain" { t.Fatalf("line = %q, want %q", e.Lines[0], "\tplain") } } func TestShiftTabOutdentsListItem(t *testing.T) { e := New() e.SetContent([]byte(" - item")) e.Cursor = Position{Row: 0, Col: 4} shiftTab(e) if e.Lines[0] != "- item" { t.Fatalf("line = %q, want %q", e.Lines[0], "- item") } if e.Cursor.Col != 2 { t.Fatalf("cursor col = %d, want 2", e.Cursor.Col) } } func TestShiftTabOutdentTabIndent(t *testing.T) { e := New() e.SetContent([]byte("\t- item")) e.Cursor = Position{Row: 0, Col: 3} shiftTab(e) if e.Lines[0] != "- item" { t.Fatalf("line = %q, want %q", e.Lines[0], "- item") } } func TestShiftTabAtZeroIndentNoop(t *testing.T) { e := New() e.SetContent([]byte("- item")) e.Cursor = Position{Row: 0, Col: 2} shiftTab(e) if e.Lines[0] != "- item" { t.Fatalf("line = %q, want unchanged", e.Lines[0]) } } func TestListContinuationIsUndoable(t *testing.T) { e := New() e.SetContent([]byte("- first")) e.Cursor = Position{Row: 0, Col: len("- first")} enter(e) e.Undo() if len(e.Lines) != 1 || e.Lines[0] != "- first" { t.Fatalf("after undo lines = %v, want [- first]", e.Lines) } } func TestParseListItemRejectsNonList(t *testing.T) { for _, s := range []string{"", "plain text", "-nodash", "1.no space", " ", "-"} { if _, ok := parseListItem(s); ok { t.Errorf("parseListItem(%q) = ok, want not a list", s) } } }