Add editor packages
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package line
|
||||
|
||||
import "strings"
|
||||
|
||||
func (l *Line) Append(s string) {
|
||||
l.data = l.data + s
|
||||
}
|
||||
|
||||
func (l *Line) DeleteRune(index int) (deleted rune) {
|
||||
if index < 0 || index >= len(l.data) {
|
||||
return
|
||||
}
|
||||
|
||||
var str strings.Builder
|
||||
for i, r := range l.data {
|
||||
if i == index {
|
||||
deleted = r
|
||||
continue
|
||||
}
|
||||
str.WriteRune(r)
|
||||
}
|
||||
|
||||
l.data = str.String()
|
||||
|
||||
return deleted
|
||||
}
|
||||
|
||||
func (l *Line) Insert(index int, nr rune) {
|
||||
if index < 0 || index > len(l.data) {
|
||||
return
|
||||
}
|
||||
|
||||
var str strings.Builder
|
||||
switch {
|
||||
|
||||
// Handle empty lines and the end of the line.
|
||||
case index == len(l.data):
|
||||
str.WriteString(l.data)
|
||||
str.WriteRune(nr)
|
||||
|
||||
default:
|
||||
for i, r := range l.data {
|
||||
if i == index {
|
||||
str.WriteRune(nr)
|
||||
}
|
||||
str.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
l.data = str.String()
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package line
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.chimeric.al/dwrz/wen/internal/editor/buffer/glyph"
|
||||
)
|
||||
|
||||
type Line struct {
|
||||
data string
|
||||
}
|
||||
|
||||
func New(s string) *Line {
|
||||
return &Line{data: s}
|
||||
}
|
||||
|
||||
func (l *Line) DecodeRune(index int) (r rune, size int) {
|
||||
return utf8.DecodeRuneInString(l.data[index:])
|
||||
}
|
||||
|
||||
// FindGlyphIndex returns the byte index and display column (g) for a target
|
||||
// display column. If target falls inside a multi-column rune, it returns the
|
||||
// start of that rune. If target is past the end of the line, it returns the
|
||||
// byte length and total display width of the line.
|
||||
func (l *Line) FindGlyphIndex(target int) (index, g int) {
|
||||
var li, lg int
|
||||
for i, r := range l.data {
|
||||
// We've reached the target.
|
||||
if g == target {
|
||||
|
||||
return i, g
|
||||
}
|
||||
// We've gone too far.
|
||||
// Return the preceding index and glyph.
|
||||
if g > target {
|
||||
return li, lg
|
||||
}
|
||||
|
||||
// Otherwise, we haven't reached the target.
|
||||
// Save this index and glyph.
|
||||
li = i
|
||||
lg = g
|
||||
|
||||
// Then increment the glyph.
|
||||
g += glyph.Width(r)
|
||||
}
|
||||
// Target falls in-between the last glyph and the end of the line.
|
||||
// Use the last glyph.
|
||||
if target >= lg && target < g {
|
||||
return li, lg
|
||||
}
|
||||
|
||||
// We weren't able to find the glyph.
|
||||
// Return the last possible index and glyph.
|
||||
return len(l.data), g
|
||||
}
|
||||
|
||||
// Length returns the byte length of the line's UTF-8 string.
|
||||
func (l *Line) Length() int {
|
||||
return len(l.data)
|
||||
}
|
||||
|
||||
// Render returns the visible substring starting at display column offset and
|
||||
// fitting within width display columns. Tabs are expanded to 8 spaces. If
|
||||
// offset splits a multi-column rune, the remaining columns of that rune are
|
||||
// padded with spaces.
|
||||
func (l *Line) Render(offset, width int) string {
|
||||
if offset < 0 || width <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
text = strings.ReplaceAll(l.data, "\t", " ")
|
||||
str strings.Builder
|
||||
)
|
||||
for _, r := range text {
|
||||
if r == utf8.RuneError {
|
||||
continue
|
||||
}
|
||||
|
||||
rw := glyph.Width(r)
|
||||
if offset > 0 {
|
||||
offset -= rw
|
||||
if offset < 0 {
|
||||
// The offset split a wide rune.
|
||||
// Pad the remaining column.
|
||||
pad := -offset
|
||||
if pad > width {
|
||||
pad = width
|
||||
}
|
||||
for i := 0; i < pad; i++ {
|
||||
str.WriteRune(' ')
|
||||
}
|
||||
width -= pad
|
||||
offset = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if width-rw < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
str.WriteRune(r)
|
||||
width -= rw
|
||||
}
|
||||
|
||||
return str.String()
|
||||
}
|
||||
|
||||
func (l *Line) Runes() []rune {
|
||||
return []rune(l.data)
|
||||
}
|
||||
|
||||
func (l *Line) String() string {
|
||||
return l.data
|
||||
}
|
||||
|
||||
// Width returns the terminal display column width of the line.
|
||||
func (l *Line) Width() (count int) {
|
||||
for _, r := range l.data {
|
||||
rw := glyph.Width(r)
|
||||
if r == utf8.RuneError {
|
||||
continue
|
||||
}
|
||||
count += rw
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package line
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type GlyphIndexTest struct {
|
||||
data string
|
||||
glyph int
|
||||
index int
|
||||
target int
|
||||
}
|
||||
|
||||
func TestGlyphIndex(t *testing.T) {
|
||||
var tests = []GlyphIndexTest{
|
||||
{
|
||||
data: "",
|
||||
index: 0,
|
||||
glyph: 0,
|
||||
target: 0,
|
||||
},
|
||||
{
|
||||
data: "\n",
|
||||
index: 0,
|
||||
glyph: 0,
|
||||
target: 0,
|
||||
},
|
||||
{
|
||||
data: "\t🤔",
|
||||
index: 0,
|
||||
glyph: 0,
|
||||
target: 0,
|
||||
},
|
||||
{
|
||||
data: "\t🤔",
|
||||
index: 0,
|
||||
glyph: 0,
|
||||
target: 1,
|
||||
},
|
||||
{
|
||||
data: "\t🤔",
|
||||
index: 1,
|
||||
glyph: 8,
|
||||
target: 8,
|
||||
},
|
||||
{
|
||||
data: "\t🤔**",
|
||||
index: 5, // \t = 0, 🤔 = 1-4, * = 5
|
||||
glyph: 10, // \t = 0, 🤔 = 8, * = 10.
|
||||
target: 10, // *
|
||||
},
|
||||
{
|
||||
data: "\t🤔*",
|
||||
index: 6, // \t = 0, 🤔 = 1-4, * = 5
|
||||
glyph: 11, // \t = 0, 🤔 = 8, * = 10.
|
||||
target: 11, // *
|
||||
},
|
||||
|
||||
{
|
||||
data: "你是谁?",
|
||||
index: 3, // 你 size is 3 bytes.
|
||||
glyph: 2, // 你 width is 2.
|
||||
target: 2, // 是
|
||||
},
|
||||
{
|
||||
data: "磨杵成针",
|
||||
index: 12, // 针
|
||||
glyph: 8, // 针
|
||||
target: 8, // Doesn't exist; return end of last rune.
|
||||
},
|
||||
{
|
||||
data: "你好!",
|
||||
index: 6, // !
|
||||
glyph: 4, // !
|
||||
target: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
i, g := New(test.data).FindGlyphIndex(test.target)
|
||||
if i != test.index {
|
||||
t.Errorf(
|
||||
"#%d: expected index %d, but got %d",
|
||||
n, test.index, i,
|
||||
)
|
||||
}
|
||||
if g != test.glyph {
|
||||
t.Errorf(
|
||||
"#%d: expected glyph %d, but got %d",
|
||||
n, test.glyph, g,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WidthTest struct {
|
||||
data string
|
||||
expected int
|
||||
}
|
||||
|
||||
func TestWidth(t *testing.T) {
|
||||
var tests = []WidthTest{
|
||||
{
|
||||
data: "",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
data: "\n",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
data: "\t",
|
||||
expected: 8,
|
||||
},
|
||||
{
|
||||
data: "Hello, World!",
|
||||
expected: 13,
|
||||
},
|
||||
{
|
||||
data: "Hello, 世界!",
|
||||
expected: 12,
|
||||
},
|
||||
{
|
||||
data: "💻💩",
|
||||
expected: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if w := New(test.data).Width(); w != test.expected {
|
||||
t.Errorf(
|
||||
"expected width %d, but got %d",
|
||||
test.expected, w,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RenderTest struct {
|
||||
data string
|
||||
offset int
|
||||
width int
|
||||
expected string
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
var tests = []RenderTest{
|
||||
{
|
||||
data: "",
|
||||
offset: 0,
|
||||
width: 10,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
data: "hello",
|
||||
offset: 0,
|
||||
width: 10,
|
||||
expected: "hello",
|
||||
},
|
||||
{
|
||||
data: "hello",
|
||||
offset: 2,
|
||||
width: 2,
|
||||
expected: "ll",
|
||||
},
|
||||
{
|
||||
data: "\ta",
|
||||
offset: 0,
|
||||
width: 9,
|
||||
expected: " a",
|
||||
},
|
||||
{
|
||||
data: "世界",
|
||||
offset: 0,
|
||||
width: 3,
|
||||
expected: "世",
|
||||
},
|
||||
{
|
||||
data: "世界",
|
||||
offset: 1,
|
||||
width: 3,
|
||||
expected: " 界",
|
||||
},
|
||||
{
|
||||
data: "hello",
|
||||
offset: -1,
|
||||
width: 5,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
data: "hello",
|
||||
offset: 0,
|
||||
width: 0,
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
got := New(test.data).Render(test.offset, test.width)
|
||||
if got != test.expected {
|
||||
t.Errorf(
|
||||
"#%d: expected %q, but got %q",
|
||||
n, test.expected, got,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type InsertTest struct {
|
||||
data string
|
||||
index int
|
||||
r rune
|
||||
expected string
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
var tests = []InsertTest{
|
||||
{
|
||||
data: "",
|
||||
index: 0,
|
||||
r: 'a',
|
||||
expected: "a",
|
||||
},
|
||||
{
|
||||
data: "bc",
|
||||
index: 0,
|
||||
r: 'a',
|
||||
expected: "abc",
|
||||
},
|
||||
{
|
||||
data: "ac",
|
||||
index: 1,
|
||||
r: 'b',
|
||||
expected: "abc",
|
||||
},
|
||||
{
|
||||
data: "ab",
|
||||
index: 2,
|
||||
r: 'c',
|
||||
expected: "abc",
|
||||
},
|
||||
{
|
||||
data: "世",
|
||||
index: 3,
|
||||
r: '界',
|
||||
expected: "世界",
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
l := New(test.data)
|
||||
l.Insert(test.index, test.r)
|
||||
if got := l.String(); got != test.expected {
|
||||
t.Errorf(
|
||||
"#%d: expected %q, but got %q",
|
||||
n, test.expected, got,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteRuneTest struct {
|
||||
data string
|
||||
index int
|
||||
deleted rune
|
||||
expected string
|
||||
}
|
||||
|
||||
func TestDeleteRune(t *testing.T) {
|
||||
var tests = []DeleteRuneTest{
|
||||
{
|
||||
data: "",
|
||||
index: 0,
|
||||
deleted: 0,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
data: "abc",
|
||||
index: 0,
|
||||
deleted: 'a',
|
||||
expected: "bc",
|
||||
},
|
||||
{
|
||||
data: "abc",
|
||||
index: 1,
|
||||
deleted: 'b',
|
||||
expected: "ac",
|
||||
},
|
||||
{
|
||||
data: "世界",
|
||||
index: 0,
|
||||
deleted: '世',
|
||||
expected: "界",
|
||||
},
|
||||
{
|
||||
data: "世界",
|
||||
index: 3,
|
||||
deleted: '界',
|
||||
expected: "世",
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
l := New(test.data)
|
||||
r := l.DeleteRune(test.index)
|
||||
if r != test.deleted {
|
||||
t.Errorf(
|
||||
"#%d: expected deleted %q, but got %q",
|
||||
n, test.deleted, r,
|
||||
)
|
||||
}
|
||||
if got := l.String(); got != test.expected {
|
||||
t.Errorf(
|
||||
"#%d: expected %q, but got %q",
|
||||
n, test.expected, got,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user