385 lines
10 KiB
Go
385 lines
10 KiB
Go
package syntax
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
|
"git.gophernest.net/azpect/TextEditor/internal/theme/themes"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
func TestTreeSitterEngineHighlightsGoKeywordAndString(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("sample.go").
|
|
WithFiletype("go").
|
|
WithLines([]string{
|
|
"package main",
|
|
"func main() {",
|
|
" s := \"hi\"",
|
|
"}",
|
|
}).
|
|
Build()
|
|
buf := &b
|
|
|
|
editorTheme := themes.NewDefaultTheme()
|
|
engine := NewTreeSitterEngine(editorTheme)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
|
|
base := editorTheme.Line
|
|
|
|
line0 := buf.Line(0)
|
|
map0 := engine.LineStyleMap(buf, 0, editorTheme)
|
|
if len(map0) != len([]rune(line0)) {
|
|
t.Fatalf("line 0 style map length mismatch")
|
|
}
|
|
if len(map0) == 0 || styleEquivalent(map0[0], base) {
|
|
t.Fatalf("expected 'package' keyword to be highlighted")
|
|
}
|
|
|
|
line2 := buf.Line(2)
|
|
stringStart := strings.Index(line2, "\"hi\"")
|
|
if stringStart < 0 {
|
|
t.Fatalf("test setup failed: string literal not found")
|
|
}
|
|
map2 := engine.LineStyleMap(buf, 2, editorTheme)
|
|
if styleEquivalent(map2[stringStart+1], base) {
|
|
t.Fatalf("expected string contents to be highlighted")
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineHighlightsMultilineRawString(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("sample.go").
|
|
WithFiletype("go").
|
|
WithLines([]string{
|
|
"package main",
|
|
"func main() {",
|
|
" s := `hello",
|
|
"world`",
|
|
" println(s)",
|
|
"}",
|
|
}).
|
|
Build()
|
|
buf := &b
|
|
|
|
editorTheme := themes.NewDefaultTheme()
|
|
engine := NewTreeSitterEngine(editorTheme)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
|
|
base := editorTheme.Line
|
|
map3 := engine.LineStyleMap(buf, 3, editorTheme)
|
|
if len(map3) == 0 {
|
|
t.Fatalf("expected style map on multiline raw string line")
|
|
}
|
|
if styleEquivalent(map3[0], base) {
|
|
t.Fatalf("expected multiline raw string line to be highlighted")
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineApplyEditUpdatesStyleCategory(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("sample.go").
|
|
WithFiletype("go").
|
|
WithLines([]string{
|
|
"package main",
|
|
"func main() {",
|
|
" x := 123",
|
|
"}",
|
|
}).
|
|
Build()
|
|
buf := &b
|
|
|
|
editorTheme := themes.NewDefaultTheme()
|
|
engine := NewTreeSitterEngine(editorTheme)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
|
|
oldLine := buf.Line(2)
|
|
oldIdx := strings.Index(oldLine, "123")
|
|
if oldIdx < 0 {
|
|
t.Fatalf("test setup failed: number not found")
|
|
}
|
|
oldMap := engine.LineStyleMap(buf, 2, editorTheme)
|
|
oldStyle := oldMap[oldIdx]
|
|
|
|
var edit *core.BufferEdit
|
|
buf.OnChange = func(change core.BufferChange) {
|
|
edit = change.Edit
|
|
}
|
|
|
|
buf.SetLine(2, " x := \"abc\"")
|
|
if edit == nil {
|
|
t.Fatalf("expected edit metadata from SetLine")
|
|
}
|
|
|
|
engine.ApplyEdit(buf, edit)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
|
|
newLine := buf.Line(2)
|
|
newIdx := strings.Index(newLine, "abc")
|
|
if newIdx < 0 {
|
|
t.Fatalf("test setup failed: string not found")
|
|
}
|
|
newMap := engine.LineStyleMap(buf, 2, editorTheme)
|
|
newStyle := newMap[newIdx]
|
|
|
|
if styleEquivalent(newStyle, editorTheme.Line) {
|
|
t.Fatalf("expected updated string to be highlighted")
|
|
}
|
|
if styleEquivalent(oldStyle, newStyle) {
|
|
t.Fatalf("expected style category to change from number to string")
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineApplyEditLineCountChangeForcesFullDirty(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("sample.go").
|
|
WithFiletype("go").
|
|
WithLines([]string{"package main", "func main() {}"}).
|
|
Build()
|
|
buf := &b
|
|
|
|
editorTheme := themes.NewDefaultTheme()
|
|
engine := NewTreeSitterEngine(editorTheme)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
bc := engine.getCache(buf)
|
|
|
|
var edit *core.BufferEdit
|
|
buf.OnChange = func(change core.BufferChange) {
|
|
edit = change.Edit
|
|
}
|
|
|
|
buf.InsertLine(1, "var x = 1")
|
|
if edit == nil {
|
|
t.Fatalf("expected edit metadata from InsertLine")
|
|
}
|
|
|
|
engine.ApplyEdit(buf, edit)
|
|
if !bc.dirtyAll {
|
|
t.Fatalf("expected line count change to set dirtyAll")
|
|
}
|
|
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
if !bc.built {
|
|
t.Fatalf("expected cache rebuilt after prepare")
|
|
}
|
|
if bc.count != buf.LineCount() {
|
|
t.Fatalf("expected cache line count to match buffer")
|
|
}
|
|
if bc.dirtyAll {
|
|
t.Fatalf("expected dirtyAll to clear after rebuild")
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineUnsupportedBufferFallsBackToDefaultStyles(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("notes.txt").
|
|
WithFiletype("txt").
|
|
WithLines([]string{"just text", "with no language"}).
|
|
Build()
|
|
buf := &b
|
|
|
|
editorTheme := themes.NewDefaultTheme()
|
|
engine := NewTreeSitterEngine(editorTheme)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
|
|
base := editorTheme.Line
|
|
line := buf.Line(0)
|
|
m := engine.LineStyleMap(buf, 0, editorTheme)
|
|
if len(m) != len([]rune(line)) {
|
|
t.Fatalf("style map length mismatch on fallback buffer")
|
|
}
|
|
for i := range m {
|
|
if !styleEquivalent(m[i], base) {
|
|
t.Fatalf("expected default style for unsupported filetype at rune %d", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineLastLineEditDoesNotPanicAndRebuilds(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("sample.go").
|
|
WithFiletype("go").
|
|
WithLines([]string{"package main", "func main() {", " return", "}"}).
|
|
Build()
|
|
buf := &b
|
|
|
|
editorTheme := themes.NewDefaultTheme()
|
|
engine := NewTreeSitterEngine(editorTheme)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
bc := engine.getCache(buf)
|
|
|
|
var edit *core.BufferEdit
|
|
buf.OnChange = func(change core.BufferChange) {
|
|
edit = change.Edit
|
|
}
|
|
|
|
buf.SetLine(3, "// end")
|
|
if edit == nil {
|
|
t.Fatalf("expected edit metadata for last line change")
|
|
}
|
|
|
|
engine.ApplyEdit(buf, edit)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
|
|
if !bc.built {
|
|
t.Fatalf("expected cache built after last-line edit")
|
|
}
|
|
if len(bc.dirty) != 0 {
|
|
t.Fatalf("expected dirty ranges cleared after rebuild")
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineThemeChangeRebuildsWithNewCaptureStylesAfterInvalidation(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("sample.go").
|
|
WithFiletype("go").
|
|
WithLines([]string{"package main", "func main() {", " return", "}"}).
|
|
Build()
|
|
buf := &b
|
|
|
|
themeA := themes.NewDefaultTheme()
|
|
themeB := makeThemeWithCaptureOverrides(lipgloss.Color("#ffffff"), lipgloss.Color("#ff00ff"), lipgloss.Color("#00ffaa"))
|
|
|
|
engine := NewTreeSitterEngine(themeA)
|
|
engine.PrepareBuffer(buf, themeA)
|
|
|
|
line := buf.Line(0)
|
|
keywordIdx := strings.Index(line, "package")
|
|
if keywordIdx < 0 {
|
|
t.Fatalf("test setup failed: expected package keyword")
|
|
}
|
|
|
|
before := engine.LineStyleMap(buf, 0, themeA)
|
|
beforeStyle := before[keywordIdx]
|
|
if styleEquivalent(beforeStyle, themeA.Line) {
|
|
t.Fatalf("expected keyword style to differ from base style before theme switch")
|
|
}
|
|
|
|
engine.InvalidateBuffer(buf)
|
|
engine.PrepareBuffer(buf, themeB)
|
|
|
|
after := engine.LineStyleMap(buf, 0, themeB)
|
|
afterStyle := after[keywordIdx]
|
|
if styleEquivalent(afterStyle, themeB.Line) {
|
|
t.Fatalf("expected keyword style to differ from base style after theme switch")
|
|
}
|
|
if styleEquivalent(beforeStyle, afterStyle) {
|
|
t.Fatalf("expected keyword style to change after theme switch and invalidation")
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineThemeChangeRebuildsFallbackLineStylesAfterInvalidation(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("notes.txt").
|
|
WithFiletype("txt").
|
|
WithLines([]string{"plain text"}).
|
|
Build()
|
|
buf := &b
|
|
|
|
themeA := themes.NewDefaultTheme()
|
|
themeB := makeThemeWithCaptureOverrides(lipgloss.Color("#101010"), lipgloss.Color("#ff00ff"), lipgloss.Color("#00ffaa"))
|
|
|
|
engine := NewTreeSitterEngine(themeA)
|
|
mapA := engine.LineStyleMap(buf, 0, themeA)
|
|
if len(mapA) == 0 {
|
|
t.Fatalf("expected non-empty style map for text line")
|
|
}
|
|
if !styleEquivalent(mapA[0], themeA.Line) {
|
|
t.Fatalf("expected fallback style to use first theme line style")
|
|
}
|
|
|
|
engine.InvalidateBuffer(buf)
|
|
mapB := engine.LineStyleMap(buf, 0, themeB)
|
|
if !styleEquivalent(mapB[0], themeB.Line) {
|
|
t.Fatalf("expected fallback style to use second theme line style after invalidation")
|
|
}
|
|
if styleEquivalent(mapA[0], mapB[0]) {
|
|
t.Fatalf("expected fallback style to change after theme switch and invalidation")
|
|
}
|
|
}
|
|
|
|
func TestTreeSitterEngineUnsupportedApplyEditFallsBackToDefaultStyles(t *testing.T) {
|
|
b := core.NewBufferBuilder().
|
|
WithFilename("notes.txt").
|
|
WithFiletype("txt").
|
|
WithLines([]string{"just text"}).
|
|
Build()
|
|
buf := &b
|
|
|
|
editorTheme := themes.NewDefaultTheme()
|
|
engine := NewTreeSitterEngine(editorTheme)
|
|
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
baseline := engine.LineStyleMap(buf, 0, editorTheme)
|
|
if len(baseline) == 0 {
|
|
t.Fatalf("expected baseline style map")
|
|
}
|
|
|
|
var edit *core.BufferEdit
|
|
buf.OnChange = func(change core.BufferChange) {
|
|
edit = change.Edit
|
|
}
|
|
buf.SetLine(0, "still plain text")
|
|
if edit == nil {
|
|
t.Fatalf("expected edit metadata from SetLine")
|
|
}
|
|
|
|
engine.ApplyEdit(buf, edit)
|
|
engine.PrepareBuffer(buf, editorTheme)
|
|
|
|
after := engine.LineStyleMap(buf, 0, editorTheme)
|
|
if len(after) != len([]rune(buf.Line(0))) {
|
|
t.Fatalf("style map length mismatch after unsupported apply edit")
|
|
}
|
|
for i := range after {
|
|
if !styleEquivalent(after[i], editorTheme.Line) {
|
|
t.Fatalf("expected fallback line style at rune %d after unsupported apply edit", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeThemeWithCaptureOverrides(lineFg, keywordFg, stringFg lipgloss.Color) theme.EditorTheme {
|
|
t := themes.NewDefaultTheme()
|
|
t.Line = t.Line.Foreground(lineFg)
|
|
t.Syntax.Group = cloneStyleMap(t.Syntax.Group)
|
|
t.Syntax.Exact = cloneStyleMap(t.Syntax.Exact)
|
|
t.Syntax.Group["keyword"] = lipgloss.NewStyle().Foreground(keywordFg)
|
|
t.Syntax.Group["string"] = lipgloss.NewStyle().Foreground(stringFg)
|
|
for key := range t.Syntax.Exact {
|
|
if strings.HasPrefix(key, "keyword") {
|
|
t.Syntax.Exact[key] = lipgloss.NewStyle().Foreground(keywordFg)
|
|
}
|
|
if strings.HasPrefix(key, "string") {
|
|
t.Syntax.Exact[key] = lipgloss.NewStyle().Foreground(stringFg)
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func cloneStyleMap(in map[string]lipgloss.Style) map[string]lipgloss.Style {
|
|
out := make(map[string]lipgloss.Style, len(in))
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func styleEquivalent(a, b lipgloss.Style) bool {
|
|
return styleSignature(a) == styleSignature(b)
|
|
}
|
|
|
|
func styleSignature(s lipgloss.Style) string {
|
|
return fmt.Sprintf(
|
|
"fg=%v,bg=%v,bold=%v,italic=%v,underline=%v,reverse=%v",
|
|
s.GetForeground(),
|
|
s.GetBackground(),
|
|
s.GetBold(),
|
|
s.GetItalic(),
|
|
s.GetUnderline(),
|
|
s.GetReverse(),
|
|
)
|
|
}
|