| 🛟 Updated. 28d5985 k33g 20h ago | 1 | package buffer |
| 2 | |
| 3 | import "testing" |
| 4 | |
| 5 | // atLine returns a buffer holding text with the cursor on a line. |
| 6 | func atLine(text string, line, col int) *Buffer { |
| 7 | b := NewFromString(text) |
| 8 | b.SetCursor(Position{Line: line, Col: col}) |
| 9 | return b |
| 10 | } |
| 11 | |
| 12 | func TestInsertLineAbovePushesTheCurrentLineDown(t *testing.T) { |
| 13 | b := atLine("one\ntwo\nthree\n", 1, 2) |
| 14 | |
| 15 | b.InsertLineAbove() |
| 16 | |
| 17 | if got := b.Text(); got != "one\n\ntwo\nthree\n" { |
| 18 | t.Errorf("Text() = %q, want a blank line before \"two\"", got) |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | func TestInsertLineAboveLeavesTheCursorOnItsOwnText(t *testing.T) { |
| 23 | // The point of inserting *above*: you keep looking at what you were |
| 24 | // looking at, with room made over it. |
| 25 | b := atLine("one\ntwo\n", 1, 2) |
| 26 | |
| 27 | b.InsertLineAbove() |
| 28 | |
| 29 | if got := b.Cursor(); got.Line != 2 || got.Col != 2 { |
| 30 | t.Errorf("Cursor() = %+v, want line 2 column 2 — the same text, one line lower", got) |
| 31 | } |
| 32 | if got := b.Line(b.Cursor().Line); got != "two" { |
| 33 | t.Errorf("the cursor is on %q, want the line it started on", got) |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | func TestInsertLineAboveWorksOnTheFirstLine(t *testing.T) { |
| 38 | b := atLine("one\n", 0, 1) |
| 39 | |
| 40 | b.InsertLineAbove() |
| 41 | |
| 42 | if got := b.Text(); got != "\none\n" { |
| 43 | t.Errorf("Text() = %q", got) |
| 44 | } |
| 45 | if got := b.Cursor().Line; got != 1 { |
| 46 | t.Errorf("Cursor().Line = %d, want 1", got) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestTheInsertedLineIsBlank(t *testing.T) { |
| 51 | // Not indented like its neighbour: an indent nobody asked for becomes |
| 52 | // trailing whitespace the moment they change their mind. |
| 53 | b := atLine("\t\tdeep\n", 0, 3) |
| 54 | |
| 55 | b.InsertLineAbove() |
| 56 | |
| 57 | if got := b.Line(0); got != "" { |
| 58 | t.Errorf("the new line is %q, want it empty", got) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | func TestDeleteLineClosesTheGap(t *testing.T) { |
| 63 | b := atLine("one\ntwo\nthree\n", 1, 0) |
| 64 | |
| 65 | b.DeleteLine() |
| 66 | |
| 67 | if got := b.Text(); got != "one\nthree\n" { |
| 68 | t.Errorf("Text() = %q", got) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | func TestDeleteLineLeavesTheCursorWhereTheNextLineNowIs(t *testing.T) { |
| 73 | // So that holding the key deletes a run, which is the whole point. |
| 74 | b := atLine("one\ntwo\nthree\nfour\n", 1, 0) |
| 75 | |
| 76 | b.DeleteLine() |
| 77 | b.DeleteLine() |
| 78 | |
| 79 | if got := b.Text(); got != "one\nfour\n" { |
| 80 | t.Errorf("Text() = %q, want two lines gone", got) |
| 81 | } |
| 82 | if got := b.Cursor().Line; got != 1 { |
| 83 | t.Errorf("Cursor().Line = %d, want 1", got) |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | func TestDeletingTheLastLineTakesTheNewlineBeforeIt(t *testing.T) { |
| 88 | // There is no newline after the last line to take, so its own leading one |
| 89 | // goes instead. Taking nothing would leave a blank line behind. |
| 90 | b := NewFromString("one\ntwo") |
| 91 | b.SetCursor(Position{Line: 1, Col: 0}) |
| 92 | |
| 93 | b.DeleteLine() |
| 94 | |
| 95 | if got := b.Text(); got != "one" { |
| 96 | t.Errorf("Text() = %q, want the line and the newline before it gone", got) |
| 97 | } |
| 98 | if got := b.LineCount(); got != 1 { |
| 99 | t.Errorf("LineCount() = %d, want 1", got) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | func TestDeletingTheOnlyLineEmptiesItRatherThanRemovingIt(t *testing.T) { |
| 104 | // Every other operation here assumes there is always a line to be on. |
| 105 | b := atLine("alone", 0, 3) |
| 106 | |
| 107 | b.DeleteLine() |
| 108 | |
| 109 | if got := b.LineCount(); got != 1 { |
| 110 | t.Fatalf("LineCount() = %d, want 1 — a buffer always has a line", got) |
| 111 | } |
| 112 | if got := b.Text(); got != "" { |
| 113 | t.Errorf("Text() = %q, want it empty", got) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestDeleteLineOnTheLastOfSeveralLeavesACursorInTheBuffer(t *testing.T) { |
| 118 | b := NewFromString("one\ntwo") |
| 119 | b.SetCursor(Position{Line: 1, Col: 3}) |
| 120 | |
| 121 | b.DeleteLine() |
| 122 | |
| 123 | if got := b.Cursor().Line; got >= b.LineCount() { |
| 124 | t.Errorf("Cursor().Line = %d with %d lines — the cursor is off the end", got, b.LineCount()) |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | func TestBothLineEditsAreUndoneInOneStep(t *testing.T) { |
| 129 | // They go through ReplaceRange, the single mutation path, so the undo |
| 130 | // history gets one entry each rather than a splice nobody recorded. |
| 131 | for name, edit := range map[string]func(*Buffer){ |
| 132 | "insert": (*Buffer).InsertLineAbove, |
| 133 | "delete": (*Buffer).DeleteLine, |
| 134 | } { |
| 135 | t.Run(name, func(t *testing.T) { |
| 136 | b := atLine("one\ntwo\nthree\n", 1, 1) |
| 137 | before := b.Text() |
| 138 | |
| 139 | edit(b) |
| 140 | if b.Text() == before { |
| 141 | t.Fatal("the edit changed nothing") |
| 142 | } |
| 143 | if !b.Undo() { |
| 144 | t.Fatal("Undo() reported nothing to undo") |
| 145 | } |
| 146 | |
| 147 | if got := b.Text(); got != before { |
| 148 | t.Errorf("after one undo Text() = %q, want %q", got, before) |
| 149 | } |
| 150 | }) |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | func TestALineEditClearsTheSelection(t *testing.T) { |
| 155 | // ReplaceRange clears it, and a selection left behind after the text under |
| 156 | // it moved would highlight the wrong characters. |
| 157 | b := atLine("one\ntwo\nthree\n", 1, 0) |
| 158 | b.StartSelection() |
| 159 | b.SetCursorKeepingSelection(Position{Line: 1, Col: 3}) |
| 160 | |
| 161 | b.DeleteLine() |
| 162 | |
| 163 | if _, has := b.Selection(); has { |
| 164 | t.Error("a selection survived the line being deleted") |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | // selected returns the text a buffer's selection covers, and whether there is |
| 169 | // one at all. |
| 170 | func selected(b *Buffer) (string, bool) { |
| 171 | if _, has := b.Selection(); !has { |
| 172 | return "", false |
| 173 | } |
| 174 | return b.SelectedText(), true |
| 175 | } |
| 176 | |
| 177 | func TestSelectWordTakesTheWholeWordFromAnywhereInIt(t *testing.T) { |
| 178 | const line = "func mainLoop() {}" |
| 179 | |
| 180 | for _, col := range []int{5, 9, 12} { |
| 181 | b := NewFromString(line) |
| 182 | b.SelectWord(Position{Line: 0, Col: col}) |
| 183 | |
| 184 | got, has := selected(b) |
| 185 | if !has || got != "mainLoop" { |
| 186 | t.Errorf("from column %d the selection is %q (any: %v), want %q", col, got, has, "mainLoop") |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | func TestSelectWordCountsUnderscoresAndDigits(t *testing.T) { |
| 192 | // The same rule as Ctrl-Left and the completion popup: one editor, one |
| 193 | // idea of a word. |
| 194 | b := NewFromString("var max_retries2 = 3") |
| 195 | b.SelectWord(Position{Line: 0, Col: 6}) |
| 196 | |
| 197 | if got, _ := selected(b); got != "max_retries2" { |
| 198 | t.Errorf("the selection is %q", got) |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | func TestSelectWordStopsAtPunctuation(t *testing.T) { |
| 203 | b := NewFromString("fmt.Println(x)") |
| 204 | b.SelectWord(Position{Line: 0, Col: 5}) |
| 205 | |
| 206 | if got, _ := selected(b); got != "Println" { |
| 207 | t.Errorf("the selection is %q, want the dot and bracket left out", got) |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | func TestSelectWordOffAWordSelectsNothing(t *testing.T) { |
| 212 | // Editors disagree about what a run of punctuation means; nothing is at |
| 213 | // least predictable. |
| 214 | // "var x = fmt.Println" — a space at 3, a dot at 11, and nothing at 40. |
| 215 | for name, col := range map[string]int{"a space": 3, "a dot": 11, "past the end": 40} { |
| 216 | t.Run(name, func(t *testing.T) { |
| 217 | b := NewFromString("var x = fmt.Println") |
| 218 | b.SelectWord(Position{Line: 0, Col: col}) |
| 219 | |
| 220 | if got, has := selected(b); has { |
| 221 | t.Errorf("selected %q, want nothing", got) |
| 222 | } |
| 223 | }) |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | func TestSelectWordOnAnEmptyLineSelectsNothing(t *testing.T) { |
| 228 | b := NewFromString("one\n\nthree\n") |
| 229 | b.SelectWord(Position{Line: 1, Col: 0}) |
| 230 | |
| 231 | if _, has := selected(b); has { |
| 232 | t.Error("an empty line has a word on it") |
| 233 | } |
| 234 | if got := b.Cursor(); got.Line != 1 { |
| 235 | t.Errorf("Cursor() = %+v, want the cursor moved to the click", got) |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | func TestSelectWordLeavesTheCursorAtTheEndOfTheWord(t *testing.T) { |
| 240 | // So that typing replaces the word, and Shift-Right extends from its end, |
| 241 | // which is what a selection made by dragging would do. |
| 242 | b := NewFromString("alpha beta") |
| 243 | b.SelectWord(Position{Line: 0, Col: 7}) |
| 244 | |
| 245 | if got := b.Cursor().Col; got != 10 { |
| 246 | t.Errorf("Cursor().Col = %d, want 10 — the end of \"beta\"", got) |
| 247 | } |
| 248 | } |