Changed project layout and updated readme

This commit is contained in:
Luke Wilson
2021-04-15 11:59:25 -05:00
parent 27b2f564eb
commit fe6970508d
25 changed files with 35 additions and 526 deletions

90
pkg/buffer/buffer.go Executable file
View File

@@ -0,0 +1,90 @@
package buffer
import (
"io"
)
// A Buffer is wrapper around any buffer data structure like ropes or a gap buffer
// that can be used for text editors. One way this interface helps is by making
// all API function parameters line and column indexes, so it is simple and easy
// to index and use like a text editor. All lines and columns start at zero, and
// all "end" ranges are inclusive.
//
// Any bounds out of range are panics! If you are unsure your position or range
// may be out of bounds, use ClampLineCol() or compare with Lines() or ColsInLine().
type Buffer interface {
// Line returns a slice of the data at the given line, including the ending line-
// delimiter. line starts from zero. Data returned may or may not be a copy: do not
// write to it.
Line(line int) []byte
// Returns a slice of the buffer from startLine, startCol, to endLine, endCol,
// inclusive bounds. The returned value may or may not be a copy of the data,
// so do not write to it.
Slice(startLine, startCol, endLine, endCol int) []byte
// Bytes returns all of the bytes in the buffer. This function is very likely
// to copy all of the data in the buffer. Use sparingly. Try using other methods,
// where possible.
Bytes() []byte
// Insert copies a byte slice (inserting it) into the position at line, col.
Insert(line, col int, value []byte)
// Remove deletes any characters between startLine, startCol, and endLine,
// endCol, inclusive bounds.
Remove(startLine, startCol, endLine, endCol int)
// Returns the number of occurrences of 'sequence' in the buffer, within the range
// of start line and col, to end line and col. [start, end) (exclusive end).
Count(startLine, startCol, endLine, endCol int, sequence []byte) int
// Len returns the number of bytes in the buffer.
Len() int
// Lines returns the number of lines in the buffer. If the buffer is empty,
// 1 is returned, because there is always at least one line. This function
// basically counts the number of newline ('\n') characters in a buffer.
Lines() int
// RunesInLine returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints in the line, not bytes. Includes the line delimiter
// in the count. If that line delimiter is CRLF ('\r\n'), then it adds two.
RunesInLineWithDelim(line int) int
// RunesInLine returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints in the line, not bytes. Excludes line delimiters.
RunesInLine(line int) int
// ClampLineCol is a utility function to clamp any provided line and col to
// only possible values within the buffer, pointing to runes. It first clamps
// the line, then clamps the column. The column is clamped between zero and
// the last rune before the line delimiter.
ClampLineCol(line, col int) (int, int)
// LineColToPos returns the index of the byte at line, col. If line is less than
// zero, or more than the number of available lines, the function will panic. If
// col is less than zero, the function will panic. If col is greater than the
// length of the line, the position of the last byte of the line is returned,
// instead.
LineColToPos(line, col int) int
// PosToLineCol converts a byte offset (position) of the buffer's bytes, into
// a line and column. Unless you are working with the Bytes() function, this
// is unlikely to be useful to you. Position will be clamped.
PosToLineCol(pos int) (int, int)
WriteTo(w io.Writer) (int64, error)
// RegisterCursor adds the Cursor to a slice which the Buffer uses to update
// each Cursor based on changes that occur in the Buffer. Various functions are
// called on the Cursor depending upon where the edits occurred and how it should
// modify the Cursor's position. Unregister a Cursor before deleting it from
// memory, or forgetting it, with UnregisterPosition.
RegisterCursor(cursor *Cursor)
// UnregisterCursor will remove the cursor from the list of watched Cursors.
// It is mandatory that a Cursor be unregistered before being freed from memory,
// or otherwise being forgotten.
UnregisterCursor(cursor *Cursor)
}

103
pkg/buffer/cursor.go Executable file
View File

