| 🛟 Updated. 28d5985 k33g 16h ago | 1 | package buffer |
| 2 | |
| 3 | import "strings" |
| 4 | |
| 5 | // Insert puts text in at the cursor, replacing the selection if there is one. |
| 6 | // |
| 7 | // Line feeds inside text split the current line, so a whole paste is a single |
| 8 | // call and a single undo step. |
| 9 | // |
| 10 | // b := buffer.NewFromString("ab") |
| 11 | // b.SetCursor(buffer.Position{Line: 0, Col: 1}) |
| 12 | // b.Insert("X") |
| 13 | // fmt.Println(b.Line(0)) // aXb |
| 14 | func (b *Buffer) Insert(text string) { |
| 15 | b.DeleteSelection() |
| 16 | b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, text) |
| 17 | } |
| 18 | |
| 19 | // InsertRune puts a single character in at the cursor, replacing the selection |
| 20 | // if there is one. Consecutive InsertRune calls that type forward on one line |
| 21 | // collapse into a single undo step, so undo removes a word rather than a |
| 22 | // letter. |
| 23 | func (b *Buffer) InsertRune(r rune) { |
| 24 | b.DeleteSelection() |
| 25 | b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, string(r)) |
| 26 | // Arm the merge for the *next* keystroke; the one just recorded stays a |
| 27 | // step of its own unless more typing follows. |
| 28 | b.coalesce = true |
| 29 | } |
| 30 | |
| 31 | // InsertNewlineAndIndent splits the line at the cursor and copies the leading |
| 32 | // whitespace of the current line onto the new one, which is what you want when |
| 33 | // typing a block of Go code. |
| 34 | // |
| 35 | // b := buffer.NewFromString("\tfoo()") |
| 36 | // b.SetCursor(buffer.Position{Line: 0, Col: 6}) |
| 37 | // b.InsertNewlineAndIndent() |
| 38 | // fmt.Printf("%q\n", b.Line(1)) // "\t" |
| 39 | func (b *Buffer) InsertNewlineAndIndent() { |
| 40 | b.DeleteSelection() |
| 41 | indent := leadingWhitespace(b.lines[b.cursor.Line]) |
| 42 | // Whitespace to the right of the cursor is not part of the indent: only |
| 43 | // the run that precedes it can be carried over. |
| 44 | if len(indent) > b.cursor.Col { |
| 45 | indent = indent[:b.cursor.Col] |
| 46 | } |
| 47 | b.Insert("\n" + string(indent)) |
| 48 | } |
| 49 | |
| 50 | // Backspace deletes the selection, or the character before the cursor when |
| 51 | // nothing is selected. At the very start of a line it joins that line to the |
| 52 | // previous one. Consecutive backspaces collapse into a single undo step. |
| 53 | func (b *Buffer) Backspace() { |
| 54 | if b.DeleteSelection() { |
| 55 | return |
| 56 | } |
| 57 | if b.cursor == (Position{}) { |
| 58 | return |
| 59 | } |
| 60 | |
| 61 | start := b.cursor |
| 62 | if start.Col > 0 { |
| 63 | start.Col-- |
| 64 | } else { |
| 65 | start.Line-- |
| 66 | start.Col = len(b.lines[start.Line]) |
| 67 | } |
| 68 | |
| 69 | b.DeleteRange(Range{Start: start, End: b.cursor}) |
| 70 | b.coalesce = true |
| 71 | } |
| 72 | |
| 73 | // Delete removes the selection, or the character under the cursor when nothing |
| 74 | // is selected. At the end of a line it joins the next line onto this one. |
| 75 | func (b *Buffer) Delete() { |
| 76 | if b.DeleteSelection() { |
| 77 | return |
| 78 | } |
| 79 | |
| 80 | end := b.cursor |
| 81 | switch { |
| 82 | case end.Col < len(b.lines[end.Line]): |
| 83 | end.Col++ |
| 84 | case end.Line < len(b.lines)-1: |
| 85 | end.Line++ |
| 86 | end.Col = 0 |
| 87 | default: |
| 88 | return // end of the buffer, nothing to delete |
| 89 | } |
| 90 | |
| 91 | b.DeleteRange(Range{Start: b.cursor, End: end}) |
| 92 | } |
| 93 | |
| 94 | // DeleteRange removes the text covered by r and puts the cursor where it |
| 95 | // started. Positions outside the buffer are clamped, so a caller may pass a |
| 96 | // generous range without checking its bounds first. |
| 97 | func (b *Buffer) DeleteRange(r Range) { |
| 98 | b.ReplaceRange(r, "") |
| 99 | } |
| 100 | |
| 101 | // ReplaceRange swaps the text covered by r for text and returns the position |
| 102 | // just after the inserted text, which is where a cursor following an edit |
| 103 | // belongs. |
| 104 | // |
| 105 | // This is the single point through which every change to the text passes: the |
| 106 | // undo history, the modified flag and the cursor are all maintained here. |
| 107 | func (b *Buffer) ReplaceRange(r Range, text string) Position { |
| 108 | r = Range{Start: b.clamp(r.Start), End: b.clamp(r.End)} |
| 109 | if r.End.Before(r.Start) { |
| 110 | r.Start, r.End = r.End, r.Start |
| 111 | } |
| 112 | |
| 113 | inserted := splitLines(text) |
| 114 | change := edit{ |
| 115 | rng: r, |
| 116 | old: b.textIn(r), |
| 117 | new: inserted, |
| 118 | cursorBefore: b.cursor, |
| 119 | } |
| 120 | |
| 121 | end := b.splice(r, inserted) |
| 122 | b.cursor = end |
| 123 | change.cursorAfter = end |
| 124 | |
| 125 | b.record(change) |
| 126 | b.modified = true |
| 127 | b.revision++ |
| 128 | b.ClearSelection() |
| 129 | return end |
| 130 | } |
| 131 | |
| 132 | // splice swaps the text covered by r for lines without touching the undo |
| 133 | // history, and returns the position just after the inserted text. |
| 134 | // |
| 135 | // r must already be clamped and correctly ordered. |
| 136 | func (b *Buffer) splice(r Range, lines [][]rune) Position { |
| 137 | head := b.lines[r.Start.Line][:r.Start.Col] |
| 138 | tail := b.lines[r.End.Line][r.End.Col:] |
| 139 | |
| 140 | replacement := make([][]rune, 0, len(lines)) |
| 141 | first := concat(head, lines[0]) |
| 142 | |
| 143 | var end Position |
| 144 | if len(lines) == 1 { |
| 145 | end = Position{Line: r.Start.Line, Col: len(first)} |
| 146 | replacement = append(replacement, concat(first, tail)) |
| 147 | } else { |
| 148 | replacement = append(replacement, first) |
| 149 | for _, middle := range lines[1 : len(lines)-1] { |
| 150 | replacement = append(replacement, concat(middle, nil)) |
| 151 | } |
| 152 | last := lines[len(lines)-1] |
| 153 | end = Position{Line: r.Start.Line + len(lines) - 1, Col: len(last)} |
| 154 | replacement = append(replacement, concat(last, tail)) |
| 155 | } |
| 156 | |
| 157 | b.lines = spliceLines(b.lines, r.Start.Line, r.End.Line+1, replacement) |
| 158 | return end |
| 159 | } |
| 160 | |
| 161 | // textIn returns a copy of the text covered by r, which must be clamped. |
| 162 | func (b *Buffer) textIn(r Range) [][]rune { |
| 163 | if r.Start.Line == r.End.Line { |
| 164 | return [][]rune{concat(b.lines[r.Start.Line][r.Start.Col:r.End.Col], nil)} |
| 165 | } |
| 166 | |
| 167 | out := make([][]rune, 0, r.End.Line-r.Start.Line+1) |
| 168 | out = append(out, concat(b.lines[r.Start.Line][r.Start.Col:], nil)) |
| 169 | for _, line := range b.lines[r.Start.Line+1 : r.End.Line] { |
| 170 | out = append(out, concat(line, nil)) |
| 171 | } |
| 172 | out = append(out, concat(b.lines[r.End.Line][:r.End.Col], nil)) |
| 173 | return out |
| 174 | } |
| 175 | |
| 176 | // spliceLines returns lines with the half-open span [from, to) replaced by |
| 177 | // replacement. The input slice is left untouched. |
| 178 | func spliceLines(lines [][]rune, from, to int, replacement [][]rune) [][]rune { |
| 179 | out := make([][]rune, 0, len(lines)-(to-from)+len(replacement)) |
| 180 | out = append(out, lines[:from]...) |
| 181 | out = append(out, replacement...) |
| 182 | out = append(out, lines[to:]...) |
| 183 | return out |
| 184 | } |
| 185 | |
| 186 | // concat returns a fresh slice holding a followed by b, so that the result |
| 187 | // shares no memory with either input. |
| 188 | func concat(a, b []rune) []rune { |
| 189 | out := make([]rune, 0, len(a)+len(b)) |
| 190 | out = append(out, a...) |
| 191 | out = append(out, b...) |
| 192 | return out |
| 193 | } |
| 194 | |
| 195 | // leadingWhitespace returns the run of spaces and tabs that starts the line. |
| 196 | func leadingWhitespace(line []rune) []rune { |
| 197 | end := 0 |
| 198 | for end < len(line) && (line[end] == ' ' || line[end] == '\t') { |
| 199 | end++ |
| 200 | } |
| 201 | return line[:end] |
| 202 | } |
| 203 | |
| 204 | // endOf returns the position just after lines, when they are laid down |
| 205 | // starting at start. |
| 206 | func endOf(start Position, lines [][]rune) Position { |
| 207 | if len(lines) == 1 { |
| 208 | return Position{Line: start.Line, Col: start.Col + len(lines[0])} |
| 209 | } |
| 210 | return Position{ |
| 211 | Line: start.Line + len(lines) - 1, |
| 212 | Col: len(lines[len(lines)-1]), |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | // joinRunes renders lines as a single string separated by line feeds. |
| 217 | func joinRunes(lines [][]rune) string { |
| 218 | parts := make([]string, len(lines)) |
| 219 | for i, line := range lines { |
| 220 | parts[i] = string(line) |
| 221 | } |
| 222 | return strings.Join(parts, "\n") |
| 223 | } |
| 224 | |
| 225 | // InsertLineAbove opens a blank line where the cursor is and pushes the |
| 226 | // current line down, leaving the cursor on the text it was on — which is now |
| 227 | // one line lower. |
| 228 | // |
| 229 | // This is Turbo C's Ctrl-N. It makes room *above* what you are looking at, |
| 230 | // which is what you want when the thing you are about to write belongs before |
| 231 | // the line in front of you. |
| 232 | // |
| 233 | // b := buffer.NewFromString("second\n") |
| 234 | // b.InsertLineAbove() |
| 235 | // b.Text() // "\nsecond\n", cursor still on "second" |
| 236 | // |
| 237 | // The new line is blank rather than indented like its neighbour. Turbo C left |
| 238 | // it blank, and an indent nobody asked for becomes trailing whitespace the |
| 239 | // moment they change their mind. |
| 240 | func (b *Buffer) InsertLineAbove() { |
| 241 | cursor := b.cursor |
| 242 | start := Position{Line: cursor.Line, Col: 0} |
| 243 | |
| 244 | b.ReplaceRange(Range{Start: start, End: start}, "\n") |
| 245 | b.SetCursor(Position{Line: cursor.Line + 1, Col: cursor.Col}) |
| 246 | } |
| 247 | |
| 248 | // DeleteLine removes the line the cursor is on, closing the gap. |
| 249 | // |
| 250 | // This is Turbo C's Ctrl-Y. The cursor stays on the same line number, which |
| 251 | // now holds what used to be the line below — so holding the key deletes a run |
| 252 | // of lines, which is the whole point of having it. |
| 253 | // |
| 254 | // b := buffer.NewFromString("one\ntwo\nthree\n") |
| 255 | // b.SetCursor(buffer.Position{Line: 1}) |
| 256 | // b.DeleteLine() |
| 257 | // b.Text() // "one\nthree\n" |
| 258 | // |
| 259 | // The last line of a buffer has no newline after it to take, so its own |
| 260 | // leading newline goes instead. A buffer of one line keeps that line and is |
| 261 | // emptied rather than left with no lines at all, because every other operation |
| 262 | // here assumes there is always a line to be on. |
| 263 | // |
| 264 | // That one-line case is written out although ReplaceRange would reach the same |
| 265 | // answer without it — the range it would build starts on line −1, which clamp |
| 266 | // pulls back to 0. Leaning on that is a thing a reader has to work out from |
| 267 | // two files away, and the day clamp stops being so forgiving this would fail |
| 268 | // silently. |
| 269 | func (b *Buffer) DeleteLine() { |
| 270 | line := b.cursor.Line |
| 271 | column := b.cursor.Col |
| 272 | |
| 273 | switch { |
| 274 | case b.LineCount() == 1: |
| 275 | b.ReplaceRange(Range{ |
| 276 | Start: Position{Line: 0, Col: 0}, |
| 277 | End: Position{Line: 0, Col: b.LineLen(0)}, |
| 278 | }, "") |
| 279 | case line == b.LineCount()-1: |
| 280 | b.ReplaceRange(Range{ |
| 281 | Start: Position{Line: line - 1, Col: b.LineLen(line - 1)}, |
| 282 | End: Position{Line: line, Col: b.LineLen(line)}, |
| 283 | }, "") |
| 284 | default: |
| 285 | b.ReplaceRange(Range{ |
| 286 | Start: Position{Line: line, Col: 0}, |
| 287 | End: Position{Line: line + 1, Col: 0}, |
| 288 | }, "") |
| 289 | } |
| 290 | |
| 291 | b.SetCursor(Position{Line: min(line, b.LineCount()-1), Col: column}) |
| 292 | } |