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) } } } // --- TASK-023: checkbox toggle --- func TestToggleCheckboxFlipsUnchecked(t *testing.T) { e := New() e.SetContent([]byte("- [ ] task")) e.Cursor = Position{Row: 0, Col: 8} // cursor mid-content, not on brackets if !e.ToggleCheckbox() { t.Fatal("ToggleCheckbox returned false on a checkbox line") } if got := e.Lines[0]; got != "- [x] task" { t.Fatalf("line = %q, want %q", got, "- [x] task") } if !e.Dirty { t.Fatal("toggle did not mark the buffer dirty") } } func TestToggleCheckboxFlipsChecked(t *testing.T) { e := New() e.SetContent([]byte(" 1. [X] done")) e.Cursor = Position{Row: 0, Col: 0} e.ToggleCheckbox() if got := e.Lines[0]; got != " 1. [ ] done" { t.Fatalf("line = %q, want %q", got, " 1. [ ] done") } } func TestToggleCheckboxNoOpOnPlainLine(t *testing.T) { e := New() e.SetContent([]byte("- a bullet, no box")) e.Cursor = Position{Row: 0, Col: 3} if e.ToggleCheckbox() { t.Fatal("ToggleCheckbox returned true on a non-checkbox line") } if got := e.Lines[0]; got != "- a bullet, no box" { t.Fatalf("line mutated to %q", got) } if e.Dirty { t.Fatal("no-op toggle marked the buffer dirty") } } func TestOnCheckboxBracketDetectsGlyph(t *testing.T) { e := New() e.SetContent([]byte("- [ ] task")) // "- [ ] task": brackets at rune cols 2,3,4. e.Cursor = Position{Row: 0, Col: 3} if !e.OnCheckboxBracket() { t.Fatal("cursor on the box glyph not detected") } e.Cursor = Position{Row: 0, Col: 8} if e.OnCheckboxBracket() { t.Fatal("cursor in the content wrongly detected as on the box") } }