1
0
Fork 0
mirror of https://github.com/laravel-ls/protocol.git synced 2026-06-16 03:54:56 +02:00

add inline hints

This commit is contained in:
Henrik Hautakoski 2026-03-19 14:18:29 +01:00
parent 7cebe3d818
commit 5fe36eb7d6
2 changed files with 365 additions and 0 deletions

207
inlay_hints.go Normal file
View file

@ -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")
}

158
inlay_hints_test.go Normal file
View file

@ -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, &params); 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))
}
}