package buffer import "testing" func TestInsertPutsTextAtTheCursor(t *testing.T) { b := NewFromString("ab") b.SetCursor(Position{Line: 0, Col: 1}) b.Insert("X") if got := b.Line(0); got != "aXb" { t.Errorf("Line(0) = %q, want %q", got, "aXb") } if got := b.Cursor(); got != (Position{Line: 0, Col: 2}) { t.Errorf("Cursor() = %+v, want the position just after the inserted text", got) } if !b.Modified() { t.Error("inserting must mark the buffer modified") } } func TestInsertSplitsTheLineOnALineFeed(t *testing.T) { b := NewFromString("ab") b.SetCursor(Position{Line: 0, Col: 1}) b.Insert("1\n2") want := []string{"a1", "2b"} if got := b.LineCount(); got != len(want) { t.Fatalf("LineCount() = %d, want %d", got, len(want)) } for i, w := range want { if got := b.Line(i); got != w { t.Errorf("Line(%d) = %q, want %q", i, got, w) } } if got := b.Cursor(); got != (Position{Line: 1, Col: 1}) { t.Errorf("Cursor() = %+v, want {Line:1 Col:1}", got) } } func TestInsertReplacesTheSelection(t *testing.T) { b := NewFromString("hello world") b.SetCursor(Position{Line: 0, Col: 0}) b.StartSelection() b.SetCursorKeepingSelection(Position{Line: 0, Col: 5}) b.Insert("bye") if got := b.Line(0); got != "bye world" { t.Errorf("Line(0) = %q, want %q", got, "bye world") } if _, ok := b.Selection(); ok { t.Error("the selection must be gone after typing over it") } } func TestInsertNewlineAndIndentCarriesTheLeadingWhitespace(t *testing.T) { tests := []struct { name string line string col int wantNext string }{ {"tab indent", "\tfoo()", 6, "\t"}, {"space indent", " foo()", 9, " "}, {"no indent", "foo()", 5, ""}, {"cursor inside the indent", "\t\tfoo()", 1, "\t"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { b := NewFromString(tc.line) b.SetCursor(Position{Line: 0, Col: tc.col}) b.InsertNewlineAndIndent() if got := b.LineCount(); got != 2 { t.Fatalf("LineCount() = %d, want 2", got) } if got := b.Line(1); got[:len(tc.wantNext)] != tc.wantNext { t.Errorf("Line(1) = %q, want it to start with %q", got, tc.wantNext) } if got := b.Cursor().Col; got != len(tc.wantNext) { t.Errorf("Cursor().Col = %d, want %d (just past the indent)", got, len(tc.wantNext)) } }) } } func TestBackspaceRemovesTheCharacterBefore(t *testing.T) { b := NewFromString("abc") b.SetCursor(Position{Line: 0, Col: 2}) b.Backspace() if got := b.Line(0); got != "ac" { t.Errorf("Line(0) = %q, want %q", got, "ac") } if got := b.Cursor(); got != (Position{Line: 0, Col: 1}) { t.Errorf("Cursor() = %+v, want {Line:0 Col:1}", got) } } func TestBackspaceAtColumnZeroJoinsTheLines(t *testing.T) { b := NewFromString("ab\ncd") b.SetCursor(Position{Line: 1, Col: 0}) b.Backspace() if got := b.LineCount(); got != 1 { t.Fatalf("LineCount() = %d, want 1", got) } if got := b.Line(0); got != "abcd" { t.Errorf("Line(0) = %q, want %q", got, "abcd") } if got := b.Cursor(); got != (Position{Line: 0, Col: 2}) { t.Errorf("Cursor() = %+v, want the join point", got) } } func TestBackspaceAtTheStartOfTheBufferDoesNothing(t *testing.T) { b := NewFromString("abc") b.SetCursor(Position{}) b.Backspace() if got := b.Text(); got != "abc" { t.Errorf("Text() = %q, want it unchanged", got) } if b.Modified() { t.Error("a backspace that changed nothing must not mark the buffer modified") } } func TestDeleteRemovesTheCharacterUnderTheCursor(t *testing.T) { b := NewFromString("abc") b.SetCursor(Position{Line: 0, Col: 1}) b.Delete() if got := b.Line(0); got != "ac" { t.Errorf("Line(0) = %q, want %q", got, "ac") } if got := b.Cursor(); got != (Position{Line: 0, Col: 1}) { t.Errorf("Cursor() = %+v, want it to stay put", got) } } func TestDeleteAtTheEndOfALineJoinsTheNextOne(t *testing.T) { b := NewFromString("ab\ncd") b.SetCursor(Position{Line: 0, Col: 2}) b.Delete() if got := b.Text(); got != "abcd" { t.Errorf("Text() = %q, want %q", got, "abcd") } } func TestDeleteAtTheEndOfTheBufferDoesNothing(t *testing.T) { b := NewFromString("ab") b.MoveBufferEnd() b.Delete() if got := b.Text(); got != "ab" { t.Errorf("Text() = %q, want it unchanged", got) } if b.Modified() { t.Error("a delete that changed nothing must not mark the buffer modified") } } func TestDeleteRangeAcrossSeveralLines(t *testing.T) { b := NewFromString("one\ntwo\nthree") b.DeleteRange(Range{ Start: Position{Line: 0, Col: 1}, End: Position{Line: 2, Col: 2}, }) if got := b.LineCount(); got != 1 { t.Fatalf("LineCount() = %d, want 1", got) } if got := b.Line(0); got != "oree" { t.Errorf("Line(0) = %q, want %q", got, "oree") } } func TestReplaceRangeClampsOutOfBoundsPositions(t *testing.T) { b := NewFromString("abc") end := b.ReplaceRange(Range{ Start: Position{Line: -5, Col: -5}, End: Position{Line: 99, Col: 99}, }, "z") if got := b.Text(); got != "z" { t.Errorf("Text() = %q, want %q", got, "z") } if end != (Position{Line: 0, Col: 1}) { t.Errorf("ReplaceRange returned %+v, want {Line:0 Col:1}", end) } } func TestReplaceRangeAcceptsReversedBounds(t *testing.T) { b := NewFromString("abcd") b.ReplaceRange(Range{ Start: Position{Line: 0, Col: 3}, End: Position{Line: 0, Col: 1}, }, "-") if got := b.Line(0); got != "a-d" { t.Errorf("Line(0) = %q, want %q", got, "a-d") } } func TestReplaceRangeDoesNotAliasTheInsertedText(t *testing.T) { b := NewFromString("x") // Two successive edits must not share memory through the internal line // slices, or the second would corrupt the first. b.ReplaceRange(Range{}, "one\ntwo\nthree") b.ReplaceRange(Range{Start: Position{Line: 1}, End: Position{Line: 1, Col: 3}}, "TWO") want := "one\nTWO\nthreex" if got := b.Text(); got != want { t.Errorf("Text() = %q, want %q", got, want) } }