diff --git a/inlay_hints.go b/inlay_hints.go new file mode 100644 index 0000000..a294657 --- /dev/null +++ b/inlay_hints.go @@ -0,0 +1,207 @@ +package protocol + +import ( + "encoding/json" + "errors" +) + +const ( + // MethodTextDocumentInlayHint method name of `textDocument/inlayHint`. + MethodTextDocumentInlayHint = "textDocument/inlayHint" +) + +// InlayHintParams - Parameters for a `textDocument/inlayHint` request. +// +// @since 3.17.0 +// +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams +type InlayHintParams struct { + WorkDoneProgressParams + PartialResultParams + + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + + // The visible range for which inlay hints should be computed. + Range Range `json:"range"` +} + +// InlayHintKind - The kind of an inlay hint. +// +// @since 3.17.0 +// +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintKind +type InlayHintKind int + +const ( + // InlayHintKindType - An inlay hint that shows a type annotation. + InlayHintKindType InlayHintKind = 1 + + // InlayHintKindParameter - An inlay hint that shows a parameter name. + InlayHintKindParameter InlayHintKind = 2 +) + +// InlayHintTooltip can be a plain string or a MarkupContent object. +// +// @since 3.17.0 +type InlayHintTooltip struct { + String *string + MarkupContent *MarkupContent +} + +func (t InlayHintTooltip) MarshalJSON() ([]byte, error) { + if t.String != nil { + return json.Marshal(*t.String) + } + if t.MarkupContent != nil { + return json.Marshal(t.MarkupContent) + } + return []byte("null"), nil +} + +func (t *InlayHintTooltip) UnmarshalJSON(data []byte) error { + *t = InlayHintTooltip{} + + if string(data) == "null" { + return nil + } + + var str string + if err := json.Unmarshal(data, &str); err == nil { + t.String = &str + return nil + } + + var markup MarkupContent + if err := json.Unmarshal(data, &markup); err == nil && markup.Kind != "" { + t.MarkupContent = &markup + return nil + } + + return errors.New("invalid InlayHintTooltip: not string, MarkupContent, or null") +} + +// InlayHintLabelPart - A segment of an inlay hint label. +// +// @since 3.17.0 +// +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintLabelPart +type InlayHintLabelPart struct { + // The mandatory label value. + Value string `json:"value"` + + // The tooltip text or markup shown when hovering this label part. + Tooltip *InlayHintTooltip `json:"tooltip,omitempty"` + + // A source location for this label part. + Location *Location `json:"location,omitempty"` + + // A command associated with this label part. + Command *Command `json:"command,omitempty"` +} + +// InlayHintLabel can be either a string or a list of label parts. +// +// @since 3.17.0 +type InlayHintLabel struct { + String *string + Parts []InlayHintLabelPart +} + +func (l InlayHintLabel) MarshalJSON() ([]byte, error) { + if l.String != nil { + return json.Marshal(*l.String) + } + if l.Parts != nil { + return json.Marshal(l.Parts) + } + return nil, errors.New("one of InlayHintLabel.String or InlayHintLabel.Parts needs to be set") +} + +func (l *InlayHintLabel) UnmarshalJSON(data []byte) error { + *l = InlayHintLabel{} + + var str string + if err := json.Unmarshal(data, &str); err == nil { + l.String = &str + return nil + } + + var parts []InlayHintLabelPart + if err := json.Unmarshal(data, &parts); err == nil { + l.Parts = parts + return nil + } + + return errors.New("invalid InlayHintLabel: not string or []InlayHintLabelPart") +} + +// InlayHint represents an inlay hint item. +// +// @since 3.17.0 +// +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHint +type InlayHint struct { + // The position of this hint. + Position Position `json:"position"` + + // The label of this hint. A human readable string or an array of label parts. + Label InlayHintLabel `json:"label"` + + // The kind of this hint. + Kind *InlayHintKind `json:"kind,omitempty"` + + // Optional text edits that are performed when accepting this hint. + TextEdits []TextEdit `json:"textEdits,omitempty"` + + // The tooltip text when hovering over this hint. + Tooltip *InlayHintTooltip `json:"tooltip,omitempty"` + + // Render padding before this hint. + PaddingLeft *bool `json:"paddingLeft,omitempty"` + + // Render padding after this hint. + PaddingRight *bool `json:"paddingRight,omitempty"` + + // A data entry field preserved between a hint request and resolve request. + Data LSPAny `json:"data,omitempty"` +} + +// InlayHintResponse - Result for a `textDocument/inlayHint` request. +// +// It is either an array of `InlayHint` or `null`. +// +// @since 3.17.0 +// +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHint +type InlayHintResponse struct { + Hints []InlayHint + Null bool +} + +func (r InlayHintResponse) MarshalJSON() ([]byte, error) { + if r.Null { + return []byte("null"), nil + } + if r.Hints == nil { + return []byte("null"), nil + } + return json.Marshal(r.Hints) +} + +func (r *InlayHintResponse) UnmarshalJSON(data []byte) error { + *r = InlayHintResponse{} + + if string(data) == "null" { + r.Null = true + return nil + } + + var hints []InlayHint + if err := json.Unmarshal(data, &hints); err == nil { + r.Hints = hints + return nil + } + + return errors.New("invalid inlay hint response: not null or []InlayHint") +} diff --git a/inlay_hints_test.go b/inlay_hints_test.go new file mode 100644 index 0000000..02b2fc8 --- /dev/null +++ b/inlay_hints_test.go @@ -0,0 +1,158 @@ +package protocol_test + +import ( + "encoding/json" + "testing" + + "github.com/laravel-ls/protocol" +) + +func Test_InlayHint_ParamsUnmarshalValidJSON(t *testing.T) { + data := []byte(`{"textDocument":{"uri":"file:///tmp/main.go"},"range":{"start":{"line":1,"character":2},"end":{"line":1,"character":8}}}`) + + var params protocol.InlayHintParams + if err := json.Unmarshal(data, ¶ms); err != nil { + t.Fatalf("unmarshal InlayHintParams failed: %v", err) + } + + if params.TextDocument.URI != "file:///tmp/main.go" { + t.Fatalf("unexpected textDocument URI: %q", params.TextDocument.URI) + } + + if params.Range.Start.Line != 1 || params.Range.End.Character != 8 { + t.Fatalf("unexpected range: %+v", params.Range) + } +} + +func Test_InlayHintLabel_UnmarshalString(t *testing.T) { + var label protocol.InlayHintLabel + if err := json.Unmarshal([]byte(`"x:"`), &label); err != nil { + t.Fatalf("unmarshal InlayHintLabel string failed: %v", err) + } + + if label.String == nil || *label.String != "x:" { + t.Fatalf("expected string label 'x:', got %+v", label) + } + + if label.Parts != nil { + t.Fatalf("expected parts to be nil for string label") + } +} + +func Test_InlayHintLabel_UnmarshalParts(t *testing.T) { + data := []byte(`[{"value":"name"},{"value":":","tooltip":"separator"}]`) + + var label protocol.InlayHintLabel + if err := json.Unmarshal(data, &label); err != nil { + t.Fatalf("unmarshal InlayHintLabel parts failed: %v", err) + } + + if len(label.Parts) != 2 { + t.Fatalf("expected 2 label parts, got %d", len(label.Parts)) + } + + if label.Parts[1].Tooltip == nil || label.Parts[1].Tooltip.String == nil || *label.Parts[1].Tooltip.String != "separator" { + t.Fatalf("expected second part tooltip string to be set, got %+v", label.Parts[1].Tooltip) + } +} + +func Test_InlayHintTooltip_UnmarshalMarkupContent(t *testing.T) { + data := []byte(`{"kind":"markdown","value":"**hint**"}`) + + var tooltip protocol.InlayHintTooltip + if err := json.Unmarshal(data, &tooltip); err != nil { + t.Fatalf("unmarshal InlayHintTooltip markup failed: %v", err) + } + + if tooltip.MarkupContent == nil || tooltip.MarkupContent.Kind != protocol.MarkupKindMarkdown { + t.Fatalf("expected markdown tooltip, got %+v", tooltip) + } +} + +func Test_InlayHint_UnmarshalTypedObject(t *testing.T) { + data := []byte(`{ + "position": {"line": 2, "character": 10}, + "label": [{"value":"x"},{"value":":","tooltip":{"kind":"plaintext","value":"type separator"}}], + "kind": 1, + "textEdits": [{"range":{"start":{"line":2,"character":8},"end":{"line":2,"character":9}},"newText":"value"}], + "tooltip": "inferred type", + "paddingLeft": true, + "paddingRight": false, + "data": {"id": 42} + }`) + + var hint protocol.InlayHint + if err := json.Unmarshal(data, &hint); err != nil { + t.Fatalf("unmarshal InlayHint failed: %v", err) + } + + if hint.Position.Line != 2 || hint.Position.Character != 10 { + t.Fatalf("unexpected position: %+v", hint.Position) + } + + if hint.Kind == nil || *hint.Kind != protocol.InlayHintKindType { + t.Fatalf("expected kind=InlayHintKindType, got %+v", hint.Kind) + } + + if hint.Tooltip == nil || hint.Tooltip.String == nil || *hint.Tooltip.String != "inferred type" { + t.Fatalf("expected tooltip string 'inferred type', got %+v", hint.Tooltip) + } + + if len(hint.TextEdits) != 1 { + t.Fatalf("expected 1 text edit, got %d", len(hint.TextEdits)) + } +} + +func Test_InlayHintResponse_UnmarshalArrayAndNull(t *testing.T) { + var list protocol.InlayHintResponse + if err := json.Unmarshal([]byte(`[{"position":{"line":0,"character":1},"label":"x:"}]`), &list); err != nil { + t.Fatalf("unmarshal InlayHintResponse array failed: %v", err) + } + + if list.Null { + t.Fatalf("expected non-null response for array") + } + + if len(list.Hints) != 1 { + t.Fatalf("expected 1 hint, got %d", len(list.Hints)) + } + + var nullRes protocol.InlayHintResponse + if err := json.Unmarshal([]byte(`null`), &nullRes); err != nil { + t.Fatalf("unmarshal InlayHintResponse null failed: %v", err) + } + + if !nullRes.Null { + t.Fatalf("expected null response flag to be true") + } +} + +func Test_InlayHintResponse_MarshalArrayAndNull(t *testing.T) { + response := protocol.InlayHintResponse{ + Hints: []protocol.InlayHint{{ + Position: protocol.Position{Line: 0, Character: 0}, + Label: func() protocol.InlayHintLabel { + label := "a" + return protocol.InlayHintLabel{String: &label} + }(), + }}, + } + + data, err := json.Marshal(response) + if err != nil { + t.Fatalf("marshal InlayHintResponse array failed: %v", err) + } + + if string(data) == "null" { + t.Fatalf("expected array JSON, got null") + } + + nullData, err := json.Marshal(protocol.InlayHintResponse{Null: true}) + if err != nil { + t.Fatalf("marshal InlayHintResponse null failed: %v", err) + } + + if string(nullData) != "null" { + t.Fatalf("expected null JSON, got %s", string(nullData)) + } +}