nice tested buffer package

This commit is contained in:
Liam Galvin 2018-08-06 14:51:23 +01:00
parent 89b8002d48
commit 002d617630
8 changed files with 463 additions and 68 deletions

View File

@ -1,75 +1,183 @@
package buffer
import (
"fmt"
"github.com/sirupsen/logrus"
)
type Buffer struct {
lines []line
x uint16
y uint16
columnCount uint16
viewHeight uint16
lines []Line
cursorX uint16
cursorY uint16
viewHeight uint16
viewWidth uint16
cursorAttr CellAttributes
}
// NewBuffer creates a new terminal buffer
func NewBuffer() *Buffer {
return &Buffer{
x: 0,
y: 0,
lines: []line{},
columnCount: 0,
func NewBuffer(viewCols uint16, viewLines uint16) *Buffer {
b := &Buffer{
cursorX: 0,
cursorY: 0,
lines: []Line{},
}
b.ResizeView(viewCols, viewLines)
return b
}
// Column returns cursor column
func (buffer *Buffer) Column() uint16 {
return buffer.x
func (buffer *Buffer) CursorColumn() uint16 {
return buffer.cursorX
}
// Line returns cursor line
func (buffer *Buffer) Line() uint16 {
return buffer.y
func (buffer *Buffer) CursorLine() uint16 {
return buffer.cursorY
}
// translates the cursor line to the raw buffer line
func (buffer *Buffer) RawLine() uint64 {
rawHeight := buffer.Height()
if int(buffer.viewHeight) > rawHeight {
return uint64(buffer.cursorY)
}
return uint64(int(buffer.cursorY) + (rawHeight - int(buffer.viewHeight)))
}
// Width returns the width of the buffer in columns
func (buffer *Buffer) Width() uint16 {
return buffer.columnCount
return buffer.viewWidth
}
func (buffer *Buffer) ViewWidth() uint16 {
return buffer.viewWidth
}
func (buffer *Buffer) Height() int {
return len(buffer.lines)
}
func (buffer *Buffer) ViewHeight() uint16 {
return buffer.viewHeight
}
func (buffer *Buffer) ensureLinesExistToRawHeight() {
for int(buffer.RawLine()) >= len(buffer.lines) {
buffer.lines = append(buffer.lines, newLine())
}
}
// Write will write a rune to the terminal at the position of the cursor, and increment the cursor position
func (buffer *Buffer) Write(r rune) {
for int(buffer.Line()) >= len(buffer.lines) {
buffer.lines = append(buffer.lines, newLine())
func (buffer *Buffer) Write(runes ...rune) {
for _, r := range runes {
buffer.ensureLinesExistToRawHeight()
if r == 0x0a {
buffer.NewLine()
continue
}
line := &buffer.lines[buffer.RawLine()]
for int(buffer.CursorColumn()) >= len(line.cells) {
line.cells = append(line.cells, newCell())
}
cell := &line.cells[buffer.CursorColumn()]
cell.setRune(r)
cell.attr = buffer.cursorAttr
buffer.incrementCursorPosition()
}
line := &buffer.lines[buffer.Line()]
for int(buffer.Column()) >= len(line.cells) {
line.cells = append(line.cells, newCell())
}
cell := line.cells[buffer.Column()]
cell.setRune(r)
buffer.incrementCursorPosition()
}
func (buffer *Buffer) incrementCursorPosition() {
if buffer.Column()+1 < buffer.Width() {
buffer.x++
if buffer.CursorColumn()+1 < buffer.Width() {
buffer.cursorX++
} else {
buffer.y++
buffer.x = 0
if buffer.cursorY == buffer.viewHeight-1 { // if we're on the last line, we can't move the cursor down, we have to move the buffer up, i.e. add a new line
line := newLine()
line.setWrapped(true)
buffer.lines = append(buffer.lines, line)
buffer.cursorX = 0
} else {
buffer.cursorX = 0
if buffer.Height() < int(buffer.ViewHeight()) {
line := newLine()
line.setWrapped(true)
buffer.lines = append(buffer.lines, line)
buffer.cursorY++
} else {
panic("no test for this yet - not sure if possible?")
line := &buffer.lines[buffer.RawLine()]
line.setWrapped(true)
}
}
}
}
func (buffer *Buffer) SetPosition(col uint16, line uint16) error {
if buffer.x >= buffer.Width() {
return fmt.Errorf("Cannot set cursor position: column %d is outside of the current buffer width (%d columns)", col, buffer.Width())
func (buffer *Buffer) NewLine() {
// if we're at the beginning of a line which wrapped from the previous one, and we need a new line, we can effectively not add a new line, and set the current one to non-wrapped
if buffer.cursorX == 0 {
line := &buffer.lines[buffer.RawLine()]
if line.wrapped {
line.setWrapped(false)
return
}
}
if buffer.cursorY == buffer.viewHeight-1 {
buffer.lines = append(buffer.lines, newLine())
buffer.cursorX = 0
} else {
buffer.cursorX = 0
buffer.cursorY++
}
buffer.x = col
buffer.y = line
return nil
}
func (buffer *Buffer) Resize(cols int, lines int) {
func (buffer *Buffer) MovePosition(x int16, y int16) {
if int16(buffer.cursorX)+x < 0 {
x = -int16(buffer.cursorX)
}
if int16(buffer.cursorY)+y < 0 {
y = -int16(buffer.cursorY)
}
buffer.SetPosition(uint16(int16(buffer.cursorX)+x), uint16(int16(buffer.cursorY)+y))
}
func (buffer *Buffer) SetPosition(col uint16, line uint16) {
if col >= buffer.ViewWidth() {
col = buffer.ViewWidth() - 1
logrus.Errorf("Cannot set cursor position: column %d is outside of the current view width (%d columns)", col, buffer.ViewWidth())
}
if line >= buffer.ViewHeight() {
line = buffer.ViewHeight() - 1
logrus.Errorf("Cannot set cursor position: line %d is outside of the current view height (%d lines)", line, buffer.ViewHeight())
}
buffer.cursorX = col
buffer.cursorY = line
}
func (buffer *Buffer) GetVisibleLines() []Line {
lines := []Line{}
for i := buffer.Height() - int(buffer.ViewHeight()); i < buffer.Height(); i++ {
if i >= 0 && i < len(buffer.lines) {
lines = append(lines, buffer.lines[i])
}
}
return lines
}
// tested to here
func (buffer *Buffer) Clear() {
for i := 0; i < int(buffer.ViewHeight()); i++ {
buffer.lines = append(buffer.lines, newLine())
}
buffer.SetPosition(0, 0)
}
func (buffer *Buffer) ResizeView(width uint16, height uint16) {
buffer.viewWidth = width
buffer.viewHeight = height
// @todo wrap/unwrap
}

View File

@ -8,36 +8,47 @@ import (
"github.com/stretchr/testify/assert"
)
func TestOffsets(t *testing.T) {
b := NewBuffer(10, 8)
test := "hellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\n?"
b.Write([]rune(test)...)
assert.Equal(t, uint16(10), b.ViewWidth())
assert.Equal(t, uint16(10), b.Width())
assert.Equal(t, uint16(8), b.ViewHeight())
assert.Equal(t, 13, b.Height())
}
func TestBufferCreation(t *testing.T) {
b := NewBuffer(10)
assert.Equal(t, 10, b.Width())
assert.Equal(t, 0, b.Column())
assert.Equal(t, 0, b.Line())
b := NewBuffer(10, 20)
assert.Equal(t, uint16(10), b.Width())
assert.Equal(t, uint16(20), b.ViewHeight())
assert.Equal(t, uint16(0), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
assert.NotNil(t, b.lines)
}
func TestBufferCursorIncrement(t *testing.T) {
b := NewBuffer(5)
b := NewBuffer(5, 4)
b.incrementCursorPosition()
require.Equal(t, 1, b.Column())
require.Equal(t, 0, b.Line())
require.Equal(t, uint16(1), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
b.incrementCursorPosition()
require.Equal(t, 2, b.Column())
require.Equal(t, 0, b.Line())
require.Equal(t, uint16(2), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
b.incrementCursorPosition()
require.Equal(t, 3, b.Column())
require.Equal(t, 0, b.Line())
require.Equal(t, uint16(3), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
b.incrementCursorPosition()
require.Equal(t, 4, b.Column())
require.Equal(t, 0, b.Line())
require.Equal(t, uint16(4), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
b.incrementCursorPosition()
require.Equal(t, 0, b.Column())
require.Equal(t, 1, b.Line())
require.Equal(t, uint16(0), b.CursorColumn())
require.Equal(t, uint16(1), b.CursorLine())
b.incrementCursorPosition()
b.incrementCursorPosition()
@ -50,11 +61,179 @@ func TestBufferCursorIncrement(t *testing.T) {
b.incrementCursorPosition()
b.incrementCursorPosition()
require.Equal(t, 0, b.Column())
require.Equal(t, 3, b.Line())
require.Equal(t, uint16(0), b.CursorColumn())
require.Equal(t, uint16(3), b.CursorLine())
b.Write([]rune("hello\n")...)
b.Write([]rune("hello\n")...)
b.Write([]rune("hello\n")...)
b.Write([]rune("hello\n")...)
b.Write([]rune("hello\n")...)
b.Write([]rune("hello")...)
b.SetPosition(0, 2)
b.incrementCursorPosition()
}
func TestBufferWrite(t *testing.T) {
b := NewBuffer(5, 20)
assert.Equal(t, uint16(0), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
b.Write('a')
assert.Equal(t, uint16(1), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
b.Write('b')
assert.Equal(t, uint16(2), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
b.Write('c')
assert.Equal(t, uint16(3), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
b.Write('d')
assert.Equal(t, uint16(4), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
b.Write('e')
assert.Equal(t, uint16(0), b.CursorColumn())
assert.Equal(t, uint16(1), b.CursorLine())
b.Write('f')
assert.Equal(t, uint16(1), b.CursorColumn())
assert.Equal(t, uint16(1), b.CursorLine())
//b.lines[0].cells[]
}
func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
b := NewBuffer(3, 20)
b.Write('a', 'b', 'c')
assert.Equal(t, uint16(0), b.cursorX)
b.Write(0x0a)
b.Write('d', 'e', 'f')
b.Write(0x0a)
assert.Equal(t, "abc", b.lines[0].String())
assert.Equal(t, "def", b.lines[1].String())
assert.Equal(t, "", b.lines[2].String())
}
func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
b := NewBuffer(3, 20)
b.Write('a', 'b', 'c', 'd')
b.Write(0x0a)
b.Write('e', 'f')
b.Write(0x0a)
b.Write(0x0a)
b.Write(0x0a)
b.Write('z')
assert.Equal(t, "abc", b.lines[0].String())
assert.Equal(t, "d", b.lines[1].String())
assert.Equal(t, "ef", b.lines[2].String())
assert.Equal(t, "", b.lines[3].String())
assert.Equal(t, "", b.lines[4].String())
assert.Equal(t, "z", b.lines[5].String())
}
func TestSetPosition(t *testing.T) {
b := NewBuffer(120, 80)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.SetPosition(60, 10)
assert.Equal(t, 60, int(b.CursorColumn()))
assert.Equal(t, 10, int(b.CursorLine()))
b.SetPosition(0, 0)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.SetPosition(120, 90)
assert.Equal(t, 119, int(b.CursorColumn()))
assert.Equal(t, 79, int(b.CursorLine()))
}
func TestMovePosition(t *testing.T) {
b := NewBuffer(120, 80)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.MovePosition(-1, -1)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.MovePosition(30, 20)
assert.Equal(t, 30, int(b.CursorColumn()))
assert.Equal(t, 20, int(b.CursorLine()))
b.MovePosition(30, 20)
assert.Equal(t, 60, int(b.CursorColumn()))
assert.Equal(t, 40, int(b.CursorLine()))
b.MovePosition(-1, -1)
assert.Equal(t, 59, int(b.CursorColumn()))
assert.Equal(t, 39, int(b.CursorLine()))
b.MovePosition(100, 100)
assert.Equal(t, 119, int(b.CursorColumn()))
assert.Equal(t, 79, int(b.CursorLine()))
}
func TestVisibleLines(t *testing.T) {
b := NewBuffer(80, 10)
b.Write([]rune("hello 1\n")...)
b.Write([]rune("hello 2\n")...)
b.Write([]rune("hello 3\n")...)
b.Write([]rune("hello 4\n")...)
b.Write([]rune("hello 5\n")...)
b.Write([]rune("hello 6\n")...)
b.Write([]rune("hello 7\n")...)
b.Write([]rune("hello 8\n")...)
b.Write([]rune("hello 9\n")...)
b.Write([]rune("hello 10\n")...)
b.Write([]rune("hello 11\n")...)
b.Write([]rune("hello 12\n")...)
b.Write([]rune("hello 13\n")...)
b.Write([]rune("hello 14")...)
lines := b.GetVisibleLines()
require.Equal(t, 10, len(lines))
assert.Equal(t, "hello 5", lines[0].String())
assert.Equal(t, "hello 14", lines[9].String())
}
func TestClearWithoutFullView(t *testing.T) {
b := NewBuffer(80, 10)
b.Write([]rune("hello 1\n")...)
b.Write([]rune("hello 2\n")...)
b.Write([]rune("hello 3")...)
b.Clear()
lines := b.GetVisibleLines()
for _, line := range lines {
assert.Equal(t, "", line.String())
}
}
func TestClearWithFullView(t *testing.T) {
b := NewBuffer(80, 5)
b.Write([]rune("hello 1\n")...)
b.Write([]rune("hello 2\n")...)
b.Write([]rune("hello 3\n")...)
b.Write([]rune("hello 4\n")...)
b.Write([]rune("hello 5\n")...)
b.Write([]rune("hello 6\n")...)
b.Write([]rune("hello 7\n")...)
b.Write([]rune("hello 8\n")...)
b.Clear()
lines := b.GetVisibleLines()
for _, line := range lines {
assert.Equal(t, "", line.String())
}
}
func TestResizeView(t *testing.T) {
b := NewBuffer(80, 20)
b.ResizeView(40, 10)
}

18
buffer/cell_test.go Normal file
View File

@ -0,0 +1,18 @@
package buffer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetRune(t *testing.T) {
cell := newCell()
assert.False(t, cell.hasContent)
cell.setRune('X')
assert.True(t, cell.hasContent)
assert.Equal(t, 'X', cell.r)
cell.setRune('Y')
assert.True(t, cell.hasContent)
assert.Equal(t, 'Y', cell.r)
}

58
buffer/coverage.out Normal file
View File

@ -0,0 +1,58 @@
mode: set
gitlab.com/liamg/raft/buffer/buffer.go:17.59,25.2 3 1
gitlab.com/liamg/raft/buffer/buffer.go:28.45,30.2 1 1
gitlab.com/liamg/raft/buffer/buffer.go:33.43,35.2 1 1
gitlab.com/liamg/raft/buffer/buffer.go:38.40,40.40 2 1
gitlab.com/liamg/raft/buffer/buffer.go:43.2,43.75 1 1
gitlab.com/liamg/raft/buffer/buffer.go:40.40,42.3 1 1
gitlab.com/liamg/raft/buffer/buffer.go:47.38,49.2 1 1
gitlab.com/liamg/raft/buffer/buffer.go:51.42,53.2 1 1
gitlab.com/liamg/raft/buffer/buffer.go:55.36,57.2 1 1
gitlab.com/liamg/raft/buffer/buffer.go:59.43,61.2 1 1
gitlab.com/liamg/raft/buffer/buffer.go:63.53,64.49 1 1
gitlab.com/liamg/raft/buffer/buffer.go:64.49,66.3 1 1
gitlab.com/liamg/raft/buffer/buffer.go:70.44,71.26 1 1
gitlab.com/liamg/raft/buffer/buffer.go:71.26,73.16 2 1
gitlab.com/liamg/raft/buffer/buffer.go:77.3,78.53 2 1
gitlab.com/liamg/raft/buffer/buffer.go:81.3,84.35 4 1
gitlab.com/liamg/raft/buffer/buffer.go:73.16,75.12 2 1
gitlab.com/liamg/raft/buffer/buffer.go:78.53,80.4 1 1
gitlab.com/liamg/raft/buffer/buffer.go:88.49,90.46 1 1
gitlab.com/liamg/raft/buffer/buffer.go:90.46,92.3 1 1
gitlab.com/liamg/raft/buffer/buffer.go:92.8,93.44 1 1
gitlab.com/liamg/raft/buffer/buffer.go:93.44,98.4 4 1
gitlab.com/liamg/raft/buffer/buffer.go:98.9,100.50 2 1
gitlab.com/liamg/raft/buffer/buffer.go:100.50,105.5 4 1
gitlab.com/liamg/raft/buffer/buffer.go:105.10,106.58 1 0
gitlab.com/liamg/raft/buffer/buffer.go:107.5,108.26 2 0
gitlab.com/liamg/raft/buffer/buffer.go:114.33,116.25 1 1
gitlab.com/liamg/raft/buffer/buffer.go:124.2,124.43 1 1
gitlab.com/liamg/raft/buffer/buffer.go:116.25,118.19 2 1
gitlab.com/liamg/raft/buffer/buffer.go:118.19,121.4 2 1
gitlab.com/liamg/raft/buffer/buffer.go:124.43,127.3 2 1
gitlab.com/liamg/raft/buffer/buffer.go:127.8,130.3 2 1
gitlab.com/liamg/raft/buffer/buffer.go:133.54,135.33 1 1
gitlab.com/liamg/raft/buffer/buffer.go:139.2,139.33 1 1
gitlab.com/liamg/raft/buffer/buffer.go:143.2,143.86 1 1
gitlab.com/liamg/raft/buffer/buffer.go:135.33,137.3 1 1
gitlab.com/liamg/raft/buffer/buffer.go:139.33,141.3 1 1
gitlab.com/liamg/raft/buffer/buffer.go:146.60,147.31 1 1
gitlab.com/liamg/raft/buffer/buffer.go:151.2,151.33 1 1
gitlab.com/liamg/raft/buffer/buffer.go:155.2,156.23 2 1
gitlab.com/liamg/raft/buffer/buffer.go:147.31,150.3 2 1
gitlab.com/liamg/raft/buffer/buffer.go:151.33,154.3 2 1
gitlab.com/liamg/raft/buffer/buffer.go:159.48,161.80 2 1
gitlab.com/liamg/raft/buffer/buffer.go:166.2,166.14 1 1
gitlab.com/liamg/raft/buffer/buffer.go:161.80,162.38 1 1
gitlab.com/liamg/raft/buffer/buffer.go:162.38,164.4 1 1
gitlab.com/liamg/raft/buffer/buffer.go:171.31,172.48 1 1
gitlab.com/liamg/raft/buffer/buffer.go:175.2,175.26 1 1
gitlab.com/liamg/raft/buffer/buffer.go:172.48,174.3 1 1
gitlab.com/liamg/raft/buffer/buffer.go:178.63,183.2 2 1
gitlab.com/liamg/raft/buffer/cell.go:20.21,22.2 1 1
gitlab.com/liamg/raft/buffer/cell.go:24.35,27.2 2 1
gitlab.com/liamg/raft/buffer/line.go:8.21,13.2 1 1
gitlab.com/liamg/raft/buffer/line.go:15.44,17.2 1 1
gitlab.com/liamg/raft/buffer/line.go:19.35,21.34 2 1
gitlab.com/liamg/raft/buffer/line.go:24.2,24.22 1 1
gitlab.com/liamg/raft/buffer/line.go:21.34,23.3 1 1

View File

@ -1,5 +0,0 @@
package buffer
type Cursor struct {
// holds current attr data
}

View File

@ -1,13 +1,25 @@
package buffer
type line struct {
type Line struct {
wrapped bool // whether line was wrapped onto from the previous one
cells []Cell
}
func newLine() line {
return line{
func newLine() Line {
return Line{
wrapped: false,
cells: []Cell{},
}
}
func (line *Line) setWrapped(wrapped bool) {
line.wrapped = wrapped
}
func (line *Line) String() string {
runes := []rune{}
for _, cell := range line.cells {
runes = append(runes, cell.r)
}
return string(runes)
}

29
buffer/line_test.go Normal file
View File

@ -0,0 +1,29 @@
package buffer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLine(t *testing.T) {
line := newLine()
line.cells = []Cell{
{r: 'h'},
{r: 'e'},
{r: 'l'},
{r: 'l'},
{r: 'o'},
}
assert.Equal(t, "hello", line.String())
assert.False(t, line.wrapped)
line.setWrapped(true)
assert.True(t, line.wrapped)
line.setWrapped(false)
assert.False(t, line.wrapped)
}

View File

@ -1,4 +0,0 @@
package buffer
type View struct {
}