package syntax import ( "fmt" "testing" "git.gophernest.net/azpect/TextEditor/internal/core" "git.gophernest.net/azpect/TextEditor/internal/theme" "git.gophernest.net/azpect/TextEditor/internal/theme/themes" ) type seqOp func(*core.Buffer, *core.Window) func TestTreeSitterEngineEditSequences(t *testing.T) { cases := []struct { name string lines []string opList []seqOp }{ { name: "setline and insertline", lines: []string{"package main", "func main() {", " x := 1", "}"}, opList: []seqOp{ func(b *core.Buffer, _ *core.Window) { b.SetLine(2, " x := 2") }, func(b *core.Buffer, _ *core.Window) { b.InsertLine(3, " println(x)") }, }, }, { name: "delete middle line", lines: []string{"package main", "func main() {", " x := 1", " y := 2", "}"}, opList: []seqOp{ func(b *core.Buffer, _ *core.Window) { b.DeleteLine(3) }, }, }, { name: "multiline string evolve", lines: []string{"package main", "func main() {", " s := `a", "b`", " _ = s", "}"}, opList: []seqOp{ func(b *core.Buffer, _ *core.Window) { b.SetLine(2, " s := `alpha") }, func(b *core.Buffer, _ *core.Window) { b.SetLine(3, "beta`") }, }, }, { name: "undo redo sequence", lines: []string{"package main", "func main() {", " v := 3", "}"}, opList: []seqOp{ func(b *core.Buffer, w *core.Window) { b.UndoStack.BeginBlock(w.Cursor) b.SetLine(2, " v := 9") b.UndoStack.EndBlock(core.Position{Line: 2, Col: 8}) }, func(b *core.Buffer, w *core.Window) { _ = b.Undo(w) }, func(b *core.Buffer, w *core.Window) { _ = b.Redo(w) }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { b := core.NewBufferBuilder(). WithFilename("seq.go"). WithFiletype("go"). WithLines(tc.lines). Build() buf := &b w := core.NewWindowBuilder().WithBuffer(buf).WithDimensions(120, 40).Build() win := &w editorTheme := themes.NewDefaultTheme() engine := NewTreeSitterEngine(editorTheme) buf.OnChange = func(change core.BufferChange) { if change.Edit != nil { engine.ApplyEdit(buf, change.Edit) } else { engine.InvalidateBuffer(buf) } } engine.PrepareBuffer(buf, editorTheme) assertEngineInvariants(t, engine, buf, editorTheme, "initial") for i, op := range tc.opList { op(buf, win) engine.PrepareBuffer(buf, editorTheme) assertEngineInvariants(t, engine, buf, editorTheme, fmt.Sprintf("after op %d", i+1)) } }) } } func assertEngineInvariants(t *testing.T, engine *TreeSitterEngine, buf *core.Buffer, editorTheme theme.EditorTheme, phase string) { t.Helper() bc := engine.getCache(buf) if !bc.built { t.Fatalf("%s: expected built cache", phase) } if bc.dirtyAll { t.Fatalf("%s: expected dirtyAll=false after prepare", phase) } if len(bc.dirty) != 0 { t.Fatalf("%s: expected no pending dirty ranges", phase) } for i := 0; i < buf.LineCount(); i++ { line := buf.Line(i) m := engine.LineStyleMap(buf, i, editorTheme) if len(m) != len([]rune(line)) { t.Fatalf("%s: line %d style length mismatch: got %d want %d", phase, i, len(m), len([]rune(line))) } } }