@@ -0,0 +1,103 @@
package buffer
import "math"
// So why is the code for moving the cursor in the buffer package, and not in the
// TextEdit component? Well, it used to be, but it sucked that way. The cursor
// needs to have a reference to the buffer to know where lines end and how it can
// move. The buffer is the city, and the Cursor is the car.
type position struct {
line int
col int
}
// A Selection represents a region of the buffer to be selected for text editing
// purposes. It is asserted that the start position is less than the end position.
// The start and end are inclusive. If the EndCol of a Region is one more than the
// last column of a line, then it points to the line delimiter at the end of that
// line. It is understood that as a Region spans multiple lines, those connecting
// line-delimiters are included in the selection, as well.
type Region struct {
Start Cursor
End Cursor
}
func NewRegion(in *Buffer) Region {
return Region{
NewCursor(in),
NewCursor(in),
}
}
// A Cursor's functions emulate common cursor actions. To have a Cursor be
// automatically updated when the buffer has text prepended or appended -- one
// should register the Cursor with the Buffer's function `RegisterCursor()`
// which makes the Cursor "anchored" to the Buffer.
type Cursor struct {
buffer *Buffer
prevCol int
position
}
func NewCursor(in *Buffer) Cursor {
return Cursor{
buffer: in,
}
}
func (c Cursor) Left() Cursor {
if c.col == 0 && c.line != 0 { // If we are at the beginning of the current line...
// Go to the end of the above line
c.line--
c.col = (*c.buffer).RunesInLine(c.line)
} else {
c.col = Max(c.col-1, 0)
}
return c
}
func (c Cursor) Right() Cursor {
// If we are at the end of the current line,
// and not at the last line...
if c.col >= (*c.buffer).RunesInLine(c.line) && c.line < (*c.buffer).Lines()-1 {
c.line, c.col = (*c.buffer).ClampLineCol(c.line+1, 0) // Go to beginning of line below
} else {
c.line, c.col = (*c.buffer).ClampLineCol(c.line, c.col+1)
}
return c
}
func (c Cursor) Up() Cursor {
if c.line == 0 { // If the cursor is at the first line...
c.line, c.col = 0, 0 // Go to beginning
} else {
c.line, c.col = (*c.buffer).ClampLineCol(c.line-1, c.col)
}
return c
}
func (c Cursor) Down() Cursor {
if c.line == (*c.buffer).Lines()-1 { // If the cursor is at the last line...
c.line, c.col = (*c.buffer).ClampLineCol(c.line, math.MaxInt32) // Go to end of current line
} else {
c.line, c.col = (*c.buffer).ClampLineCol(c.line+1, c.col)
}
return c
}
func (c Cursor) GetLineCol() (line, col int) {
return c.line, c.col
}
// SetLineCol sets the line and col of the Cursor to those provided. `line` is
// clamped within the range (0, lines in buffer). `col` is then clamped within
// the range (0, line length in runes).
func (c Cursor) SetLineCol(line, col int) Cursor {
c.line, c.col = (*c.buffer).ClampLineCol(line, col)
return c
}
func (c Cursor) Eq(other Cursor) bool {
return c.buffer == other.buffer && c.line == other.line && c.col == other.col
}

193
pkg/buffer/highlighter.go Executable file
View File

