// 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 }