Changed project layout and updated readme
This commit is contained in:
90
pkg/buffer/buffer.go
Executable file
90
pkg/buffer/buffer.go
Executable 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
103
pkg/buffer/cursor.go
Executable 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
193
pkg/buffer/highlighter.go
Executable 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
23
pkg/buffer/language.go
Executable 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
358
pkg/buffer/rope.go
Executable 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
148
pkg/buffer/rope_test.go
Normal 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
23
pkg/buffer/util.go
Normal 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))
|
||||
}
|
Reference in New Issue
Block a user