@@ -0,0 +1,193 @@
package buffer
import (
"regexp"
"sort"
"github.com/gdamore/tcell/v2"
)
type Colorscheme map[Syntax]tcell.Style
// Gets the tcell.Style from the Colorscheme map for the given Syntax.
// If the Syntax cannot be found in the map, either the `Default` Syntax
// is used, or `tcell.DefaultStyle` is returned if the Default is not assigned.
func (c *Colorscheme) GetStyle(s Syntax) tcell.Style {
if c != nil {
if val, ok := (*c)[s]; ok {
return val // Try to return the requested value
} else if s != Default {
if val, ok := (*c)[Default]; ok {
return val // Use default colorscheme value, instead
}
}
}
return tcell.StyleDefault // No value for Default; use default style.
}
type RegexpRegion struct {
Start *regexp.Regexp
End *regexp.Regexp // Should be "$" by default
Skip *regexp.Regexp // Optional
Error *regexp.Regexp // Optional
Specials []*regexp.Regexp // Optional (nil or zero len)
}
type Match struct {
Col int
EndLine int // Inclusive
EndCol int // Inclusive
Syntax Syntax
}
// ByCol implements sort.Interface for []Match based on the Col field.
type ByCol []Match
func (c ByCol) Len() int { return len(c) }
func (c ByCol) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ByCol) Less(i, j int) bool { return c[i].Col < c[j].Col }
// A Highlighter can answer how to color any part of a provided Buffer. It does so
// by applying regular expressions over a region of the buffer.
type Highlighter struct {
Buffer Buffer
Language *Language
Colorscheme *Colorscheme
lineMatches [][]Match
}
func NewHighlighter(buffer Buffer, lang *Language, colorscheme *Colorscheme) *Highlighter {
return &Highlighter{
buffer,
lang,
colorscheme,
make([][]Match, buffer.Lines()),
}
}
func (h *Highlighter) expandToBufferLines() {
if lines := h.Buffer.Lines(); len(h.lineMatches) < lines {
h.lineMatches = append(h.lineMatches, make([][]Match, lines-len(h.lineMatches))...) // Extend from Slice Tricks
}
}
// UpdateLines forces the highlighting matches for lines between startLine to
// endLine, inclusively, to be updated. It is more efficient to mark lines as
// invalidated when changes occur and call UpdateInvalidatedLines(...).
func (h *Highlighter) UpdateLines(startLine, endLine int) {
h.expandToBufferLines()
h.updateLines(startLine, endLine)
}
func (h *Highlighter) updateLines(startLine, endLine int) {
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
if h.lineMatches[i] != nil {
h.lineMatches[i] = h.lineMatches[i][:0] // Shrink slice to zero (hopefully save allocs)
}
}
// If the rule k does not have an End, then it can be optimized that we search from the start
// of view until the end of view. For any k that has an End, we search for ends from start
// of view, backtracking when one is found, to fulfill a multiline highlight.
endLine, endCol := h.Buffer.ClampLineCol(endLine, (h.Buffer).RunesInLineWithDelim(endLine)-1)
startPos := h.Buffer.LineColToPos(startLine, 0)
bytes := h.Buffer.Slice(startLine, 0, endLine, endCol)
for k, v := range h.Language.Rules {
var indexes [][]int // [][2]int
if k.End != nil && k.End.String() != "$" { // If this range might be a multiline range...
endIndexes := k.End.FindAllIndex(bytes, -1) // Attempt to find every ending match
startIndexes := k.Start.FindAllIndex(bytes, -1) // Attempt to find every starting match
// ...
_ = endIndexes
_ = startIndexes
} else { // A standard single-line match
indexes = k.Start.FindAllIndex(bytes, -1) // Attempt to find the start match
}
for i := range indexes {
startLine, startCol := h.Buffer.PosToLineCol(indexes[i][0] + startPos)
endLine, endCol := h.Buffer.PosToLineCol(indexes[i][1] - 1 + startPos)
match := Match{startCol, endLine, endCol, v}
h.lineMatches[startLine] = append(h.lineMatches[startLine], match) // Unsorted
}
}
h.validateLines(startLine, endLine) // Marks any "unvalidated" or nil lines as valued
}
// UpdateInvalidatedLines only updates the highlighting for lines that are invalidated
// between lines startLine and endLine, inclusively.
func (h *Highlighter) UpdateInvalidatedLines(startLine, endLine int) {
h.expandToBufferLines()
// Move startLine to first line with invalidated changes
for startLine <= endLine && startLine < len(h.lineMatches)-1 {
if h.lineMatches[startLine] == nil {
break
}
startLine++
}
// Keep endLine clamped
if endLine >= len(h.lineMatches) {
endLine = len(h.lineMatches) - 1
}
// Move endLine back to first line at or before endLine with invalidated changes
for endLine >= startLine && endLine > 0 {
if h.lineMatches[endLine] == nil {
break
}
endLine--
}
if startLine > endLine {
return // Do nothing; no invalidated lines
}
h.updateLines(startLine, endLine)
}
func (h *Highlighter) HasInvalidatedLines(startLine, endLine int) bool {
h.expandToBufferLines()
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
if h.lineMatches[i] == nil {
return true
}
}
return false
}
func (h *Highlighter) validateLines(startLine, endLine int) {
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
if h.lineMatches[i] == nil {
h.lineMatches[i] = make([]Match, 0)
}
}
}
func (h *Highlighter) InvalidateLines(startLine, endLine int) {
h.expandToBufferLines()
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
h.lineMatches[i] = nil
}
}
func (h *Highlighter) GetLineMatches(line int) []Match {
if line < 0 || line >= len(h.lineMatches) {
return nil
}
data := h.lineMatches[line]
sort.Sort(ByCol(data))
return data
}
func (h *Highlighter) GetStyle(match Match) tcell.Style {
return h.Colorscheme.GetStyle(match.Syntax)
}

