From 002d617630608ce1d2f63e5aae486ac27a7d106b Mon Sep 17 00:00:00 2001 From: Liam Galvin Date: Mon, 6 Aug 2018 14:51:23 +0100 Subject: [PATCH] nice tested buffer package --- buffer/buffer.go | 184 ++++++++++++++++++++++++++++-------- buffer/buffer_test.go | 215 ++++++++++++++++++++++++++++++++++++++---- buffer/cell_test.go | 18 ++++ buffer/coverage.out | 58 ++++++++++++ buffer/cursor.go | 5 - buffer/line.go | 18 +++- buffer/line_test.go | 29 ++++++ buffer/view.go | 4 - 8 files changed, 463 insertions(+), 68 deletions(-) create mode 100644 buffer/cell_test.go create mode 100644 buffer/coverage.out delete mode 100644 buffer/cursor.go create mode 100644 buffer/line_test.go delete mode 100644 buffer/view.go diff --git a/buffer/buffer.go b/buffer/buffer.go index 56cd86e..23fecb1 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -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 } diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go index abd616c..722858c 100644 --- a/buffer/buffer_test.go +++ b/buffer/buffer_test.go @@ -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) } diff --git a/buffer/cell_test.go b/buffer/cell_test.go new file mode 100644 index 0000000..607a659 --- /dev/null +++ b/buffer/cell_test.go @@ -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) +} diff --git a/buffer/coverage.out b/buffer/coverage.out new file mode 100644 index 0000000..6f380af --- /dev/null +++ b/buffer/coverage.out @@ -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 diff --git a/buffer/cursor.go b/buffer/cursor.go deleted file mode 100644 index 27faa92..0000000 --- a/buffer/cursor.go +++ /dev/null @@ -1,5 +0,0 @@ -package buffer - -type Cursor struct { - // holds current attr data -} diff --git a/buffer/line.go b/buffer/line.go index 6eec4ee..08c1914 100644 --- a/buffer/line.go +++ b/buffer/line.go @@ -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) +} diff --git a/buffer/line_test.go b/buffer/line_test.go new file mode 100644 index 0000000..6e9fe91 --- /dev/null +++ b/buffer/line_test.go @@ -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) + +} diff --git a/buffer/view.go b/buffer/view.go deleted file mode 100644 index d0c050c..0000000 --- a/buffer/view.go +++ /dev/null @@ -1,4 +0,0 @@ -package buffer - -type View struct { -}