1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
package grammar
import (
"bufio"
"bytes"
"testing"
"time"
)
func TestFrameRoundTrip(t *testing.T) {
var buf bytes.Buffer
bodies := [][]byte{
[]byte(`{"jsonrpc":"2.0","id":1}`),
[]byte(`{"method":"textDocument/didOpen"}`),
[]byte(`{}`),
}
for _, b := range bodies {
if err := writeFrame(&buf, b); err != nil {
t.Fatalf("writeFrame: %v", err)
}
}
r := bufio.NewReader(&buf)
for i, want := range bodies {
got, err := readFrame(r)
if err != nil {
t.Fatalf("readFrame %d: %v", i, err)
}
if !bytes.Equal(got, want) {
t.Errorf("frame %d = %q, want %q", i, got, want)
}
}
}
func TestReadFrameCaseInsensitiveHeader(t *testing.T) {
// Some servers vary header casing; the length header must still parse.
raw := "content-length: 2\r\n\r\n{}"
got, err := readFrame(bufio.NewReader(bytes.NewBufferString(raw)))
if err != nil {
t.Fatalf("readFrame: %v", err)
}
if string(got) != "{}" {
t.Errorf("got %q, want {}", got)
}
}
func TestUTF16ToRuneCol(t *testing.T) {
cases := []struct {
line string
u16 int
want int
}{
{"hello", 0, 0},
{"hello", 3, 3},
{"hello", 99, 5}, // clamp past end
{"a๐b", 0, 0}, // emoji is 2 UTF-16 units, 1 rune
{"a๐b", 1, 1}, // before the emoji
{"a๐b", 3, 2}, // after the emoji (1 + 2 units) -> rune col 2
}
for _, c := range cases {
if got := utf16ToRuneCol(c.line, c.u16); got != c.want {
t.Errorf("utf16ToRuneCol(%q, %d) = %d, want %d", c.line, c.u16, got, c.want)
}
}
}
func TestLSPRangeToDiagsSingleLine(t *testing.T) {
lines := []string{"This is a a sentence."}
got := lspRangeToDiags(lines, 0, 8, 0, 11, "Did you mean to repeat this word?", "RepeatedWords")
if len(got) != 1 {
t.Fatalf("got %d diags, want 1", len(got))
}
d := got[0]
if d.Line != 0 || d.StartCol != 8 || d.EndCol != 11 {
t.Errorf("range = line %d [%d,%d), want line 0 [8,11)", d.Line, d.StartCol, d.EndCol)
}
if d.Code != "RepeatedWords" {
t.Errorf("code = %q", d.Code)
}
}
func TestLSPRangeToDiagsMultiLine(t *testing.T) {
lines := []string{"first line", "second", "third line"}
got := lspRangeToDiags(lines, 0, 6, 2, 5, "m", "C")
if len(got) != 3 {
t.Fatalf("got %d diags, want 3 (one per covered line)", len(got))
}
// start line: from col 6 to end (10); middle: full; end: 0..5
if got[0].StartCol != 6 || got[0].EndCol != 10 {
t.Errorf("start line range [%d,%d), want [6,10)", got[0].StartCol, got[0].EndCol)
}
if got[1].StartCol != 0 || got[1].EndCol != 6 {
t.Errorf("middle line range [%d,%d), want [0,6)", got[1].StartCol, got[1].EndCol)
}
if got[2].StartCol != 0 || got[2].EndCol != 5 {
t.Errorf("end line range [%d,%d), want [0,5)", got[2].StartCol, got[2].EndCol)
}
}
func TestLSPRangeToDiagsSkipsOutOfRangeAndEmpty(t *testing.T) {
lines := []string{"only line"}
// Line 5 doesn't exist -> skipped, no panic.
if got := lspRangeToDiags(lines, 5, 0, 5, 3, "m", "C"); got != nil {
t.Errorf("out-of-range line produced %v, want nil", got)
}
// Zero-width range -> no diag.
if got := lspRangeToDiags(lines, 0, 2, 0, 2, "m", "C"); got != nil {
t.Errorf("zero-width range produced %v, want nil", got)
}
}
func TestCodeString(t *testing.T) {
if got := codeString("RepeatedWords"); got != "RepeatedWords" {
t.Errorf("string code = %q", got)
}
if got := codeString(float64(42)); got != "42" {
t.Errorf("number code = %q, want 42", got)
}
if got := codeString(nil); got != "" {
t.Errorf("nil code = %q, want empty", got)
}
}
// TestLiveHarper exercises the real harper-ls end to end. Skipped when the binary
// is absent or under -short, so CI without harper stays green.
func TestLiveHarper(t *testing.T) {
if testing.Short() {
t.Skip("skipping live harper test under -short")
}
if !Available() {
t.Skip("harper-ls not on PATH")
}
c, err := Start()
if err != nil {
t.Fatalf("Start: %v", err)
}
defer c.Close()
// "a a" is a repeated word; harper should flag it.
c.DidOpen("/tmp/glint-grammar-test.md", "This is a a test.\n")
select {
case batch := <-c.Diagnostics():
if len(batch) == 0 {
t.Fatal("harper returned an empty diagnostic batch")
}
found := false
for _, d := range batch {
if d.Line == 0 && d.Message != "" {
found = true
}
}
if !found {
t.Errorf("no usable diagnostic in batch: %+v", batch)
}
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for harper diagnostics")
}
}
|