package buffer import "testing" // atLine returns a buffer holding text with the cursor on a line. func atLine(text string, line, col int) *Buffer { b := NewFromString(text) b.SetCursor(Position{Line: line, Col: col}) return b } func TestInsertLineAbovePushesTheCurrentLineDown(t *testing.T) { b := atLine("one\ntwo\nthree\n", 1, 2) b.InsertLineAbove() if got := b.Text(); got != "one\n\ntwo\nthree\n" { t.Errorf("Text() = %q, want a blank line before \"two\"", got) } } func TestInsertLineAboveLeavesTheCursorOnItsOwnText(t *testing.T) { // The point of inserting *above*: you keep looking at what you were // looking at, with room made over it. b := atLine("one\ntwo\n", 1, 2) b.InsertLineAbove() if got := b.Cursor(); got.Line != 2 || got.Col != 2 { t.Errorf("Cursor() = %+v, want line 2 column 2 — the same text, one line lower", got) } if got := b.Line(b.Cursor().Line); got != "two" { t.Errorf("the cursor is on %q, want the line it started on", got) } } func TestInsertLineAboveWorksOnTheFirstLine(t *testing.T) { b := atLine("one\n", 0, 1) b.InsertLineAbove() if got := b.Text(); got != "\none\n" { t.Errorf("Text() = %q", got) } if got := b.Cursor().Line; got != 1 { t.Errorf("Cursor().Line = %d, want 1", got) } } func TestTheInsertedLineIsBlank(t *testing.T) { // Not indented like its neighbour: an indent nobody asked for becomes // trailing whitespace the moment they change their mind. b := atLine("\t\tdeep\n", 0, 3) b.InsertLineAbove() if got := b.Line(0); got != "" { t.Errorf("the new line is %q, want it empty", got) } } func TestDeleteLineClosesTheGap(t *testing.T) { b := atLine("one\ntwo\nthree\n", 1, 0) b.DeleteLine() if got := b.Text(); got != "one\nthree\n" { t.Errorf("Text() = %q", got) } } func TestDeleteLineLeavesTheCursorWhereTheNextLineNowIs(t *testing.T) { // So that holding the key deletes a run, which is the whole point. b := atLine("one\ntwo\nthree\nfour\n", 1, 0) b.DeleteLine() b.DeleteLine() if got := b.Text(); got != "one\nfour\n" { t.Errorf("Text() = %q, want two lines gone", got) } if got := b.Cursor().Line; got != 1 { t.Errorf("Cursor().Line = %d, want 1", got) } } func TestDeletingTheLastLineTakesTheNewlineBeforeIt(t *testing.T) { // There is no newline after the last line to take, so its own leading one // goes instead. Taking nothing would leave a blank line behind. b := NewFromString("one\ntwo") b.SetCursor(Position{Line: 1, Col: 0}) b.DeleteLine() if got := b.Text(); got != "one" { t.Errorf("Text() = %q, want the line and the newline before it gone", got) } if got := b.LineCount(); got != 1 { t.Errorf("LineCount() = %d, want 1", got) } } func TestDeletingTheOnlyLineEmptiesItRatherThanRemovingIt(t *testing.T) { // Every other operation here assumes there is always a line to be on. b := atLine("alone", 0, 3) b.DeleteLine() if got := b.LineCount(); got != 1 { t.Fatalf("LineCount() = %d, want 1 — a buffer always has a line", got) } if got := b.Text(); got != "" { t.Errorf("Text() = %q, want it empty", got) } } func TestDeleteLineOnTheLastOfSeveralLeavesACursorInTheBuffer(t *testing.T) { b := NewFromString("one\ntwo") b.SetCursor(Position{Line: 1, Col: 3}) b.DeleteLine() if got := b.Cursor().Line; got >= b.LineCount() { t.Errorf("Cursor().Line = %d with %d lines — the cursor is off the end", got, b.LineCount()) } } func TestBothLineEditsAreUndoneInOneStep(t *testing.T) { // They go through ReplaceRange, the single mutation path, so the undo // history gets one entry each rather than a splice nobody recorded. for name, edit := range map[string]func(*Buffer){ "insert": (*Buffer).InsertLineAbove, "delete": (*Buffer).DeleteLine, } { t.Run(name, func(t *testing.T) { b := atLine("one\ntwo\nthree\n", 1, 1) before := b.Text() edit(b) if b.Text() == before { t.Fatal("the edit changed nothing") } if !b.Undo() { t.Fatal("Undo() reported nothing to undo") } if got := b.Text(); got != before { t.Errorf("after one undo Text() = %q, want %q", got, before) } }) } } func TestALineEditClearsTheSelection(t *testing.T) { // ReplaceRange clears it, and a selection left behind after the text under // it moved would highlight the wrong characters. b := atLine("one\ntwo\nthree\n", 1, 0) b.StartSelection() b.SetCursorKeepingSelection(Position{Line: 1, Col: 3}) b.DeleteLine() if _, has := b.Selection(); has { t.Error("a selection survived the line being deleted") } } // selected returns the text a buffer's selection covers, and whether there is // one at all. func selected(b *Buffer) (string, bool) { if _, has := b.Selection(); !has { return "", false } return b.SelectedText(), true } func TestSelectWordTakesTheWholeWordFromAnywhereInIt(t *testing.T) { const line = "func mainLoop() {}" for _, col := range []int{5, 9, 12} { b := NewFromString(line) b.SelectWord(Position{Line: 0, Col: col}) got, has := selected(b) if !has || got != "mainLoop" { t.Errorf("from column %d the selection is %q (any: %v), want %q", col, got, has, "mainLoop") } } } func TestSelectWordCountsUnderscoresAndDigits(t *testing.T) { // The same rule as Ctrl-Left and the completion popup: one editor, one // idea of a word. b := NewFromString("var max_retries2 = 3") b.SelectWord(Position{Line: 0, Col: 6}) if got, _ := selected(b); got != "max_retries2" { t.Errorf("the selection is %q", got) } } func TestSelectWordStopsAtPunctuation(t *testing.T) { b := NewFromString("fmt.Println(x)") b.SelectWord(Position{Line: 0, Col: 5}) if got, _ := selected(b); got != "Println" { t.Errorf("the selection is %q, want the dot and bracket left out", got) } } func TestSelectWordOffAWordSelectsNothing(t *testing.T) { // Editors disagree about what a run of punctuation means; nothing is at // least predictable. // "var x = fmt.Println" — a space at 3, a dot at 11, and nothing at 40. for name, col := range map[string]int{"a space": 3, "a dot": 11, "past the end": 40} { t.Run(name, func(t *testing.T) { b := NewFromString("var x = fmt.Println") b.SelectWord(Position{Line: 0, Col: col}) if got, has := selected(b); has { t.Errorf("selected %q, want nothing", got) } }) } } func TestSelectWordOnAnEmptyLineSelectsNothing(t *testing.T) { b := NewFromString("one\n\nthree\n") b.SelectWord(Position{Line: 1, Col: 0}) if _, has := selected(b); has { t.Error("an empty line has a word on it") } if got := b.Cursor(); got.Line != 1 { t.Errorf("Cursor() = %+v, want the cursor moved to the click", got) } } func TestSelectWordLeavesTheCursorAtTheEndOfTheWord(t *testing.T) { // So that typing replaces the word, and Shift-Right extends from its end, // which is what a selection made by dragging would do. b := NewFromString("alpha beta") b.SelectWord(Position{Line: 0, Col: 7}) if got := b.Cursor().Col; got != 10 { t.Errorf("Cursor().Col = %d, want 10 — the end of \"beta\"", got) } }