exp/textinput: initialize a piece table by replacing

If the first operation is 'replace', disable undoing this.

Updates guigui-gui/guigui#279
This commit is contained in:
Hajime Hoshi
2026-01-06 13:30:52 +09:00
parent 2d63575ba0
commit fe93e453bb
2 changed files with 210 additions and 5 deletions
+29
View File
@@ -150,10 +150,39 @@ func (p *pieceTable) Len() int {
}
func (p *pieceTable) replace(text string, start, end int) {
// If the first operation is 'replace', initialize the table.
if len(p.history) == 0 {
p.init(text)
return
}
p.maybeAppendHistory(text, start, end, 0, 0, 1, false)
p.doReplace(text, start, end)
}
func (p *pieceTable) init(text string) {
if len(p.history) > 0 {
panic("textinput: init should be called when history is empty")
}
if len(p.table) > 0 {
panic("textinput: init should be called when table is empty")
}
p.table = append(p.table, text...)
p.history = append(p.history, historyItem{
items: []pieceTableItem{
{
start: 0,
end: len(text),
},
},
undoSelectionStart: 0,
undoSelectionEnd: 0,
redoSelectionStart: 0,
redoSelectionEnd: len(text),
})
}
func (p *pieceTable) doReplace(text string, start, end int) {
items := p.history[p.historyIndex].items
+181 -5
View File
@@ -574,7 +574,163 @@ func TestPieceTableHistoryMergingApplePressHold(t *testing.T) {
}
check("foo")
// Undo Op 1
// Undo Op 1 fails, as the initial state is determined by Replace.
start, end, ok = p.Undo()
if ok {
t.Fatal("Undo should fail")
}
check("foo")
// Redo Op 2, 3, 4, 5
start, end, ok = p.Redo()
if !ok {
t.Fatal("Redo failed")
}
if start != 3 || end != 7 {
t.Errorf("Redo: got (%d, %d), want (3, 7)", start, end)
}
check("fooàà")
}
func TestPieceTableInitialStateWithReplace(t *testing.T) {
var p textinput.PieceTable
check := func(want string) {
t.Helper()
var b strings.Builder
if _, err := p.WriteTo(&b); err != nil {
t.Fatalf("WriteTo failed: %v", err)
}
if got := b.String(); got != want {
t.Errorf("got %q, want %q", got, want)
}
}
check("")
// Op 1
p.Replace("foo", 0, 0)
check("foo")
// Op 2
p.Replace("bar", 0, 3)
check("bar")
// Op 3
p.Replace("baz", 0, 3)
check("baz")
// Undo Op 3
start, end, ok := p.Undo()
if !ok {
t.Fatal("Undo failed")
}
if start != 0 || end != 3 {
t.Errorf("Undo: got (%d, %d), want (0, 3)", start, end)
}
check("bar")
// Undo Op 2
start, end, ok = p.Undo()
if !ok {
t.Fatal("Undo failed")
}
if start != 0 || end != 3 {
t.Errorf("Undo: got (%d, %d), want (0, 3)", start, end)
}
check("foo")
// Undo Op 1 fails, as the initial state is determined by Replace.
start, end, ok = p.Undo()
if ok {
t.Fatal("Undo should fail")
}
check("foo")
// Redo Op 2
start, end, ok = p.Redo()
if !ok {
t.Fatal("Redo failed")
}
if start != 0 || end != 3 {
t.Errorf("Redo: got (%d, %d), want (0, 3)", start, end)
}
check("bar")
// Redo Op 3
start, end, ok = p.Redo()
if !ok {
t.Fatal("Redo failed")
}
if start != 0 || end != 3 {
t.Errorf("Redo: got (%d, %d), want (0, 3)", start, end)
}
check("baz")
}
func TestPieceTableInitialStateWithUpdateIME(t *testing.T) {
var p textinput.PieceTable
check := func(want string) {
t.Helper()
var b strings.Builder
if _, err := p.WriteTo(&b); err != nil {
t.Fatalf("WriteTo failed: %v", err)
}
if got := b.String(); got != want {
t.Errorf("got %q, want %q", got, want)
}
}
check("")
// Op 1
p.UpdateByIME(textinput.TextInputState{Text: "foo"}, 0, 0)
check("foo")
// Op 2
p.Replace("", 0, 3)
check("")
// Op 3
p.Replace("bar", 0, 0)
check("bar")
// Op 4
p.Replace("baz", 0, 3)
check("baz")
// Undo Op 4
start, end, ok := p.Undo()
if !ok {
t.Fatal("Undo failed")
}
if start != 0 || end != 3 {
t.Errorf("Undo: got (%d, %d), want (0, 3)", start, end)
}
check("bar")
// Undo Op 3
start, end, ok = p.Undo()
if !ok {
t.Fatal("Undo failed")
}
if start != 0 || end != 0 {
t.Errorf("Undo: got (%d, %d), want (0, 0)", start, end)
}
check("")
// Undo Op 2
start, end, ok = p.Undo()
if !ok {
t.Fatal("Undo failed")
}
if start != 0 || end != 3 {
t.Errorf("Undo: got (%d, %d), want (0, 3)", start, end)
}
check("foo")
// Undo Op 1 succeeds, as the first operation is UpdateByIME.
start, end, ok = p.Undo()
if !ok {
t.Fatal("Undo failed")
@@ -594,13 +750,33 @@ func TestPieceTableHistoryMergingApplePressHold(t *testing.T) {
}
check("foo")
// Redo Op 2, 3, 4, 5
// Redo Op 2
start, end, ok = p.Redo()
if !ok {
t.Fatal("Redo failed")
}
if start != 3 || end != 7 {
t.Errorf("Redo: got (%d, %d), want (3, 7)", start, end)
if start != 0 || end != 0 {
t.Errorf("Redo: got (%d, %d), want (0, 0)", start, end)
}
check("fooàà")
check("")
// Redo Op 3
start, end, ok = p.Redo()
if !ok {
t.Fatal("Redo failed")
}
if start != 0 || end != 3 {
t.Errorf("Redo: got (%d, %d), want (0, 3)", start, end)
}
check("bar")
// Redo Op 4
start, end, ok = p.Redo()
if !ok {
t.Fatal("Redo failed")
}
if start != 0 || end != 3 {
t.Errorf("Redo: got (%d, %d), want (0, 3)", start, end)
}
check("baz")
}