mirror of https://github.com/liamg/aminal.git
Implement three selection modes: char, word, and line. Make selection work in the same way as in Putty. (#191)
This commit is contained in:
parent
f7b162e83e
commit
22a5e8063a
226
buffer/buffer.go
226
buffer/buffer.go
|
@ -9,6 +9,13 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type SelectionMode int
|
||||
const (
|
||||
SelectionChar SelectionMode = iota // char-by-char selection
|
||||
SelectionWord SelectionMode = iota // by word selection
|
||||
SelectionLine SelectionMode = iota // whole line selection
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
lines []Line
|
||||
displayChangeHandlers []chan bool
|
||||
|
@ -18,7 +25,8 @@ type Buffer struct {
|
|||
dirty bool
|
||||
selectionStart *Position
|
||||
selectionEnd *Position
|
||||
selectionComplete bool // whether the selected text can update or whether it is final
|
||||
selectionMode SelectionMode
|
||||
isSelectionComplete bool
|
||||
terminalState *TerminalState
|
||||
savedCharsets []*map[rune]rune
|
||||
savedCurrentCharset int
|
||||
|
@ -29,10 +37,25 @@ type Position struct {
|
|||
Col int
|
||||
}
|
||||
|
||||
func comparePositions(pos1 *Position, pos2 *Position) int {
|
||||
if pos1.Line < pos2.Line || (pos1.Line == pos2.Line && pos1.Col < pos2.Col) {
|
||||
return 1
|
||||
}
|
||||
if pos1.Line > pos2.Line || (pos1.Line == pos2.Line && pos1.Col > pos2.Col) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// NewBuffer creates a new terminal buffer
|
||||
func NewBuffer(terminalState *TerminalState) *Buffer {
|
||||
b := &Buffer{
|
||||
lines: []Line{},
|
||||
selectionStart: nil,
|
||||
selectionEnd: nil,
|
||||
selectionMode: SelectionChar,
|
||||
isSelectionComplete: true,
|
||||
terminalState: terminalState,
|
||||
}
|
||||
return b
|
||||
|
@ -84,50 +107,13 @@ func (buffer *Buffer) GetURLAtPosition(col uint16, viewRow uint16) string {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) IsSelectionComplete() bool {
|
||||
return buffer.selectionComplete
|
||||
return buffer.isSelectionComplete
|
||||
}
|
||||
|
||||
func (buffer *Buffer) SelectLineAtPosition(col uint16, viewRow uint16) {
|
||||
row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom)
|
||||
|
||||
buffer.selectionStart = &Position {
|
||||
Col: 0,
|
||||
Line: int(row),
|
||||
}
|
||||
buffer.selectionEnd = &Position {
|
||||
Col: int(buffer.ViewWidth() - 1),
|
||||
Line: int(row),
|
||||
}
|
||||
|
||||
buffer.selectionComplete = true
|
||||
buffer.emitDisplayChange()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) {
|
||||
|
||||
row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom)
|
||||
|
||||
cell := buffer.GetRawCell(col, row)
|
||||
if cell == nil || cell.Rune() == 0x00 {
|
||||
return
|
||||
}
|
||||
|
||||
start := col
|
||||
func (buffer *Buffer) findEndOfWord(col int, row int) int {
|
||||
end := col
|
||||
|
||||
for i := col; i >= uint16(0); i-- {
|
||||
cell := buffer.GetRawCell(i, row)
|
||||
if cell == nil {
|
||||
break
|
||||
}
|
||||
if isRuneWordSelectionMarker(cell.Rune()) {
|
||||
break
|
||||
}
|
||||
start = i
|
||||
}
|
||||
|
||||
for i := col; i < buffer.terminalState.viewWidth; i++ {
|
||||
cell := buffer.GetRawCell(i, row)
|
||||
for i := col; i < int(buffer.terminalState.viewWidth); i++ {
|
||||
cell := buffer.GetRawCell(uint16(i), uint64(row))
|
||||
if cell == nil {
|
||||
break
|
||||
}
|
||||
|
@ -136,18 +122,22 @@ func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) {
|
|||
}
|
||||
end = i
|
||||
}
|
||||
|
||||
buffer.selectionStart = &Position{
|
||||
Col: int(start),
|
||||
Line: int(row),
|
||||
}
|
||||
buffer.selectionEnd = &Position{
|
||||
Col: int(end),
|
||||
Line: int(row),
|
||||
return end
|
||||
}
|
||||
|
||||
buffer.selectionComplete = true
|
||||
buffer.emitDisplayChange()
|
||||
func (buffer *Buffer) findBeginningOfWord(col int, row int) int {
|
||||
start := col
|
||||
for i := col; i >= 0; i-- {
|
||||
cell := buffer.GetRawCell(uint16(i), uint64(row))
|
||||
if cell == nil {
|
||||
break
|
||||
}
|
||||
if isRuneWordSelectionMarker(cell.Rune()) {
|
||||
break
|
||||
}
|
||||
start = i
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
// bounds for word selection
|
||||
|
@ -170,29 +160,15 @@ func isRuneURLSelectionMarker(r rune) bool {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) GetSelectedText() string {
|
||||
if buffer.selectionStart == nil || buffer.selectionEnd == nil {
|
||||
start, end := buffer.getActualSelection()
|
||||
if start == nil || end == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var x1, x2, y1, y2 int
|
||||
|
||||
if buffer.selectionStart.Line > buffer.selectionEnd.Line || (buffer.selectionStart.Line == buffer.selectionEnd.Line && buffer.selectionStart.Col > buffer.selectionEnd.Col) {
|
||||
y2 = buffer.selectionStart.Line
|
||||
y1 = buffer.selectionEnd.Line
|
||||
x2 = buffer.selectionStart.Col
|
||||
x1 = buffer.selectionEnd.Col
|
||||
} else {
|
||||
y1 = buffer.selectionStart.Line
|
||||
y2 = buffer.selectionEnd.Line
|
||||
x1 = buffer.selectionStart.Col
|
||||
x2 = buffer.selectionEnd.Col
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.Grow( int(buffer.terminalState.viewWidth) * (y2 - y1 + 1)) // reserve space to minimize allocations
|
||||
|
||||
for row := y1; row <= y2; row++ {
|
||||
builder.Grow( int(buffer.terminalState.viewWidth) * (end.Line - start.Line + 1)) // reserve space to minimize allocations
|
||||
|
||||
for row := start.Line; row <= end.Line; row++ {
|
||||
if row >= len(buffer.lines) {
|
||||
break
|
||||
}
|
||||
|
@ -201,13 +177,13 @@ func (buffer *Buffer) GetSelectedText() string {
|
|||
|
||||
minX := 0
|
||||
maxX := int(buffer.terminalState.viewWidth) - 1
|
||||
if row == y1 {
|
||||
minX = x1
|
||||
if row == start.Line {
|
||||
minX = start.Col
|
||||
} else if !line.wrapped {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if row == y2 {
|
||||
maxX = x2
|
||||
if row == end.Line {
|
||||
maxX = end.Col
|
||||
}
|
||||
|
||||
for col := minX; col <= maxX; col++ {
|
||||
|
@ -225,26 +201,35 @@ func (buffer *Buffer) GetSelectedText() string {
|
|||
return builder.String()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) StartSelection(col uint16, viewRow uint16) {
|
||||
func (buffer *Buffer) StartSelection(col uint16, viewRow uint16, mode SelectionMode) {
|
||||
row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom)
|
||||
buffer.selectionComplete = false
|
||||
buffer.selectionMode = mode
|
||||
|
||||
buffer.selectionStart = &Position {
|
||||
Col: int(col),
|
||||
Line: int(row),
|
||||
}
|
||||
|
||||
if mode == SelectionChar {
|
||||
buffer.selectionEnd = nil
|
||||
} else {
|
||||
buffer.selectionEnd = &Position {
|
||||
Col: int(col),
|
||||
Line: int(row),
|
||||
}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) EndSelection(col uint16, viewRow uint16, complete bool) {
|
||||
buffer.isSelectionComplete = false
|
||||
|
||||
if buffer.selectionComplete {
|
||||
buffer.emitDisplayChange()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) ExtendSelection(col uint16, viewRow uint16, complete bool) {
|
||||
|
||||
if buffer.isSelectionComplete {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.selectionComplete = complete
|
||||
|
||||
defer buffer.emitDisplayChange()
|
||||
|
||||
if buffer.selectionStart == nil {
|
||||
|
@ -254,47 +239,82 @@ func (buffer *Buffer) EndSelection(col uint16, viewRow uint16, complete bool) {
|
|||
|
||||
row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom)
|
||||
|
||||
if int(col) == buffer.selectionStart.Col && int(row) == int(buffer.selectionStart.Line) && complete {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.selectionEnd = &Position {
|
||||
Col: int(col),
|
||||
Line: int(row),
|
||||
}
|
||||
|
||||
if complete {
|
||||
buffer.isSelectionComplete = true
|
||||
}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) ClearSelection() {
|
||||
buffer.selectionStart = nil
|
||||
buffer.selectionEnd = nil
|
||||
buffer.selectionComplete = true
|
||||
buffer.isSelectionComplete = true
|
||||
|
||||
buffer.emitDisplayChange()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) InSelection(col uint16, row uint16) bool {
|
||||
|
||||
func (buffer *Buffer) getActualSelection() (*Position, *Position) {
|
||||
if buffer.selectionStart == nil || buffer.selectionEnd == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
start := &Position {}
|
||||
end := &Position {}
|
||||
|
||||
if comparePositions(buffer.selectionStart, buffer.selectionEnd) >= 0 {
|
||||
start.Col = buffer.selectionStart.Col
|
||||
start.Line = buffer.selectionStart.Line
|
||||
|
||||
end.Col = buffer.selectionEnd.Col
|
||||
end.Line = buffer.selectionEnd.Line
|
||||
} else {
|
||||
start.Col = buffer.selectionEnd.Col
|
||||
start.Line = buffer.selectionEnd.Line
|
||||
|
||||
end.Col = buffer.selectionStart.Col
|
||||
end.Line = buffer.selectionStart.Line
|
||||
}
|
||||
|
||||
switch buffer.selectionMode {
|
||||
case SelectionChar:
|
||||
// no action
|
||||
|
||||
case SelectionWord:
|
||||
start.Col = buffer.findBeginningOfWord(start.Col, start.Line)
|
||||
end.Col = buffer.findEndOfWord(end.Col, end.Line)
|
||||
|
||||
case SelectionLine:
|
||||
start.Col = 0
|
||||
end.Col = int(buffer.ViewWidth() - 1)
|
||||
}
|
||||
|
||||
if start.Line >= len(buffer.lines) {
|
||||
start.Col = 0
|
||||
} else if start.Col >= len(buffer.lines[start.Line].cells) {
|
||||
start.Col = len(buffer.lines[start.Line].cells)
|
||||
}
|
||||
|
||||
if end.Line >= len(buffer.lines) || end.Col >= len(buffer.lines[end.Line].cells) {
|
||||
end.Col = int(buffer.ViewWidth() - 1)
|
||||
}
|
||||
|
||||
return start, end
|
||||
}
|
||||
|
||||
func (buffer *Buffer) InSelection(col uint16, row uint16) bool {
|
||||
start, end := buffer.getActualSelection()
|
||||
if start == nil || end == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var x1, x2, y1, y2 int
|
||||
|
||||
// first, let's put the selection points in the correct order, earliest first
|
||||
if buffer.selectionStart.Line > buffer.selectionEnd.Line || (buffer.selectionStart.Line == buffer.selectionEnd.Line && buffer.selectionStart.Col > buffer.selectionEnd.Col) {
|
||||
y2 = buffer.selectionStart.Line
|
||||
y1 = buffer.selectionEnd.Line
|
||||
x2 = buffer.selectionStart.Col
|
||||
x1 = buffer.selectionEnd.Col
|
||||
} else {
|
||||
y1 = buffer.selectionStart.Line
|
||||
y2 = buffer.selectionEnd.Line
|
||||
x1 = buffer.selectionStart.Col
|
||||
x2 = buffer.selectionEnd.Col
|
||||
}
|
||||
|
||||
rawY := int(buffer.convertViewLineToRawLine(row) - uint64(buffer.terminalState.scrollLinesFromBottom))
|
||||
return (rawY > y1 || (rawY == y1 && int(col) >= x1)) && (rawY < y2 || (rawY == y2 && int(col) <= x2))
|
||||
|
||||
return (rawY > start.Line || (rawY == start.Line && int(col) >= start.Col)) &&
|
||||
(rawY < end.Line || (rawY == end.Line && int(col) <= end.Col))
|
||||
}
|
||||
|
||||
func (buffer *Buffer) IsDirty() bool {
|
||||
|
|
|
@ -639,3 +639,75 @@ func TestBufferMaxLines(t *testing.T) {
|
|||
assert.Equal(t, "funny", b.lines[0].String())
|
||||
assert.Equal(t, "world", b.lines[1].String())
|
||||
}
|
||||
|
||||
func makeBufferForTestingSelection() *Buffer {
|
||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 10))
|
||||
b.terminalState.LineFeedMode = false
|
||||
|
||||
b.Write([]rune("The quick brown")...)
|
||||
b.NewLine()
|
||||
b.Write([]rune("fox jumps over")...)
|
||||
b.NewLine()
|
||||
b.Write([]rune("the lazy dog")...)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func TestSelectingChars(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(2, 0, SelectionChar)
|
||||
b.ExtendSelection(4, 1, true)
|
||||
|
||||
assert.Equal(t, "e quick brown\nfox j", b.GetSelectedText())
|
||||
}
|
||||
|
||||
func TestSelectingWordsDown(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(6, 1, SelectionWord)
|
||||
b.ExtendSelection(5, 2, true)
|
||||
|
||||
assert.Equal(t, "jumps over\nthe lazy", b.GetSelectedText())
|
||||
}
|
||||
|
||||
func TestSelectingWordsUp(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(5, 2, SelectionWord)
|
||||
b.ExtendSelection(6, 1, true)
|
||||
|
||||
assert.Equal(t, "jumps over\nthe lazy", b.GetSelectedText())
|
||||
}
|
||||
|
||||
func TestSelectingLinesDown(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(6, 1, SelectionLine)
|
||||
b.ExtendSelection(4, 2, true)
|
||||
|
||||
assert.Equal(t, "fox jumps over\nthe lazy dog", b.GetSelectedText())
|
||||
}
|
||||
|
||||
func TestSelectingLineUp(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(8, 2, SelectionLine)
|
||||
b.ExtendSelection(3, 1, true)
|
||||
|
||||
assert.Equal(t, "fox jumps over\nthe lazy dog", b.GetSelectedText())
|
||||
}
|
||||
|
||||
func TestSelectingAfterText(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(6, 3, SelectionChar)
|
||||
b.ExtendSelection(6, 3, true)
|
||||
|
||||
start, end := b.getActualSelection()
|
||||
|
||||
assert.Equal(t, start.Col, 0)
|
||||
assert.Equal(t, start.Line, 3)
|
||||
assert.Equal(t, end.Col, 79)
|
||||
assert.Equal(t, end.Line, 3)
|
||||
}
|
|
@ -54,6 +54,7 @@ type GUI struct {
|
|||
prevLeftClickY uint16
|
||||
leftClickTime time.Time
|
||||
leftClickCount int // number of clicks in a serie - single click, double click, or triple click
|
||||
mouseMovedAfterSelectionStarted bool
|
||||
internalResize bool
|
||||
}
|
||||
|
||||
|
|
26
gui/mouse.go
26
gui/mouse.go
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/liamg/aminal/terminal"
|
||||
"time"
|
||||
"github.com/liamg/aminal/buffer"
|
||||
)
|
||||
|
||||
func (gui *GUI) glfwScrollCallback(w *glfw.Window, xoff float64, yoff float64) {
|
||||
|
@ -39,7 +40,7 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) {
|
|||
x, y := gui.convertMouseCoordinates(px, py)
|
||||
|
||||
if gui.mouseDown {
|
||||
gui.terminal.ActiveBuffer().EndSelection(x, y, false)
|
||||
gui.terminal.ActiveBuffer().ExtendSelection(x, y, false)
|
||||
} else {
|
||||
|
||||
hint := gui.terminal.ActiveBuffer().GetHintAtPosition(x, y)
|
||||
|
@ -48,7 +49,6 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) {
|
|||
} else {
|
||||
gui.setOverlay(nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" {
|
||||
|
@ -109,20 +109,28 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
|
|||
gui.mouseDown = true
|
||||
|
||||
clickCount := gui.updateLeftClickCount(x, y)
|
||||
if clickCount == 1 || !activeBuffer.IsSelectionComplete() {
|
||||
activeBuffer.StartSelection(x, y)
|
||||
} else {
|
||||
switch clickCount {
|
||||
case 1:
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionChar)
|
||||
case 2:
|
||||
activeBuffer.SelectWordAtPosition(x, y)
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionWord)
|
||||
case 3:
|
||||
activeBuffer.SelectLineAtPosition(x, y)
|
||||
}
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionLine)
|
||||
}
|
||||
gui.mouseMovedAfterSelectionStarted = false
|
||||
|
||||
} else if action == glfw.Release {
|
||||
gui.mouseDown = false
|
||||
activeBuffer.EndSelection(x, y, true)
|
||||
|
||||
if x != gui.prevLeftClickX || y != gui.prevLeftClickY {
|
||||
gui.mouseMovedAfterSelectionStarted = true
|
||||
}
|
||||
|
||||
if gui.leftClickCount != 1 || gui.mouseMovedAfterSelectionStarted {
|
||||
activeBuffer.ExtendSelection(x, y, true)
|
||||
}
|
||||
|
||||
// Do copy to clipboard *or* open URL, but not both.
|
||||
handled := false
|
||||
if gui.config.CopyAndPasteWithMouse {
|
||||
selectedText := activeBuffer.GetSelectedText()
|
||||
|
|
Loading…
Reference in New Issue