Add editor packages

This commit is contained in:
dwrz
2026-10-26 02:32:03 +00:00
parent 38714f6c80
commit 4ed2ee97e0
23 changed files with 2114 additions and 0 deletions
+51
View File
@@ -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()
}
+131
View File
@@ -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
}
+319
View File
@@ -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,
)
}
}
}