23
pkg/buffer/language.go Executable file
View File

@@ -0,0 +1,23 @@
package buffer
type Syntax uint8
const (
Default Syntax = iota
Column // Not necessarily a Syntax; useful for Colorscheming editor column
Keyword
String
Special
Type
Number
Builtin
Comment
DocComment
)
type Language struct {
Name string
Filetypes []string // .go, .c, etc.
Rules map[*RegexpRegion]Syntax
// TODO: add other language details
}

358
pkg/buffer/rope.go Executable file
View File

@@ -0,0 +1,358 @@
package buffer
import (
"io"
"unicode/utf8"
ropes "github.com/zyedidia/rope"
)
type RopeBuffer struct {
rope *ropes.Node
anchors []*Cursor
}
func NewRopeBuffer(contents []byte) *RopeBuffer {
return &RopeBuffer{
ropes.New(contents),
nil,
}
}
// LineColToPos returns the index of the byte at line, col. If line is less than
// zero, or more than the number of available lines, the function will panic. If
// col is less than zero, the function will panic. If col is greater than the
// length of the line, the position of the last byte of the line is returned,
// instead.
func (b *RopeBuffer) LineColToPos(line, col int) int {
pos := b.getLineStartPos(line)
// Have to do this algorithm for safety. If this function was declared to panic
// or index out of bounds memory, if col > the given line length, it would be
// more efficient and simpler. But unfortunately, I believe it is necessary.
if col > 0 {
_, r := b.rope.SplitAt(pos)
l, _ := r.SplitAt(b.rope.Len() - pos)
l.EachLeaf(func(n *ropes.Node) bool {
data := n.Value() // Reference; not a copy.
for _, r := range string(data) {
if col == 0 || r == '\n' {
return true // Found the position of the column
}
pos++
col--
}
return false // Have not gotten to the appropriate position, yet
})
}
return pos
}
// Line returns a slice of the data at the given line, including the ending line-
// delimiter. line starts from zero. Data returned may or may not be a copy: do not
// write it.
func (b *RopeBuffer) Line(line int) []byte {
pos := b.getLineStartPos(line)
bytes := 0
_, r := b.rope.SplitAt(pos)
l, _ := r.SplitAt(b.rope.Len() - pos)
var isCRLF bool // true if the last byte was '\r'
l.EachLeaf(func(n *ropes.Node) bool {
data := n.Value() // Reference; not a copy.
for i, r := range string(data) {
if r == '\r' {
isCRLF = true
} else if r == '\n' {
bytes += i // Add bytes before i
if isCRLF {
bytes += 2 // Add the CRLF bytes
} else {
bytes += 1 // Add LF byte
}
return true // Read (past-tense) the whole line
} else {
isCRLF = false
}
}
bytes += len(data)
return false // Have not read the whole line, yet
})
return b.rope.Slice(pos, pos+bytes) // NOTE: may be faster to do it ourselves
}
// Returns a slice of the buffer from startLine, startCol, to endLine, endCol,
// inclusive bounds. The returned value may or may not be a copy of the data,
// so do not write to it.
func (b *RopeBuffer) Slice(startLine, startCol, endLine, endCol int) []byte {
endPos := b.LineColToPos(endLine, endCol)
if length := b.rope.Len(); endPos >= length {
endPos = length - 1
}
return b.rope.Slice(b.LineColToPos(startLine, startCol), endPos+1)
}
// Bytes returns all of the bytes in the buffer. This function is very likely
// to copy all of the data in the buffer. Use sparingly. Try using other methods,
// where possible.
func (b *RopeBuffer) Bytes() []byte {
return b.rope.Value()
}
// Insert copies a byte slice (inserting it) into the position at line, col.
func (b *RopeBuffer) Insert(line, col int, value []byte) {
b.rope.Insert(b.LineColToPos(line, col), value)
b.shiftAnchors(line, col, utf8.RuneCount(value))
}
// Remove deletes any characters between startLine, startCol, and endLine,
// endCol, inclusive bounds.
func (b *RopeBuffer) Remove(startLine, startCol, endLine, endCol int) {
start := b.LineColToPos(startLine, startCol)
end := b.LineColToPos(endLine, endCol) + 1
if len := b.rope.Len(); end >= len {
end = len
if start > end {
return
}
}
b.rope.Remove(start, end)
// Shift anchors within the range
b.shiftAnchorsRemovedRange(start, startLine, startCol, endLine, endCol)
// Shift anchors after the range
b.shiftAnchors(endLine, endCol+1, start-end-1)
}
// Returns the number of occurrences of 'sequence' in the buffer, within the range
// of start line and col, to end line and col. End is exclusive.
func (b *RopeBuffer) Count(startLine, startCol, endLine, endCol int, sequence []byte) int {
startPos := b.LineColToPos(startLine, startCol)
endPos := b.LineColToPos(endLine, endCol)
return b.rope.Count(startPos, endPos, sequence)
}
// Len returns the number of bytes in the buffer.
func (b *RopeBuffer) Len() int {
return b.rope.Len()
}
// Lines returns the number of lines in the buffer. If the buffer is empty,
// 1 is returned, because there is always at least one line. This function
// basically counts the number of newline ('\n') characters in a buffer.
func (b *RopeBuffer) Lines() int {
rope := b.rope
return rope.Count(0, rope.Len(), []byte{'\n'}) + 1
}
// getLineStartPos returns the first byte index of the given line (starting from zero).
// The returned index can be equal to the length of the buffer, not pointing to any byte,
// which means the byte is on the last, and empty, line of the buffer. If line is greater
// than or equal to the number of lines in the buffer, a panic is issued.
func (b *RopeBuffer) getLineStartPos(line int) int {
var pos int
if line > 0 {
b.rope.IndexAllFunc(0, b.rope.Len(), []byte{'\n'}, func(idx int) bool {
line--
pos = idx + 1 // idx+1 = start of line after delimiter
return line <= 0 // If pos is now the start of the line we're searching for
})
}
if line > 0 { // If there aren't enough lines to reach line...
panic("getLineStartPos: not enough lines in buffer to reach position")
}
return pos
}
// RunesInLineWithDelim returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints in the line, not bytes. Includes the line delimiter
// in the count. If that line delimiter is CRLF ('\r\n'), then it adds two.
func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
linePos := b.getLineStartPos(line)
ropeLen := b.rope.Len()
if linePos >= ropeLen {
return 0
}
var count int
_, r := b.rope.SplitAt(linePos)
l, _ := r.SplitAt(ropeLen - linePos)
l.EachLeaf(func(n *ropes.Node) bool {
data := n.Value() // Reference; not a copy.
for _, r := range string(data) {
count++ // Before: we count the line delimiter
if r == '\n' {
return true // Read (past-tense) the whole line
}
}
return false // Have not read the whole line, yet
})
return count
}
// RunesInLine returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints in the line, not bytes. Excludes line delimiters.
func (b *RopeBuffer) RunesInLine(line int) int {
linePos := b.getLineStartPos(line)
ropeLen := b.rope.Len()
if linePos >= ropeLen {
return 0
}
var count int
_, r := b.rope.SplitAt(linePos)
l, _ := r.SplitAt(ropeLen - linePos)
var isCRLF bool
l.EachLeaf(func(n *ropes.Node) bool {
data := n.Value() // Reference; not a copy.
for _, r := range string(data) {
if r == '\r' {
isCRLF = true
} else if r == '\n' {
return true // Read (past-tense) the whole line
} else {
if isCRLF {
isCRLF = false
count++ // Add the '\r' we previously thought was part of the delim.
}
}
count++
}
return false // Have not read the whole line, yet
})
return count
}
// ClampLineCol is a utility function to clamp any provided line and col to
// only possible values within the buffer, pointing to runes. It first clamps
// the line, then clamps the column. The column is clamped between zero and
// the last rune before the line delimiter.
func (b *RopeBuffer) ClampLineCol(line, col int) (int, int) {
if line < 0 {
line = 0
} else if lines := b.Lines() - 1; line > lines {
line = lines
}
if col < 0 {
col = 0
} else if runes := b.RunesInLine(line); col > runes {
col = runes
}
return line, col
}
// PosToLineCol converts a byte offset (position) of the buffer's bytes, into
// a line and column. Unless you are working with the Bytes() function, this
// is unlikely to be useful to you. Position will be clamped.
func (b *RopeBuffer) PosToLineCol(pos int) (int, int) {
var line, col int
var wasAtNewline bool
if pos <= 0 {
return line, col
}
b.rope.EachLeaf(func(n *ropes.Node) bool {
data := n.Value()
var i int
for i < len(data) {
if wasAtNewline { // Start of line
if data[i] != '\n' { // If the start of this line does not happen to be a delim...
wasAtNewline = false // Say we weren't previously at a delimiter
}
line, col = line+1, 0
} else if data[i] == '\n' { // End of line
wasAtNewline = true
col++
} else {
col++
}
_, size := utf8.DecodeRune(data[i:])
i += size
pos -= size
if pos < 0 {
return true
}
}
return false
})
return line, col
}
func (b *RopeBuffer) WriteTo(w io.Writer) (int64, error) {
return b.rope.WriteTo(w)
}
// Currently meant for the Remove function: imagine if the removed region passes through a Cursor position.
// We want to shift the cursor to the start of the region, based upon where the cursor position is.
func (b *RopeBuffer) shiftAnchorsRemovedRange(startPos, startLine, startCol, endLine, endCol int) {
for _, v := range b.anchors {
cursorLine, cursorCol := v.GetLineCol()
if cursorLine >= startLine && cursorLine <= endLine {
// If the anchor is not within the start or end columns
if (cursorLine == startLine && cursorCol < startCol) || (cursorLine == endLine && cursorCol > endCol) {
continue
}
cursorPos := b.LineColToPos(cursorLine, cursorCol)
v.line, v.col = b.PosToLineCol(cursorPos + (startPos - cursorPos))
}
}
}
func (b *RopeBuffer) shiftAnchors(insertLine, insertCol, runeCount int) {
for _, v := range b.anchors {
cursorLine, cursorCol := v.GetLineCol()
if insertLine < cursorLine || (insertLine == cursorLine && insertCol <= cursorCol) {
v.line, v.col = b.PosToLineCol(b.LineColToPos(cursorLine, cursorCol) + runeCount)
}
}
}
// RegisterCursor adds the Cursor to a slice which the Buffer uses to update
// each Cursor based on changes that occur in the Buffer. Various functions are
// called on the Cursor depending upon where the edits occurred and how it should
// modify the Cursor's position. Unregister a Cursor before deleting it from
// memory, or forgetting it, with UnregisterPosition.
func (b *RopeBuffer) RegisterCursor(cursor *Cursor) {
if cursor == nil {
return
}
b.anchors = append(b.anchors, cursor)
}
// UnregisterCursor will remove the cursor from the list of watched Cursors.
// It is mandatory that a Cursor be unregistered before being freed from memory,
// or otherwise being forgotten.
func (b *RopeBuffer) UnregisterCursor(cursor *Cursor) {
for i, v := range b.anchors {
if cursor == v {
// Delete item at i without preserving order
b.anchors[i] = b.anchors[len(b.anchors)-1]
b.anchors[len(b.anchors)-1] = nil
b.anchors = b.anchors[:len(b.anchors)-1]
}
}
}

