206 lines
4.9 KiB
Go
206 lines
4.9 KiB
Go
// Package lsp is a minimal Language Server Protocol client: it spawns a language
|
|
// server per language on demand, syncs queried documents from disk, and adapts a
|
|
// few read-only capabilities (definition, references, hover, diagnostics) to the
|
|
// tool.Tool interface. Servers are not bundled — they resolve on PATH and a clear
|
|
// install hint is returned when one is missing.
|
|
package lsp
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type rpcError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (e *rpcError) Error() string { return fmt.Sprintf("lsp error %d: %s", e.Code, e.Message) }
|
|
|
|
type outMsg struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID *int64 `json:"id,omitempty"`
|
|
Method string `json:"method,omitempty"`
|
|
Params any `json:"params,omitempty"`
|
|
Result any `json:"result,omitempty"`
|
|
}
|
|
|
|
type inMsg struct {
|
|
ID *int64 `json:"id"`
|
|
Method string `json:"method"`
|
|
Result json.RawMessage `json:"result"`
|
|
Error *rpcError `json:"error"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
|
|
// conn speaks LSP framing (Content-Length headers) over a subprocess's
|
|
// stdin/stdout. A single read-pump goroutine demultiplexes the stream:
|
|
// id-bearing responses wake the matching call; method-only messages are
|
|
// notifications (diagnostics); id+method messages are server→client requests
|
|
// that must be answered or some servers stall during initialize.
|
|
type conn struct {
|
|
w io.Writer
|
|
writeMu sync.Mutex
|
|
|
|
mu sync.Mutex
|
|
nextID int64
|
|
pending map[int64]chan inMsg
|
|
|
|
onNotify func(method string, params json.RawMessage)
|
|
onRequest func(id int64, method string, params json.RawMessage)
|
|
|
|
closeOnce sync.Once
|
|
closed chan struct{}
|
|
err error
|
|
}
|
|
|
|
func newConn(w io.Writer, r io.Reader,
|
|
onNotify func(string, json.RawMessage),
|
|
onRequest func(int64, string, json.RawMessage)) *conn {
|
|
c := &conn{
|
|
w: w,
|
|
pending: map[int64]chan inMsg{},
|
|
onNotify: onNotify,
|
|
onRequest: onRequest,
|
|
closed: make(chan struct{}),
|
|
}
|
|
go c.readLoop(r)
|
|
return c
|
|
}
|
|
|
|
func (c *conn) call(ctx context.Context, method string, params any) (json.RawMessage, error) {
|
|
c.mu.Lock()
|
|
c.nextID++
|
|
id := c.nextID
|
|
ch := make(chan inMsg, 1)
|
|
c.pending[id] = ch
|
|
c.mu.Unlock()
|
|
defer func() {
|
|
c.mu.Lock()
|
|
delete(c.pending, id)
|
|
c.mu.Unlock()
|
|
}()
|
|
|
|
if err := c.writeMsg(outMsg{JSONRPC: "2.0", ID: &id, Method: method, Params: params}); err != nil {
|
|
return nil, err
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-c.closed:
|
|
return nil, c.err
|
|
case m := <-ch:
|
|
if m.Error != nil {
|
|
return nil, m.Error
|
|
}
|
|
return m.Result, nil
|
|
}
|
|
}
|
|
|
|
func (c *conn) notify(method string, params any) error {
|
|
return c.writeMsg(outMsg{JSONRPC: "2.0", Method: method, Params: params})
|
|
}
|
|
|
|
func (c *conn) reply(id int64, result any) error {
|
|
return c.writeMsg(outMsg{JSONRPC: "2.0", ID: &id, Result: result})
|
|
}
|
|
|
|
func (c *conn) writeMsg(v any) error {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.writeMu.Lock()
|
|
defer c.writeMu.Unlock()
|
|
if _, err := fmt.Fprintf(c.w, "Content-Length: %d\r\n\r\n", len(b)); err != nil {
|
|
return err
|
|
}
|
|
_, err = c.w.Write(b)
|
|
return err
|
|
}
|
|
|
|
func (c *conn) readLoop(r io.Reader) {
|
|
br := bufio.NewReader(r)
|
|
for {
|
|
body, err := readFrame(br)
|
|
if err != nil {
|
|
c.fail(err)
|
|
return
|
|
}
|
|
var m inMsg
|
|
if json.Unmarshal(body, &m) != nil {
|
|
continue
|
|
}
|
|
switch {
|
|
case m.Method != "" && m.ID != nil:
|
|
if c.onRequest != nil {
|
|
c.onRequest(*m.ID, m.Method, m.Params)
|
|
}
|
|
case m.Method != "":
|
|
if c.onNotify != nil {
|
|
c.onNotify(m.Method, m.Params)
|
|
}
|
|
case m.ID != nil:
|
|
c.mu.Lock()
|
|
ch := c.pending[*m.ID]
|
|
c.mu.Unlock()
|
|
if ch != nil {
|
|
ch <- m
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *conn) fail(err error) {
|
|
c.closeOnce.Do(func() {
|
|
c.err = err
|
|
close(c.closed)
|
|
})
|
|
}
|
|
|
|
// maxFrameBytes caps a single LSP message body. Generous for any real response
|
|
// (document symbols, semantic tokens for a huge file) while stopping a corrupt or
|
|
// desynced Content-Length from triggering an unbounded allocation that OOMs the
|
|
// whole process.
|
|
const maxFrameBytes = 64 << 20 // 64 MiB
|
|
|
|
// readFrame reads one LSP message: header lines terminated by a blank line, then
|
|
// exactly Content-Length bytes of JSON body.
|
|
func readFrame(r *bufio.Reader) ([]byte, error) {
|
|
n := -1
|
|
for {
|
|
line, err := r.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
line = strings.TrimRight(line, "\r\n")
|
|
if line == "" {
|
|
break
|
|
}
|
|
if v, ok := strings.CutPrefix(line, "Content-Length:"); ok {
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(v))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bad Content-Length %q: %w", v, err)
|
|
}
|
|
n = parsed
|
|
}
|
|
}
|
|
if n < 0 {
|
|
return nil, fmt.Errorf("missing Content-Length header")
|
|
}
|
|
if n > maxFrameBytes {
|
|
return nil, fmt.Errorf("Content-Length %d exceeds the %d-byte frame cap", n, maxFrameBytes)
|
|
}
|
|
buf := make([]byte, n)
|
|
if _, err := io.ReadFull(r, buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|