package editor import ( "testing" "glint/internal/spell" "glint/internal/theme" ) // grammarSpans returns, for each Wavy span in the built visual model, its text // mapped to its undercurl color — enough to tell grammar (green) from spell (red). func grammarSpans(e *Editor) map[string]string { got := map[string]string{} for _, vr := range e.buildVisual() { for _, sp := range vr.spans { if sp.Wavy { got[sp.Text] = string(sp.UnderColor) } } } return got } func grammarEditor(t *testing.T, content string) *Editor { t.Helper() e := New() e.SetTheme(theme.FlexokiDark()) e.SetContent([]byte(content)) e.SetGrammar(true) return e } func TestGrammarUnderlinesRange(t *testing.T) { e := grammarEditor(t, "This is a a test") // "a a" spans rune columns [8,11). e.SetGrammarDiags(map[int][][2]int{0: {{8, 11}}}) green := string(theme.FlexokiDark().Grammar) spans := grammarSpans(e) if spans["a a"] != green { t.Errorf("expected \"a a\" underlined green (%s); got wavy spans %v", green, spans) } } func TestGrammarSpanAt(t *testing.T) { e := grammarEditor(t, "This is a a test") e.SetGrammarDiags(map[int][][2]int{0: {{8, 11}}}) // Inside and at both boundaries (end inclusive) resolves the span. for _, col := range []int{8, 9, 11} { s, en, ok := e.GrammarSpanAt(0, col) if !ok || s != 8 || en != 11 { t.Errorf("GrammarSpanAt(0,%d) = %d,%d,%v; want 8,11,true", col, s, en, ok) } } // Outside the span, and when grammar is off. if _, _, ok := e.GrammarSpanAt(0, 3); ok { t.Error("col 3 should not be in a grammar span") } e.SetGrammar(false) if _, _, ok := e.GrammarSpanAt(0, 9); ok { t.Error("grammar off should report no span") } } func TestReplaceRuneRangeSingleLine(t *testing.T) { e := grammarEditor(t, "This is a a test") e.ReplaceRuneRange(0, 8, 0, 11, "a") // collapse "a a" -> "a" if got := string(e.Bytes()); got != "This is a test" { t.Errorf("got %q, want %q", got, "This is a test") } if e.Cursor.Row != 0 || e.Cursor.Col != 9 { t.Errorf("cursor = %d,%d; want 0,9 (after inserted text)", e.Cursor.Row, e.Cursor.Col) } if !e.Dirty { t.Error("replacement should mark the buffer dirty") } } func TestReplaceRuneRangeMultiLineInsert(t *testing.T) { e := grammarEditor(t, "one two three") // Replace "two" (runes [4,7)) with a two-line insertion. e.ReplaceRuneRange(0, 4, 0, 7, "X\nY") if got := string(e.Bytes()); got != "one X\nY three" { t.Errorf("got %q, want %q", got, "one X\nY three") } if e.Cursor.Row != 1 || e.Cursor.Col != 1 { t.Errorf("cursor = %d,%d; want 1,1", e.Cursor.Row, e.Cursor.Col) } } func TestReplaceRuneRangeUndo(t *testing.T) { e := grammarEditor(t, "This is a a test") e.ReplaceRuneRange(0, 8, 0, 11, "a") e.Undo() if got := string(e.Bytes()); got != "This is a a test" { t.Errorf("undo left %q, want original", got) } } func TestRuneRangeText(t *testing.T) { e := grammarEditor(t, "This is a a test") if got := e.RuneRangeText(0, 8, 11); got != "a a" { t.Errorf("RuneRangeText = %q, want %q", got, "a a") } if got := e.RuneRangeText(0, 8, 999); got != "a a test" { t.Errorf("clamped RuneRangeText = %q", got) } if got := e.RuneRangeText(5, 0, 3); got != "" { t.Errorf("out-of-range row = %q, want empty", got) } } func TestGrammarInactiveWhenOff(t *testing.T) { e := grammarEditor(t, "This is a a test") e.SetGrammarDiags(map[int][][2]int{0: {{8, 11}}}) e.SetGrammar(false) if s := grammarSpans(e); len(s) != 0 { t.Errorf("grammar off should render no undercurl; got %v", s) } } func TestGrammarInactiveOnCodeFile(t *testing.T) { e := grammarEditor(t, "This is a a test") e.SetLanguage("main.go") // code file: grammar skipped like spellcheck e.SetGrammarDiags(map[int][][2]int{0: {{8, 11}}}) if s := grammarSpans(e); len(s) != 0 { t.Errorf("grammar should skip code files; got %v", s) } } // TestSpellingWinsOverGrammar asserts a misspelled word inside a grammar range // keeps its red spell underline rather than being repainted green. func TestSpellingWinsOverGrammar(t *testing.T) { d, err := spell.Load() if err != nil { t.Fatalf("spell.Load: %v", err) } e := New() e.SetTheme(theme.FlexokiDark()) e.SetContent([]byte("This recieve is wrong")) e.SetDict(d) e.SetSpell(true) e.SetGrammar(true) // "recieve" is a misspelling at rune columns [5,12); cover it with a grammar range. e.SetGrammarDiags(map[int][][2]int{0: {{5, 12}}}) spans := grammarSpans(e) red := string(theme.FlexokiDark().Spell) if spans["recieve"] != red { t.Errorf("misspelled \"recieve\" should stay red (%s), not grammar green; got %v", red, spans) } }