148
pkg/buffer/rope_test.go Normal file
View File

@@ -0,0 +1,148 @@
package buffer
import "testing"
func TestRopePosToLineCol(t *testing.T) {
var buf Buffer = NewRopeBuffer([]byte("line0\nline1\n\nline3\n"))
//line0
//line1
//
//line3
//
startLine, startCol := buf.PosToLineCol(0)
if startLine != 0 {
t.Errorf("Expected startLine == 0, got %v", startLine)
}
if startCol != 0 {
t.Errorf("Expected startCol == 0, got %v", startCol)
}
endPos := buf.Len() - 1
endLine, endCol := buf.PosToLineCol(endPos)
t.Logf("endPos = %v", endPos)
if endLine != 3 {
t.Errorf("Expected endLine == 3, got %v", endLine)
}
if endCol != 5 {
t.Errorf("Expected endCol == 5, got %v", endCol)
}
line1Pos := 11 // Byte index of the delim separating line1 and line 2
line1Line, line1Col := buf.PosToLineCol(line1Pos)
if line1Line != 1 {
t.Errorf("Expected line1Line == 1, got %v", line1Line)
}
if line1Col != 5 {
t.Errorf("Expected line1Col == 5, got %v", line1Col)
}
}
func TestRopeInserting(t *testing.T) {
var buf Buffer = NewRopeBuffer([]byte("some"))
buf.Insert(0, 4, []byte(" text\n")) // Insert " text" after "some"
buf.Insert(0, 0, []byte("with\n\t"))
//with
// some text
//
buf.Remove(0, 4, 1, 5) // Delete from line 0, col 4, to line 1, col 6 "\n\tsome "
if str := string(buf.Bytes()); str != "withtext\n" {
t.Errorf("string does not match \"withtext\", got %#v", str)
}
}
func TestRopeBounds(t *testing.T) {
var buf Buffer = NewRopeBuffer([]byte("this\nis (は)\n\tsome\ntext\n"))
//this
//is (は)
// some
//text
//
if buf.Lines() != 5 {
t.Errorf("Expected buf.Lines() == 5")
}
if len := buf.RunesInLine(1); len != 6 { // "is" in English and in japanese
t.Errorf("Expected 6 runes in line 2, found %v", len)
}
if len := buf.RunesInLineWithDelim(4); len != 0 {
t.Errorf("Expected 0 runes in line 5, found %v", len)
}
line, col := buf.ClampLineCol(15, 5) // Should become last line, first column
if line != 4 && col != 0 {
t.Errorf("Expected to clamp line col to 4,0 got %v,%v", line, col)
}
line, col = buf.ClampLineCol(4, -1)
if line != 4 && col != 0 {
t.Errorf("Expected to clamp line col to 4,0 got %v,%v", line, col)
}
line, col = buf.ClampLineCol(2, 5) // Should be third line, pointing at the newline char
if line != 2 && col != 5 {
t.Errorf("Expected to clamp line, col to 2,5 got %v,%v", line, col)
}
if line := string(buf.Line(2)); line != "\tsome\n" {
t.Errorf("Expected line 3 to equal \"\\tsome\", got %#v", line)
}
if line := string(buf.Line(4)); line != "" {
t.Errorf("Got %#v", line)
}
}
func TestRopeCount(t *testing.T) {
var buf Buffer = NewRopeBuffer([]byte("\t\tlot of\n\ttabs"))
tabsAtOf := buf.Count(0, 0, 0, 7, []byte{'\t'})
if tabsAtOf != 2 {
t.Errorf("Expected 2 tabs before 'of', got %#v", tabsAtOf)
}
tabs := buf.Count(0, 0, 0, 0, []byte{'\t'})
if tabs != 0 {
t.Errorf("Expected no tabs at column zero, got %v", tabs)
}
}
func TestRopeSlice(t *testing.T) {
var buf Buffer = NewRopeBuffer([]byte("abc\ndef\n"))
wholeSlice := buf.Slice(0, 0, 2, 0) // Position points to after the newline char
if string(wholeSlice) != "abc\ndef\n" {
t.Errorf("Whole slice was not equal, got \"%s\"", wholeSlice)
}
secondLine := buf.Slice(1, 0, 1, 3)
if string(secondLine) != "def\n" {
t.Errorf("Second line and slice were not equal, got \"%s\"", secondLine)
}
}
func TestRopeAnchors(t *testing.T) {
var buf Buffer = NewRopeBuffer([]byte("abc\ndef\nghi"))
myCursor := Cursor{buffer: &buf, position: position{1, 1}} // Points to 'e' on second line
buf.RegisterCursor(&myCursor) // Now the buffer will move the cursor based on changes to contents
buf.Remove(1, 0, 1, 3) // Remove whole second line ('d' to '\n')
if line, col := myCursor.GetLineCol(); line != 1 && col != 0 {
t.Errorf("Expected cursor line,col: %d,%d ; got %d,%d", 1, 0, line, col)
}
buf.Insert(0, 0, []byte("def\n")) // "def\nabc\nghi"
if line, col := myCursor.GetLineCol(); line != 2 && col != 0 {
t.Errorf("Expected cursor line,col: %d,%d ; got %d,%d", 2, 0, line, col)
}
buf.UnregisterCursor(&myCursor)
}

23
pkg/buffer/util.go Normal file
View File

@@ -0,0 +1,23 @@
package buffer
// Max returns the larger integer.
func Max(a, b int) int {
if a > b {
return a
}
return b
}
// Min returns the smaller integer.
func Min(a, b int) int {
if a < b {
return a
}
return b
}
// Clamp keeps `v` within `a` and `b` numerically. `a` must be smaller than `b`.
// Returns clamped `v`.
func Clamp(v, a, b int) int {
return Max(a, Min(v, b))
}