mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
delete unicode chars with length > 1 correctly
When there were non ASCII characters (for exemple éèàçîï) in a deleted selection or word, more characters were deleted because there was a mismatch between runes and bytes in Delete and deleteWord Fixes: https://todo.sr.ht/~eliasnaur/gio/330 Signed-off-by: Fabien Jansem <fabien@jansem.eu.org>
This commit is contained in:
committed by
Elias Naur
parent
ac3bbc72ac
commit
9b7ec167bc
@@ -46,6 +46,25 @@ func (e *editBuffer) deleteRunes(caret, runes int) int {
|
||||
return caret
|
||||
}
|
||||
|
||||
func (e *editBuffer) deleteBytes(caret, bytes int) int {
|
||||
e.moveGap(caret, 0)
|
||||
if bytes < 0 {
|
||||
e.gapstart += bytes
|
||||
if e.gapstart < 0 {
|
||||
e.gapstart = 0
|
||||
}
|
||||
caret = e.gapstart
|
||||
}
|
||||
if bytes > 0 {
|
||||
e.gapend += bytes
|
||||
if e.gapend > len(e.text) {
|
||||
e.gapend = len(e.text)
|
||||
}
|
||||
}
|
||||
e.changed = e.changed || bytes != 0
|
||||
return caret
|
||||
}
|
||||
|
||||
// moveGap moves the gap to the caret position. After returning,
|
||||
// the gap is guaranteed to be at least space bytes long.
|
||||
func (e *editBuffer) moveGap(caret, space int) {
|
||||
|
||||
+16
-10
@@ -838,7 +838,7 @@ func (e *Editor) Delete(runes int) {
|
||||
}
|
||||
|
||||
if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 {
|
||||
e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, l)
|
||||
e.caret.start.ofs = e.rr.deleteBytes(e.caret.start.ofs, l)
|
||||
runes -= sign(runes)
|
||||
}
|
||||
|
||||
@@ -871,7 +871,7 @@ func (e *Editor) prepend(s string) {
|
||||
if e.SingleLine {
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
}
|
||||
e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first.
|
||||
e.caret.start.ofs = e.rr.deleteBytes(e.caret.start.ofs, e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first.
|
||||
e.rr.prepend(e.caret.start.ofs, s)
|
||||
e.caret.start.xoff = 0
|
||||
e.invalidate()
|
||||
@@ -1122,8 +1122,8 @@ func (e *Editor) deleteWord(distance int) {
|
||||
idx := e.caret.start.ofs + offset*direction
|
||||
return idx <= 0 || idx >= e.rr.len()
|
||||
}
|
||||
// next returns the appropriate rune given the direction and offset.
|
||||
next := func(offset int) (r rune) {
|
||||
// next returns the appropriate rune and length given the direction and offset (in bytes).
|
||||
next := func(offset int) (r rune, l int) {
|
||||
idx := e.caret.start.ofs + offset*direction
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
@@ -1131,21 +1131,27 @@ func (e *Editor) deleteWord(distance int) {
|
||||
idx = e.rr.len()
|
||||
}
|
||||
if direction < 0 {
|
||||
r, _ = e.rr.runeBefore(idx)
|
||||
r, l = e.rr.runeBefore(idx)
|
||||
} else {
|
||||
r, _ = e.rr.runeAt(idx)
|
||||
r, l = e.rr.runeAt(idx)
|
||||
}
|
||||
return r
|
||||
return
|
||||
}
|
||||
var runes = 1
|
||||
_, bytes := e.rr.runeAt(e.caret.start.ofs)
|
||||
if direction < 0 {
|
||||
_, bytes = e.rr.runeBefore(e.caret.start.ofs)
|
||||
}
|
||||
for ii := 0; ii < words; ii++ {
|
||||
if r := next(runes); unicode.IsSpace(r) {
|
||||
for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
|
||||
if r, _ := next(bytes); unicode.IsSpace(r) {
|
||||
for r, lg := next(bytes); unicode.IsSpace(r) && !atEnd(bytes); r, lg = next(bytes) {
|
||||
runes += 1
|
||||
bytes += lg
|
||||
}
|
||||
} else {
|
||||
for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
|
||||
for r, lg := next(bytes); !unicode.IsSpace(r) && !atEnd(bytes); r, lg = next(bytes) {
|
||||
runes += 1
|
||||
bytes += lg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +245,110 @@ func TestEditorMoveWord(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorInsert(t *testing.T) {
|
||||
type Test struct {
|
||||
Text string
|
||||
Start int
|
||||
Selection int
|
||||
Insertion string
|
||||
|
||||
Result string
|
||||
}
|
||||
tests := []Test{
|
||||
// Nothing inserted
|
||||
{"", 0, 0, "", ""},
|
||||
{"", 0, -1, "", ""},
|
||||
{"", 0, 1, "", ""},
|
||||
{"", 0, -2, "", ""},
|
||||
{"", 0, 2, "", ""},
|
||||
{"world", 0, 0, "", "world"},
|
||||
{"world", 0, -1, "", "world"},
|
||||
{"world", 0, 1, "", "orld"},
|
||||
{"world", 2, 0, "", "world"},
|
||||
{"world", 2, -1, "", "wrld"},
|
||||
{"world", 2, 1, "", "wold"},
|
||||
{"world", 5, 0, "", "world"},
|
||||
{"world", 5, -1, "", "worl"},
|
||||
{"world", 5, 1, "", "world"},
|
||||
// One rune inserted
|
||||
{"", 0, 0, "_", "_"},
|
||||
{"", 0, -1, "_", "_"},
|
||||
{"", 0, 1, "_", "_"},
|
||||
{"", 0, -2, "_", "_"},
|
||||
{"", 0, 2, "_", "_"},
|
||||
{"world", 0, 0, "_", "_world"},
|
||||
{"world", 0, -1, "_", "_world"},
|
||||
{"world", 0, 1, "_", "_orld"},
|
||||
{"world", 2, 0, "_", "wo_rld"},
|
||||
{"world", 2, -1, "_", "w_rld"},
|
||||
{"world", 2, 1, "_", "wo_ld"},
|
||||
{"world", 5, 0, "_", "world_"},
|
||||
{"world", 5, -1, "_", "worl_"},
|
||||
{"world", 5, 1, "_", "world_"},
|
||||
// More runes inserted
|
||||
{"", 0, 0, "-3-", "-3-"},
|
||||
{"", 0, -1, "-3-", "-3-"},
|
||||
{"", 0, 1, "-3-", "-3-"},
|
||||
{"", 0, -2, "-3-", "-3-"},
|
||||
{"", 0, 2, "-3-", "-3-"},
|
||||
{"world", 0, 0, "-3-", "-3-world"},
|
||||
{"world", 0, -1, "-3-", "-3-world"},
|
||||
{"world", 0, 1, "-3-", "-3-orld"},
|
||||
{"world", 2, 0, "-3-", "wo-3-rld"},
|
||||
{"world", 2, -1, "-3-", "w-3-rld"},
|
||||
{"world", 2, 1, "-3-", "wo-3-ld"},
|
||||
{"world", 5, 0, "-3-", "world-3-"},
|
||||
{"world", 5, -1, "-3-", "worl-3-"},
|
||||
{"world", 5, 1, "-3-", "world-3-"},
|
||||
// Runes with length > 1 inserted
|
||||
{"", 0, 0, "éêè", "éêè"},
|
||||
{"", 0, -1, "éêè", "éêè"},
|
||||
{"", 0, 1, "éêè", "éêè"},
|
||||
{"", 0, -2, "éêè", "éêè"},
|
||||
{"", 0, 2, "éêè", "éêè"},
|
||||
{"world", 0, 0, "éêè", "éêèworld"},
|
||||
{"world", 0, -1, "éêè", "éêèworld"},
|
||||
{"world", 0, 1, "éêè", "éêèorld"},
|
||||
{"world", 2, 0, "éêè", "woéêèrld"},
|
||||
{"world", 2, -1, "éêè", "wéêèrld"},
|
||||
{"world", 2, 1, "éêè", "woéêèld"},
|
||||
{"world", 5, 0, "éêè", "worldéêè"},
|
||||
{"world", 5, -1, "éêè", "worléêè"},
|
||||
{"world", 5, 1, "éêè", "worldéêè"},
|
||||
// Runes with length > 1 deleted from selection
|
||||
{"élançé", 0, 1, "", "lançé"},
|
||||
{"élançé", 0, 1, "-3-", "-3-lançé"},
|
||||
{"élançé", 3, 2, "-3-", "éla-3-é"},
|
||||
{"élançé", 3, 3, "-3-", "éla-3-"},
|
||||
{"élançé", 3, 10, "-3-", "éla-3-"},
|
||||
{"élançé", 5, -1, "-3-", "élan-3-é"},
|
||||
{"élançé", 6, -1, "-3-", "élanç-3-"},
|
||||
{"élançé", 6, -3, "-3-", "éla-3-"},
|
||||
}
|
||||
setup := func(t string) *Editor {
|
||||
e := new(Editor)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
}
|
||||
cache := text.NewCache(gofont.Collection())
|
||||
fontSize := unit.Px(10)
|
||||
font := text.Font{}
|
||||
e.SetText(t)
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
return e
|
||||
}
|
||||
for ii, tt := range tests {
|
||||
e := setup(tt.Text)
|
||||
e.MoveCaret(tt.Start, tt.Start)
|
||||
e.MoveCaret(0, tt.Selection)
|
||||
e.Insert(tt.Insertion)
|
||||
if e.Text() != tt.Result {
|
||||
t.Fatalf("[%d] Insert: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorDeleteWord(t *testing.T) {
|
||||
type Test struct {
|
||||
Text string
|
||||
@@ -283,6 +387,7 @@ func TestEditorDeleteWord(t *testing.T) {
|
||||
{"hello world", 8, 0, 1, 8, "hello "},
|
||||
{"hello world", 8, 0, -1, 5, "hello world"},
|
||||
{"hello brave new world", 0, 0, 3, 0, " new world"},
|
||||
{"helléèçàô world", 3, 0, 1, 3, "hel world"}, // unicode char with length > 1 in deleted part
|
||||
// Add selected text.
|
||||
//
|
||||
// Several permutations must be tested:
|
||||
@@ -295,11 +400,16 @@ func TestEditorDeleteWord(t *testing.T) {
|
||||
{"hello there brave new world", 12, 6, 2, 12, "hello there world"}, // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases.
|
||||
{"hello there brave new world", 12, 6, -1, 12, "hello there new world"},
|
||||
{"hello there brave new world", 12, 6, -2, 6, "hello new world"},
|
||||
{"hello there b®âve new world", 12, 6, 1, 12, "hello there new world"}, // unicode chars with length > 1 in selection
|
||||
{"hello there b®âve new world", 12, 6, 2, 12, "hello there world"}, // ditto
|
||||
{"hello there b®âve new world", 12, 6, -1, 12, "hello there new world"}, // ditto
|
||||
{"hello there b®âve new world", 12, 6, -2, 6, "hello new world"}, // ditto
|
||||
// "|brave " selected
|
||||
{"hello there brave new world", 18, -6, 1, 12, "hello there new world"}, // #20
|
||||
{"hello there brave new world", 18, -6, 2, 12, "hello there world"}, // ditto
|
||||
{"hello there brave new world", 18, -6, -1, 12, "hello there new world"},
|
||||
{"hello there brave new world", 18, -6, -2, 6, "hello new world"},
|
||||
{"hello there b®âve new world", 18, -6, 1, 12, "hello there new world"}, // unicode chars with length > 1 in selection
|
||||
// Random edge cases
|
||||
{"hello there brave new world", 12, 6, 99, 12, "hello there "},
|
||||
{"hello there brave new world", 18, -6, -99, 0, "new world"},
|
||||
|
||||
Reference in New Issue
Block a user