313 lines
7.9 KiB
Go
313 lines
7.9 KiB
Go
package lsproto
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/microsoft/typescript-go/internal/bundled"
|
|
"github.com/microsoft/typescript-go/internal/json"
|
|
|
|
"github.com/microsoft/typescript-go/internal/jsonrpc"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
)
|
|
|
|
type DocumentUri string // !!!
|
|
|
|
func (uri DocumentUri) FileName() string {
|
|
if bundled.IsBundled(string(uri)) {
|
|
return string(uri)
|
|
}
|
|
if strings.HasPrefix(string(uri), "file://") {
|
|
parsed, err := url.Parse(string(uri))
|
|
if err != nil {
|
|
panic(fmt.Sprintf("invalid file URI: %s", uri))
|
|
}
|
|
if parsed.Host != "" {
|
|
return "//" + parsed.Host + parsed.Path
|
|
}
|
|
return fixWindowsURIPath(parsed.Path)
|
|
}
|
|
|
|
// Leave all other URIs escaped so we can round-trip them.
|
|
|
|
scheme, path, ok := strings.Cut(string(uri), ":")
|
|
if !ok {
|
|
panic(fmt.Sprintf("invalid URI: %s", uri))
|
|
}
|
|
|
|
authority := "ts-nul-authority"
|
|
if rest, ok := strings.CutPrefix(path, "//"); ok {
|
|
authority, path, ok = strings.Cut(rest, "/")
|
|
if !ok {
|
|
panic(fmt.Sprintf("invalid URI: %s", uri))
|
|
}
|
|
}
|
|
|
|
return "^/" + scheme + "/" + authority + "/" + path
|
|
}
|
|
|
|
func (uri DocumentUri) Path(useCaseSensitiveFileNames bool) tspath.Path {
|
|
fileName := uri.FileName()
|
|
return tspath.ToPath(fileName, "", useCaseSensitiveFileNames)
|
|
}
|
|
|
|
func fixWindowsURIPath(path string) string {
|
|
if rest, ok := strings.CutPrefix(path, "/"); ok {
|
|
if volume, rest, ok := tspath.SplitVolumePath(rest); ok {
|
|
return volume + rest
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
|
|
type HasTextDocumentURI interface {
|
|
TextDocumentURI() DocumentUri
|
|
}
|
|
|
|
type HasTextDocumentPosition interface {
|
|
HasTextDocumentURI
|
|
TextDocumentPosition() Position
|
|
}
|
|
|
|
type HasLocations interface {
|
|
GetLocations() *[]Location
|
|
}
|
|
|
|
type HasLocation interface {
|
|
GetLocation() Location
|
|
}
|
|
|
|
type URI string // !!!
|
|
|
|
type Method string
|
|
|
|
func errNotObject(k json.Kind) error {
|
|
return fmt.Errorf("expected object start, but encountered %v", k)
|
|
}
|
|
|
|
func errNull(field string) error {
|
|
return fmt.Errorf("null value is not allowed for field %q", field)
|
|
}
|
|
|
|
func errMissing(props []string) error {
|
|
return fmt.Errorf("missing required properties: %s", strings.Join(props, ", "))
|
|
}
|
|
|
|
func errInvalidKind(typeName string, got json.Kind) error {
|
|
return fmt.Errorf("invalid %s: got %v", typeName, got)
|
|
}
|
|
|
|
func errInvalidValue(typeName string, data []byte) error {
|
|
return fmt.Errorf("invalid %s: %s", typeName, data)
|
|
}
|
|
|
|
func errLiteralMismatch(typeName string, expected string, got []byte) error {
|
|
return fmt.Errorf("expected %s value %s, got %s", typeName, expected, got)
|
|
}
|
|
|
|
func assertOnlyOne(message string, count int) {
|
|
if count != 1 {
|
|
panic(message)
|
|
}
|
|
}
|
|
|
|
func assertAtMostOne(message string, count int) {
|
|
if count > 1 {
|
|
panic(message)
|
|
}
|
|
}
|
|
|
|
// jsonKeyCheck compares a raw JSON key token (including quotes) against a Go string.
|
|
func jsonKeyCheck(name []byte, key string) bool {
|
|
return len(name) == len(key)+2 && name[0] == '"' && string(name[1:len(name)-1]) == key
|
|
}
|
|
|
|
// jsonObjectRawField scans the top-level keys of a JSON object looking for the
|
|
// given field name, and returns its raw JSON value (e.g. `"full"` with quotes).
|
|
// Returns nil if the field is not found.
|
|
func jsonObjectRawField(data []byte, field string) json.Value {
|
|
dec := json.NewDecoder(bytes.NewBuffer(data))
|
|
if dec.PeekKind() != '{' {
|
|
return nil
|
|
}
|
|
if _, err := dec.ReadToken(); err != nil {
|
|
return nil
|
|
}
|
|
for dec.PeekKind() != '}' {
|
|
name, err := dec.ReadValue()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if jsonKeyCheck(name, field) {
|
|
val, err := dec.ReadValue()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return val
|
|
}
|
|
if err := dec.SkipValue(); err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jsonObjectHasKey scans the top-level keys of a JSON object looking for any of the
|
|
// given keys. Returns the index of the first key found, or -1 if none match.
|
|
// Bails early on first match without decoding any values.
|
|
func jsonObjectHasKey(data []byte, keys ...string) int {
|
|
dec := json.NewDecoder(bytes.NewBuffer(data))
|
|
if dec.PeekKind() != '{' {
|
|
return -1
|
|
}
|
|
if _, err := dec.ReadToken(); err != nil {
|
|
return -1
|
|
}
|
|
for dec.PeekKind() != '}' {
|
|
name, err := dec.ReadValue()
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
for i, key := range keys {
|
|
if jsonKeyCheck(name, key) {
|
|
return i
|
|
}
|
|
}
|
|
if err := dec.SkipValue(); err != nil {
|
|
return -1
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Inspired by https://www.youtube.com/watch?v=dab3I-HcTVk
|
|
|
|
type RequestInfo[Params, Resp any] struct {
|
|
_ [0]Params
|
|
_ [0]Resp
|
|
Method Method
|
|
}
|
|
|
|
func (info RequestInfo[Params, Resp]) UnmarshalResult(result any) (Resp, error) {
|
|
raw, ok := result.(json.Value)
|
|
if !ok {
|
|
return *new(Resp), fmt.Errorf("expected json.Value, got %T", result)
|
|
}
|
|
|
|
var r Resp
|
|
if err := json.Unmarshal(raw, &r); err != nil {
|
|
return *new(Resp), err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (info RequestInfo[Params, Resp]) NewRequestMessage(id *jsonrpc.ID, params Params) *RequestMessage {
|
|
return &RequestMessage{
|
|
ID: id,
|
|
Method: info.Method,
|
|
Params: params,
|
|
}
|
|
}
|
|
|
|
type NotificationInfo[Params any] struct {
|
|
_ [0]Params
|
|
Method Method
|
|
}
|
|
|
|
func (info NotificationInfo[Params]) NewNotificationMessage(params Params) *RequestMessage {
|
|
return &RequestMessage{
|
|
Method: info.Method,
|
|
Params: params,
|
|
}
|
|
}
|
|
|
|
// UnmarshalParams decodes the params of an inbound request or notification
|
|
// message into the requested type. Inbound messages store their params as a
|
|
// raw [json.Value] (see [Message.UnmarshalJSON]); decoding is deferred to the
|
|
// point of dispatch so that param types for methods the server never handles
|
|
// are not forced into the binary.
|
|
//
|
|
// A [NoParams] method must be given no params; every other method must be given
|
|
// params as an object or array. A violation returns [ErrorCodeInvalidParams].
|
|
func UnmarshalParams[T any](req *RequestMessage) (T, error) {
|
|
var params T
|
|
var raw json.Value
|
|
if req.Params != nil {
|
|
v, ok := req.Params.(json.Value)
|
|
if !ok {
|
|
return params, fmt.Errorf("%w: unexpected params type %T", ErrorCodeInvalidParams, req.Params)
|
|
}
|
|
raw = v
|
|
}
|
|
|
|
// params is the zero value of T; this asserts on its type, i.e. whether the
|
|
// method was declared with NoParams.
|
|
if _, declaresNoParams := any(params).(NoParams); declaresNoParams {
|
|
if len(raw) != 0 {
|
|
return params, fmt.Errorf("%w: expected no params, got %s", ErrorCodeInvalidParams, raw)
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
// The base protocol defines params as `array | object`; reject anything else
|
|
// (absent, null, or a scalar).
|
|
if k := raw.Kind(); k != '{' && k != '[' {
|
|
return params, fmt.Errorf("%w: params must be an object or array", ErrorCodeInvalidParams)
|
|
}
|
|
if err := json.Unmarshal(raw, ¶ms); err != nil {
|
|
return params, fmt.Errorf("%w: %w", ErrorCodeInvalidParams, err)
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
type Null struct{}
|
|
|
|
func (Null) UnmarshalJSONFrom(dec *json.Decoder) error {
|
|
data, err := dec.ReadValue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if string(data) != "null" {
|
|
return fmt.Errorf("expected null, got %s", data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (Null) MarshalJSONTo(enc *json.Encoder) error {
|
|
return enc.WriteToken(json.Null)
|
|
}
|
|
|
|
type NoParams struct{}
|
|
|
|
func (NoParams) IsZero() bool { return true }
|
|
|
|
type clientCapabilitiesKey struct{}
|
|
|
|
func WithClientCapabilities(ctx context.Context, caps *ResolvedClientCapabilities) context.Context {
|
|
return context.WithValue(ctx, clientCapabilitiesKey{}, caps)
|
|
}
|
|
|
|
func GetClientCapabilities(ctx context.Context) *ResolvedClientCapabilities {
|
|
if caps, _ := ctx.Value(clientCapabilitiesKey{}).(*ResolvedClientCapabilities); caps != nil {
|
|
return caps
|
|
}
|
|
return &ResolvedClientCapabilities{}
|
|
}
|
|
|
|
// PreferredMarkupKind returns the first (most preferred) markup kind from the given formats,
|
|
// or MarkupKindPlainText if the slice is empty.
|
|
func PreferredMarkupKind(formats []MarkupKind) MarkupKind {
|
|
if len(formats) > 0 {
|
|
return formats[0]
|
|
}
|
|
return MarkupKindPlainText
|
|
}
|
|
|
|
const (
|
|
CodeActionKindSourceRemoveUnusedImports CodeActionKind = "source.removeUnusedImports"
|
|
CodeActionKindSourceSortImports CodeActionKind = "source.sortImports"
|
|
)
|