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
|
// Package grammar drives an optional Harper grammar checker for glint. It speaks
// LSP (JSON-RPC over stdio) to a harper-ls subprocess, so glint stays pure-Go
// and zero-cgo: harper-core is Rust, but it lives in a separate process reached
// only through pipes. The feature is entirely optional โ with no harper-ls on
// PATH the package is inert (Available reports false) and glint behaves exactly
// as before.
package grammar
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
)
// writeFrame writes one LSP message: a Content-Length header, a blank line, then
// the JSON body. Callers serialize concurrent writes; this does no locking.
func writeFrame(w io.Writer, body []byte) error {
if _, err := fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(body)); err != nil {
return err
}
_, err := w.Write(body)
return err
}
// readFrame reads one LSP message body, parsing the Content-Length header and
// discarding any other headers up to the blank separator line. It returns
// io.EOF (possibly wrapped) once the stream closes.
func readFrame(r *bufio.Reader) ([]byte, error) {
length := -1
for {
line, err := r.ReadString('\n')
if err != nil {
return nil, err
}
trimmed := strings.TrimRight(line, "\r\n")
if trimmed == "" { // blank line: headers done
break
}
if name, val, ok := strings.Cut(trimmed, ":"); ok &&
strings.EqualFold(strings.TrimSpace(name), "Content-Length") {
n, err := strconv.Atoi(strings.TrimSpace(val))
if err != nil {
return nil, fmt.Errorf("grammar: bad Content-Length %q: %w", val, err)
}
length = n
}
}
if length < 0 {
return nil, fmt.Errorf("grammar: frame missing Content-Length")
}
body := make([]byte, length)
if _, err := io.ReadFull(r, body); err != nil {
return nil, err
}
return body, nil
}
|