package buffer // StartSelection drops the selection anchor where the cursor is. Moving the // cursor afterwards extends the selection from that anchor, which is what // holding Shift does. // // b := buffer.NewFromString("hello") // b.StartSelection() // b.MoveWordRight() // fmt.Println(b.SelectedText()) // hello func (b *Buffer) StartSelection() { anchor := b.cursor b.anchor = &anchor } // ClearSelection forgets the anchor, leaving nothing selected. func (b *Buffer) ClearSelection() { b.anchor = nil } // SelectAll selects the whole buffer and puts the cursor at its end. func (b *Buffer) SelectAll() { b.MoveBufferStart() b.Extend(b.MoveBufferEnd) } // Extend runs a cursor movement while keeping the selection anchor, which is // exactly what holding Shift does. If nothing was selected yet, the anchor is // dropped where the cursor stands before the move. // // Every movement method works here, so there is no Shift-flavoured twin of // each of them: // // b.Extend(b.MoveWordRight) // b.Extend(b.MoveDown) func (b *Buffer) Extend(move func()) { if b.anchor == nil { b.StartSelection() } anchor := b.anchor move() b.anchor = anchor } // SetCursorKeepingSelection moves the cursor to p while keeping the selection // anchor. It is Extend for an absolute position, which is what a mouse drag // needs; SetCursor is the plain, selection-clearing move. func (b *Buffer) SetCursorKeepingSelection(p Position) { b.Extend(func() { b.SetCursor(p) }) } // Selection returns the selected span and whether there is one. A selection // that has been reduced to nothing by moving the cursor back onto the anchor // counts as absent. func (b *Buffer) Selection() (Range, bool) { if b.anchor == nil { return Range{}, false } r := NewRange(*b.anchor, b.cursor) if r.IsEmpty() { return Range{}, false } return r, true } // SelectedText returns the selected text, or the empty string when nothing is // selected. Lines are joined with line feeds regardless of the file's own line // endings, because this is what goes on the clipboard. func (b *Buffer) SelectedText() string { r, ok := b.Selection() if !ok { return "" } return joinRunes(b.textIn(r)) } // DeleteSelection removes the selected text and reports whether there was any. // It is the first thing every editing operation does, so that typing over a // selection replaces it. func (b *Buffer) DeleteSelection() bool { r, ok := b.Selection() if !ok { b.ClearSelection() return false } b.ClearSelection() b.DeleteRange(r) return true } // SelectWord selects the word at a position and puts the cursor at its end. // // The word is a run of the runes IsWordRune accepts — the same notion // Ctrl-Left and Ctrl-Right move by, and the same one the completion popup uses // to work out what is being typed. One editor, one idea of a word. // // b := buffer.NewFromString("func main() {}") // b.SelectWord(buffer.Position{Line: 0, Col: 6}) // r, _ := b.Selection() // covers "main" // // A position that is not on a word rune — a space, a bracket, past the end of // a line — selects nothing and only moves the cursor. Editors disagree about // what a run of punctuation means, and answering nothing is at least an answer // that can be predicted. func (b *Buffer) SelectWord(p Position) { p = b.clamp(p) line := b.lines[p.Line] if p.Col >= len(line) || !IsWordRune(line[p.Col]) { b.ClearSelection() b.SetCursor(p) return } start, end := p.Col, p.Col for start > 0 && IsWordRune(line[start-1]) { start-- } for end < len(line) && IsWordRune(line[end]) { end++ } b.SetCursor(Position{Line: p.Line, Col: start}) b.StartSelection() b.SetCursorKeepingSelection(Position{Line: p.Line, Col: end}) }