Add the standard-library language server, editor-neutral protocol contract, additive version feature discovery, and bounded security regressions while leaving the Sando runtime unchanged. Material design and implementation assistance was provided by OpenAI Codex. Signed-off-by: Cole Speelman <crspeelman@gmail.com>
175 lines
3.9 KiB
Go
175 lines
3.9 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package lsp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
maxMessageBytes = 16 << 20
|
|
maxHeaderBytes = 64 << 10
|
|
maxHeaderLines = 64
|
|
)
|
|
|
|
type rpcMessage struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id,omitempty"`
|
|
Method string `json:"method,omitempty"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
Result any `json:"result,omitempty"`
|
|
Error *rpcError `json:"error,omitempty"`
|
|
}
|
|
|
|
type rpcError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func validRequestID(id json.RawMessage) bool {
|
|
if len(id) == 0 {
|
|
return true
|
|
}
|
|
var value any
|
|
if err := json.Unmarshal(id, &value); err != nil {
|
|
return false
|
|
}
|
|
switch value := value.(type) {
|
|
case string:
|
|
return true
|
|
case float64:
|
|
return value == math.Trunc(value)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
const (
|
|
errParse = -32700
|
|
errInvalidRequest = -32600
|
|
errMethodNotFound = -32601
|
|
errInvalidParams = -32602
|
|
errInternal = -32603
|
|
errRequestCancelled = -32800
|
|
)
|
|
|
|
type frameReader struct{ reader *bufio.Reader }
|
|
|
|
func newFrameReader(input io.Reader) *frameReader {
|
|
return &frameReader{reader: bufio.NewReaderSize(input, 64<<10)}
|
|
}
|
|
|
|
func (reader *frameReader) read() ([]byte, error) {
|
|
contentLength := -1
|
|
headerBytes := 0
|
|
headerLines := 0
|
|
for {
|
|
line, err := reader.reader.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headerBytes += len(line)
|
|
headerLines++
|
|
if headerBytes > maxHeaderBytes || headerLines > maxHeaderLines {
|
|
return nil, errors.New("LSP headers exceed configured limits")
|
|
}
|
|
if len(line) > 8<<10 {
|
|
return nil, errors.New("LSP header line exceeds 8 KiB")
|
|
}
|
|
line = strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r")
|
|
if line == "" {
|
|
break
|
|
}
|
|
name, value, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
return nil, errors.New("malformed LSP header")
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(name), "Content-Length") {
|
|
if contentLength >= 0 {
|
|
return nil, errors.New("duplicate Content-Length header")
|
|
}
|
|
parsed, parseErr := strconv.Atoi(strings.TrimSpace(value))
|
|
if parseErr != nil || parsed < 0 || parsed > maxMessageBytes {
|
|
return nil, errors.New("invalid or excessive Content-Length")
|
|
}
|
|
contentLength = parsed
|
|
}
|
|
}
|
|
if contentLength < 0 {
|
|
return nil, errors.New("missing Content-Length header")
|
|
}
|
|
payload := make([]byte, contentLength)
|
|
if _, err := io.ReadFull(reader.reader, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
type frameWriter struct {
|
|
mu sync.Mutex
|
|
output io.Writer
|
|
}
|
|
|
|
func (writer *frameWriter) write(message rpcMessage) error {
|
|
payload, err := json.Marshal(message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var frame bytes.Buffer
|
|
fmt.Fprintf(&frame, "Content-Length: %d\r\n\r\n", len(payload))
|
|
frame.Write(payload)
|
|
writer.mu.Lock()
|
|
defer writer.mu.Unlock()
|
|
written, err := writer.output.Write(frame.Bytes())
|
|
if err == nil && written != frame.Len() {
|
|
return io.ErrShortWrite
|
|
}
|
|
return err
|
|
}
|
|
|
|
type Position struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
}
|
|
|
|
type Range struct {
|
|
Start Position `json:"start"`
|
|
End Position `json:"end"`
|
|
}
|
|
|
|
type Location struct {
|
|
URI string `json:"uri"`
|
|
Range Range `json:"range"`
|
|
}
|
|
|
|
type lspDiagnostic struct {
|
|
Range Range `json:"range"`
|
|
Severity int `json:"severity,omitempty"`
|
|
Code string `json:"code,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type textDocumentIdentifier struct {
|
|
URI string `json:"uri"`
|
|
}
|
|
|
|
type versionedTextDocumentIdentifier struct {
|
|
URI string `json:"uri"`
|
|
Version int `json:"version"`
|
|
}
|
|
|
|
type textDocumentPositionParams struct {
|
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
|
Position Position `json:"position"`
|
|
}
|