From 4ec83c54a43c84438e48242e652a4d1fe358621e Mon Sep 17 00:00:00 2001 From: rrrooommmaaa Date: Tue, 5 Mar 2019 09:55:29 +0300 Subject: [PATCH] #217 rectangular select and copy (#241) * #217 rectangular select and copy --- buffer/buffer.go | 62 ++++++++++++++++++++++++++++++++++++++----- buffer/buffer_test.go | 12 ++++----- gui/actions.go | 4 +-- gui/gui.go | 3 ++- gui/input.go | 13 ++++++++- gui/mouse.go | 7 ++--- 6 files changed, 82 insertions(+), 19 deletions(-) diff --git a/buffer/buffer.go b/buffer/buffer.go index c721e3e..cb4544f 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -10,11 +10,15 @@ import ( ) type SelectionMode int +type SelectionRegionMode int const ( SelectionChar SelectionMode = iota // char-by-char selection SelectionWord SelectionMode = iota // by word selection SelectionLine SelectionMode = iota // whole line selection + + SelectionRegionNormal SelectionRegionMode = iota + SelectionRegionRectangular ) type Buffer struct { @@ -160,12 +164,38 @@ func isRuneURLSelectionMarker(r rune) bool { return false } -func (buffer *Buffer) GetSelectedText() string { - start, end := buffer.getActualSelection() +func (buffer *Buffer) getRectangleText(start *Position, end *Position) string { + var builder strings.Builder + builder.Grow((end.Col - start.Col + 2) * (end.Line - start.Line + 1)) // reserve space to minimize allocations + + for row := start.Line; row <= end.Line; row++ { + for col := start.Col; col <= end.Col; col++ { + if row < len(buffer.lines) && col < len(buffer.lines[row].cells) { + r := buffer.lines[row].cells[col].Rune() + if r == 0x00 { + r = ' ' + } + builder.WriteRune(r) + } else { + builder.WriteRune(' ') + } + } + builder.WriteString("\n") + } + + return builder.String() +} + +func (buffer *Buffer) GetSelectedText(selectionRegionMode SelectionRegionMode) string { + start, end := buffer.getActualSelection(selectionRegionMode) if start == nil || end == nil { return "" } + if selectionRegionMode == SelectionRegionRectangular { + return buffer.getRectangleText(start, end) + } + var builder strings.Builder builder.Grow(int(buffer.terminalState.viewWidth) * (end.Line - start.Line + 1)) // reserve space to minimize allocations @@ -258,7 +288,7 @@ func (buffer *Buffer) ClearSelection() { buffer.emitDisplayChange() } -func (buffer *Buffer) getActualSelection() (*Position, *Position) { +func (buffer *Buffer) getActualSelection(selectionRegionMode SelectionRegionMode) (*Position, *Position) { if buffer.selectionStart == nil || buffer.selectionEnd == nil { return nil, nil } @@ -266,7 +296,23 @@ func (buffer *Buffer) getActualSelection() (*Position, *Position) { start := &Position{} end := &Position{} - if comparePositions(buffer.selectionStart, buffer.selectionEnd) >= 0 { + if selectionRegionMode == SelectionRegionRectangular { + if buffer.selectionStart.Col > buffer.selectionEnd.Col { + start.Col = buffer.selectionEnd.Col + end.Col = buffer.selectionStart.Col + } else { + start.Col = buffer.selectionStart.Col + end.Col = buffer.selectionEnd.Col + } + if buffer.selectionStart.Line > buffer.selectionEnd.Line { + start.Line = buffer.selectionEnd.Line + end.Line = buffer.selectionStart.Line + } else { + start.Line = buffer.selectionStart.Line + end.Line = buffer.selectionEnd.Line + } + return start, end + } else if comparePositions(buffer.selectionStart, buffer.selectionEnd) >= 0 { start.Col = buffer.selectionStart.Col start.Line = buffer.selectionStart.Line @@ -306,14 +352,18 @@ func (buffer *Buffer) getActualSelection() (*Position, *Position) { return start, end } -func (buffer *Buffer) InSelection(col uint16, row uint16) bool { - start, end := buffer.getActualSelection() +func (buffer *Buffer) InSelection(col uint16, row uint16, selectionRegionMode SelectionRegionMode) bool { + start, end := buffer.getActualSelection(selectionRegionMode) if start == nil || end == nil { return false } rawY := int(buffer.convertViewLineToRawLine(row) - uint64(buffer.terminalState.scrollLinesFromBottom)) + if selectionRegionMode == SelectionRegionRectangular { + return rawY >= start.Line && rawY <= end.Line && int(col) >= start.Col && int(col) <= end.Col + } + return (rawY > start.Line || (rawY == start.Line && int(col) >= start.Col)) && (rawY < end.Line || (rawY == end.Line && int(col) <= end.Col)) } diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go index ad866ec..43064bf 100644 --- a/buffer/buffer_test.go +++ b/buffer/buffer_test.go @@ -659,7 +659,7 @@ func TestSelectingChars(t *testing.T) { b.StartSelection(2, 0, SelectionChar) b.ExtendSelection(4, 1, true) - assert.Equal(t, "e quick brown\nfox j", b.GetSelectedText()) + assert.Equal(t, "e quick brown\nfox j", b.GetSelectedText(SelectionRegionNormal)) } func TestSelectingWordsDown(t *testing.T) { @@ -668,7 +668,7 @@ func TestSelectingWordsDown(t *testing.T) { b.StartSelection(6, 1, SelectionWord) b.ExtendSelection(5, 2, true) - assert.Equal(t, "jumps over\nthe lazy", b.GetSelectedText()) + assert.Equal(t, "jumps over\nthe lazy", b.GetSelectedText(SelectionRegionNormal)) } func TestSelectingWordsUp(t *testing.T) { @@ -677,7 +677,7 @@ func TestSelectingWordsUp(t *testing.T) { b.StartSelection(5, 2, SelectionWord) b.ExtendSelection(6, 1, true) - assert.Equal(t, "jumps over\nthe lazy", b.GetSelectedText()) + assert.Equal(t, "jumps over\nthe lazy", b.GetSelectedText(SelectionRegionNormal)) } func TestSelectingLinesDown(t *testing.T) { @@ -686,7 +686,7 @@ func TestSelectingLinesDown(t *testing.T) { b.StartSelection(6, 1, SelectionLine) b.ExtendSelection(4, 2, true) - assert.Equal(t, "fox jumps over\nthe lazy dog", b.GetSelectedText()) + assert.Equal(t, "fox jumps over\nthe lazy dog", b.GetSelectedText(SelectionRegionNormal)) } func TestSelectingLineUp(t *testing.T) { @@ -695,7 +695,7 @@ func TestSelectingLineUp(t *testing.T) { b.StartSelection(8, 2, SelectionLine) b.ExtendSelection(3, 1, true) - assert.Equal(t, "fox jumps over\nthe lazy dog", b.GetSelectedText()) + assert.Equal(t, "fox jumps over\nthe lazy dog", b.GetSelectedText(SelectionRegionNormal)) } func TestSelectingAfterText(t *testing.T) { @@ -704,7 +704,7 @@ func TestSelectingAfterText(t *testing.T) { b.StartSelection(6, 3, SelectionChar) b.ExtendSelection(6, 3, true) - start, end := b.getActualSelection() + start, end := b.getActualSelection(SelectionRegionNormal) assert.Equal(t, start.Col, 0) assert.Equal(t, start.Line, 3) diff --git a/gui/actions.go b/gui/actions.go index 64c030f..4134f7c 100644 --- a/gui/actions.go +++ b/gui/actions.go @@ -18,7 +18,7 @@ var actionMap = map[config.UserAction]func(gui *GUI){ } func actionCopy(gui *GUI) { - selectedText := gui.terminal.ActiveBuffer().GetSelectedText() + selectedText := gui.terminal.ActiveBuffer().GetSelectedText(gui.selectionRegionMode) if selectedText != "" { gui.window.SetClipboardString(selectedText) @@ -37,7 +37,7 @@ func actionToggleDebug(gui *GUI) { } func actionSearchSelection(gui *GUI) { - keywords := gui.terminal.ActiveBuffer().GetSelectedText() + keywords := gui.terminal.ActiveBuffer().GetSelectedText(gui.selectionRegionMode) if keywords != "" && gui.config.SearchURL != "" && strings.Contains(gui.config.SearchURL, "$QUERY") { gui.launchTarget(fmt.Sprintf(strings.Replace(gui.config.SearchURL, "$QUERY", "%s", 1), url.QueryEscape(keywords))) } diff --git a/gui/gui.go b/gui/gui.go index 2d968c6..bbeecdf 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -58,6 +58,7 @@ type GUI struct { leftClickCount int // number of clicks in a serie - single click, double click, or triple click mouseMovedAfterSelectionStarted bool internalResize bool + selectionRegionMode buffer.SelectionRegionMode } func Min(x, y int) int { @@ -488,7 +489,7 @@ func (gui *GUI) redraw() { cursor = cx == uint(x) && cy == uint(y) } - if gui.terminal.ActiveBuffer().InSelection(uint16(x), uint16(y)) { + if gui.terminal.ActiveBuffer().InSelection(uint16(x), uint16(y), gui.selectionRegionMode) { colour = &gui.config.ColourScheme.Selection } else { colour = nil diff --git a/gui/input.go b/gui/input.go index bc016c2..c5caada 100644 --- a/gui/input.go +++ b/gui/input.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/go-gl/glfw/v3.2/glfw" + "github.com/liamg/aminal/buffer" ) // send typed runes straight through to the pty @@ -44,8 +45,18 @@ func getModStr(mods glfw.ModifierKey) string { return "" } -func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { +func (gui *GUI) updateSelectionMode(mods glfw.ModifierKey) { + mode := buffer.SelectionRegionNormal + if modsPressed(mods, glfw.ModAlt) { + mode = buffer.SelectionRegionRectangular + } + if gui.selectionRegionMode != mode { + gui.selectionRegionMode = mode + gui.terminal.SetDirty() + } +} +func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { if action == glfw.Repeat || action == glfw.Press { if gui.overlay != nil { diff --git a/gui/mouse.go b/gui/mouse.go index 3c8962d..14c71c5 100644 --- a/gui/mouse.go +++ b/gui/mouse.go @@ -143,7 +143,7 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act gui.mouseDown = true if gui.terminal.GetMouseMode() != terminal.MouseModeButtonEvent { - gui.handleSelectionButtonPress(x, y) + gui.handleSelectionButtonPress(x, y, mod) } } else if action == glfw.Release { gui.mouseDown = false @@ -255,9 +255,10 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act } -func (gui *GUI) handleSelectionButtonPress(x uint16, y uint16) { +func (gui *GUI) handleSelectionButtonPress(x uint16, y uint16, mod glfw.ModifierKey) { activeBuffer := gui.terminal.ActiveBuffer() clickCount := gui.updateLeftClickCount(x, y) + gui.updateSelectionMode(mod) switch clickCount { case 1: activeBuffer.StartSelection(x, y, buffer.SelectionChar) @@ -282,7 +283,7 @@ func (gui *GUI) handleSelectionButtonRelease(x uint16, y uint16) { // Do copy to clipboard *or* open URL, but not both. handled := false if gui.config.CopyAndPasteWithMouse { - selectedText := activeBuffer.GetSelectedText() + selectedText := activeBuffer.GetSelectedText(gui.selectionRegionMode) if selectedText != "" { gui.window.SetClipboardString(selectedText) handled = true