โ– humdrum codex / glint v1.1.2
license AGPL-3.0
1.8 KB raw
 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
}