package buffer import "strings" // Insert puts text in at the cursor, replacing the selection if there is one. // // Line feeds inside text split the current line, so a whole paste is a single // call and a single undo step. // // b := buffer.NewFromString("ab") // b.SetCursor(buffer.Position{Line: 0, Col: 1}) // b.Insert("X") // fmt.Println(b.Line(0)) // aXb func (b *Buffer) Insert(text string) { b.DeleteSelection() b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, text) } // InsertRune puts a single character in at the cursor, replacing the selection // if there is one. Consecutive InsertRune calls that type forward on one line // collapse into a single undo step, so undo removes a word rather than a // letter. func (b *Buffer) InsertRune(r rune) { b.DeleteSelection() b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, string(r)) // Arm the merge for the *next* keystroke; the one just recorded stays a // step of its own unless more typing follows. b.coalesce = true } // InsertNewlineAndIndent splits the line at the cursor and copies the leading // whitespace of the current line onto the new one, which is what you want when // typing a block of Go code. // // b := buffer.NewFromString("\tfoo()") // b.SetCursor(buffer.Position{Line: 0, Col: 6}) // b.InsertNewlineAndIndent() // fmt.Printf("%q\n", b.Line(1)) // "\t" func (b *Buffer) InsertNewlineAndIndent() { b.DeleteSelection() indent := leadingWhitespace(b.lines[b.cursor.Line]) // Whitespace to the right of the cursor is not part of the indent: only // the run that precedes it can be carried over. if len(indent) > b.cursor.Col { indent = indent[:b.cursor.Col] } b.Insert("\n" + string(indent)) } // Backspace deletes the selection, or the character before the cursor when // nothing is selected. At the very start of a line it joins that line to the // previous one. Consecutive backspaces collapse into a single undo step. func (b *Buffer) Backspace() { if b.DeleteSelection() { return } if b.cursor == (Position{}) { return } start := b.cursor if start.Col > 0 { start.Col-- } else { start.Line-- start.Col = len(b.lines[start.Line]) } b.DeleteRange(Range{Start: start, End: b.cursor}) b.coalesce = true } // Delete removes the selection, or the character under the cursor when nothing // is selected. At the end of a line it joins the next line onto this one. func (b *Buffer) Delete() { if b.DeleteSelection() { return } end := b.cursor switch { case end.Col < len(b.lines[end.Line]): end.Col++ case end.Line < len(b.lines)-1: end.Line++ end.Col = 0 default: return // end of the buffer, nothing to delete } b.DeleteRange(Range{Start: b.cursor, End: end}) } // DeleteRange removes the text covered by r and puts the cursor where it // started. Positions outside the buffer are clamped, so a caller may pass a // generous range without checking its bounds first. func (b *Buffer) DeleteRange(r Range) { b.ReplaceRange(r, "") } // ReplaceRange swaps the text covered by r for text and returns the position // just after the inserted text, which is where a cursor following an edit // belongs. // // This is the single point through which every change to the text passes: the // undo history, the modified flag and the cursor are all maintained here. func (b *Buffer) ReplaceRange(r Range, text string) Position { r = Range{Start: b.clamp(r.Start), End: b.clamp(r.End)} if r.End.Before(r.Start) { r.Start, r.End = r.End, r.Start } inserted := splitLines(text) change := edit{ rng: r, old: b.textIn(r), new: inserted, cursorBefore: b.cursor, } end := b.splice(r, inserted) b.cursor = end change.cursorAfter = end b.record(change) b.modified = true b.revision++ b.ClearSelection() return end } // splice swaps the text covered by r for lines without touching the undo // history, and returns the position just after the inserted text. // // r must already be clamped and correctly ordered. func (b *Buffer) splice(r Range, lines [][]rune) Position { head := b.lines[r.Start.Line][:r.Start.Col] tail := b.lines[r.End.Line][r.End.Col:] replacement := make([][]rune, 0, len(lines)) first := concat(head, lines[0]) var end Position if len(lines) == 1 { end = Position{Line: r.Start.Line, Col: len(first)} replacement = append(replacement, concat(first, tail)) } else { replacement = append(replacement, first) for _, middle := range lines[1 : len(lines)-1] { replacement = append(replacement, concat(middle, nil)) } last := lines[len(lines)-1] end = Position{Line: r.Start.Line + len(lines) - 1, Col: len(last)} replacement = append(replacement, concat(last, tail)) } b.lines = spliceLines(b.lines, r.Start.Line, r.End.Line+1, replacement) return end } // textIn returns a copy of the text covered by r, which must be clamped. func (b *Buffer) textIn(r Range) [][]rune { if r.Start.Line == r.End.Line { return [][]rune{concat(b.lines[r.Start.Line][r.Start.Col:r.End.Col], nil)} } out := make([][]rune, 0, r.End.Line-r.Start.Line+1) out = append(out, concat(b.lines[r.Start.Line][r.Start.Col:], nil)) for _, line := range b.lines[r.Start.Line+1 : r.End.Line] { out = append(out, concat(line, nil)) } out = append(out, concat(b.lines[r.End.Line][:r.End.Col], nil)) return out } // spliceLines returns lines with the half-open span [from, to) replaced by // replacement. The input slice is left untouched. func spliceLines(lines [][]rune, from, to int, replacement [][]rune) [][]rune { out := make([][]rune, 0, len(lines)-(to-from)+len(replacement)) out = append(out, lines[:from]...) out = append(out, replacement...) out = append(out, lines[to:]...) return out } // concat returns a fresh slice holding a followed by b, so that the result // shares no memory with either input. func concat(a, b []rune) []rune { out := make([]rune, 0, len(a)+len(b)) out = append(out, a...) out = append(out, b...) return out } // leadingWhitespace returns the run of spaces and tabs that starts the line. func leadingWhitespace(line []rune) []rune { end := 0 for end < len(line) && (line[end] == ' ' || line[end] == '\t') { end++ } return line[:end] } // endOf returns the position just after lines, when they are laid down // starting at start. func endOf(start Position, lines [][]rune) Position { if len(lines) == 1 { return Position{Line: start.Line, Col: start.Col + len(lines[0])} } return Position{ Line: start.Line + len(lines) - 1, Col: len(lines[len(lines)-1]), } } // joinRunes renders lines as a single string separated by line feeds. func joinRunes(lines [][]rune) string { parts := make([]string, len(lines)) for i, line := range lines { parts[i] = string(line) } return strings.Join(parts, "\n") } // InsertLineAbove opens a blank line where the cursor is and pushes the // current line down, leaving the cursor on the text it was on — which is now // one line lower. // // This is Turbo C's Ctrl-N. It makes room *above* what you are looking at, // which is what you want when the thing you are about to write belongs before // the line in front of you. // // b := buffer.NewFromString("second\n") // b.InsertLineAbove() // b.Text() // "\nsecond\n", cursor still on "second" // // The new line is blank rather than indented like its neighbour. Turbo C left // it blank, and an indent nobody asked for becomes trailing whitespace the // moment they change their mind. func (b *Buffer) InsertLineAbove() { cursor := b.cursor start := Position{Line: cursor.Line, Col: 0} b.ReplaceRange(Range{Start: start, End: start}, "\n") b.SetCursor(Position{Line: cursor.Line + 1, Col: cursor.Col}) } // DeleteLine removes the line the cursor is on, closing the gap. // // This is Turbo C's Ctrl-Y. The cursor stays on the same line number, which // now holds what used to be the line below — so holding the key deletes a run // of lines, which is the whole point of having it. // // b := buffer.NewFromString("one\ntwo\nthree\n") // b.SetCursor(buffer.Position{Line: 1}) // b.DeleteLine() // b.Text() // "one\nthree\n" // // The last line of a buffer has no newline after it to take, so its own // leading newline goes instead. A buffer of one line keeps that line and is // emptied rather than left with no lines at all, because every other operation // here assumes there is always a line to be on. // // That one-line case is written out although ReplaceRange would reach the same // answer without it — the range it would build starts on line −1, which clamp // pulls back to 0. Leaning on that is a thing a reader has to work out from // two files away, and the day clamp stops being so forgiving this would fail // silently. func (b *Buffer) DeleteLine() { line := b.cursor.Line column := b.cursor.Col switch { case b.LineCount() == 1: b.ReplaceRange(Range{ Start: Position{Line: 0, Col: 0}, End: Position{Line: 0, Col: b.LineLen(0)}, }, "") case line == b.LineCount()-1: b.ReplaceRange(Range{ Start: Position{Line: line - 1, Col: b.LineLen(line - 1)}, End: Position{Line: line, Col: b.LineLen(line)}, }, "") default: b.ReplaceRange(Range{ Start: Position{Line: line, Col: 0}, End: Position{Line: line + 1, Col: 0}, }, "") } b.SetCursor(Position{Line: min(line, b.LineCount()-1), Col: column}) }