Implemented inlay hints

This commit is contained in:
Martino Ferrari
2026-02-02 18:18:50 +01:00
parent ee9235c24d
commit 23ddbc0e91
3 changed files with 306 additions and 16 deletions

View File

@@ -409,6 +409,11 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
File: file, File: file,
IsVariable: true, IsVariable: true,
}) })
case *parser.BinaryExpression:
pt.indexValue(file, v.Left)
pt.indexValue(file, v.Right)
case *parser.UnaryExpression:
pt.indexValue(file, v.Right)
case *parser.ArrayValue: case *parser.ArrayValue:
for _, elem := range v.Elements { for _, elem := range v.Elements {
pt.indexValue(file, elem) pt.indexValue(file, elem)
@@ -644,7 +649,7 @@ func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableI
} }
curr = curr.Parent curr = curr.Parent
} }
if ctx == nil { if pt.Root != nil {
if v, ok := pt.Root.Variables[name]; ok { if v, ok := pt.Root.Variables[name]; ok {
return &v return &v
} }

View File

@@ -97,15 +97,30 @@ type TextDocumentContentChangeEvent struct {
Text string `json:"text"` Text string `json:"text"`
} }
type TextDocumentIdentifier struct {
URI string `json:"uri"`
}
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 HoverParams struct { type HoverParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"` TextDocument TextDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"` Position Position `json:"position"`
} }
type TextDocumentIdentifier struct {
URI string `json:"uri"`
}
type DefinitionParams struct { type DefinitionParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"` TextDocument TextDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"` Position Position `json:"position"`
@@ -121,19 +136,17 @@ type ReferenceContext struct {
IncludeDeclaration bool `json:"includeDeclaration"` IncludeDeclaration bool `json:"includeDeclaration"`
} }
type Location struct { type InlayHintParams struct {
URI string `json:"uri"` TextDocument TextDocumentIdentifier `json:"textDocument"`
Range Range `json:"range"` Range Range `json:"range"`
} }
type Range struct { type InlayHint struct {
Start Position `json:"start"` Position Position `json:"position"`
End Position `json:"end"` Label string `json:"label"`
} Kind int `json:"kind,omitempty"` // 1: Parameter, 2: Type
PaddingLeft bool `json:"paddingLeft,omitempty"`
type Position struct { PaddingRight bool `json:"paddingRight,omitempty"`
Line int `json:"line"`
Character int `json:"character"`
} }
type Hover struct { type Hover struct {
@@ -264,6 +277,7 @@ func HandleMessage(msg *JsonRpcMessage) {
"referencesProvider": true, "referencesProvider": true,
"documentFormattingProvider": true, "documentFormattingProvider": true,
"renameProvider": true, "renameProvider": true,
"inlayHintProvider": true,
"completionProvider": map[string]any{ "completionProvider": map[string]any{
"triggerCharacters": []string{"=", " ", "@"}, "triggerCharacters": []string{"=", " ", "@"},
}, },
@@ -325,6 +339,11 @@ func HandleMessage(msg *JsonRpcMessage) {
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, HandleRename(params)) respond(msg.ID, HandleRename(params))
} }
case "textDocument/inlayHint":
var params InlayHintParams
if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, HandleInlayHint(params))
}
} }
} }
@@ -1946,3 +1965,161 @@ func computeUnary(op parser.Token, val parser.Value) parser.Value {
} }
return val return val
} }
func isComplexValue(val parser.Value) bool {
switch val.(type) {
case *parser.BinaryExpression, *parser.UnaryExpression, *parser.VariableReferenceValue:
return true
}
return false
}
func HandleInlayHint(params InlayHintParams) []InlayHint {
path := uriToPath(params.TextDocument.URI)
var hints []InlayHint
seenPositions := make(map[Position]bool)
addHint := func(h InlayHint) {
if !seenPositions[h.Position] {
hints = append(hints, h)
seenPositions[h.Position] = true
}
}
Tree.Walk(func(node *index.ProjectNode) {
for _, frag := range node.Fragments {
if frag.File != path {
continue
}
// Signal Name Hint (::TYPE[SIZE])
if node.Parent != nil && (node.Parent.Name == "InputSignals" || node.Parent.Name == "OutputSignals") {
typ := getEvaluatedMetadata(node, "Type")
elems := getEvaluatedMetadata(node, "NumberOfElements")
dims := getEvaluatedMetadata(node, "NumberOfDimensions")
if typ == "" && node.Target != nil {
typ = node.Target.Metadata["Type"]
if elems == "" {
elems = node.Target.Metadata["NumberOfElements"]
}
if dims == "" {
dims = node.Target.Metadata["NumberOfDimensions"]
}
}
if typ != "" {
if elems == "" {
elems = "1"
}
if dims == "" {
dims = "1"
}
label := fmt.Sprintf("::%s[%sx%s]", typ, elems, dims)
pos := frag.ObjectPos
addHint(InlayHint{
Position: Position{Line: pos.Line - 1, Character: pos.Column - 1 + len(node.RealName)},
Label: label,
Kind: 2, // Type
})
}
}
// Field-based hints (DataSource class and Expression evaluation)
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
// DataSource Class Hint
if f.Name == "DataSource" && (node.Parent != nil && (node.Parent.Name == "InputSignals" || node.Parent.Name == "OutputSignals")) {
dsName := valueToString(f.Value, node)
dsNode := Tree.ResolveName(node, dsName, isDataSource)
if dsNode != nil {
cls := dsNode.Metadata["Class"]
if cls != "" {
addHint(InlayHint{
Position: Position{Line: f.Position.Line - 1, Character: f.Position.Column - 1 + len(f.Name) + 3}, // "DataSource = "
Label: cls + "::",
Kind: 1, // Parameter
})
}
}
}
// Expression Evaluation Hint
if isComplexValue(f.Value) {
res := valueToString(f.Value, node)
if res != "" {
uri := params.TextDocument.URI
text, ok := Documents[uri]
if ok {
lines := strings.Split(text, "\n")
lineIdx := f.Position.Line - 1
if lineIdx >= 0 && lineIdx < len(lines) {
line := lines[lineIdx]
addHint(InlayHint{
Position: Position{Line: lineIdx, Character: len(line)},
Label: " => " + res,
Kind: 2, // Type/Value
})
}
}
}
}
} else if v, ok := def.(*parser.VariableDefinition); ok {
// Expression Evaluation Hint for #let/#var
if v.DefaultValue != nil && isComplexValue(v.DefaultValue) {
res := valueToString(v.DefaultValue, node)
if res != "" {
uri := params.TextDocument.URI
text, ok := Documents[uri]
if ok {
lines := strings.Split(text, "\n")
lineIdx := v.Position.Line - 1
if lineIdx >= 0 && lineIdx < len(lines) {
line := lines[lineIdx]
addHint(InlayHint{
Position: Position{Line: lineIdx, Character: len(line)},
Label: " => " + res,
Kind: 2,
})
}
}
}
}
}
}
}
})
// Add logic for general object references
for _, ref := range Tree.References {
if ref.File != path {
continue
}
if ref.Target != nil {
cls := ref.Target.Metadata["Class"]
if cls != "" {
addHint(InlayHint{
Position: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
Label: cls + "::",
Kind: 1, // Parameter
})
}
} else if ref.IsVariable {
// Variable reference evaluation hint: @VAR(=> VALUE)
container := Tree.GetNodeContaining(ref.File, ref.Position)
if info := Tree.ResolveVariable(container, ref.Name); info != nil && info.Def.DefaultValue != nil {
val := valueToString(info.Def.DefaultValue, container)
if val != "" {
addHint(InlayHint{
Position: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name) + 1},
Label: "(=> " + val + ")",
Kind: 2,
})
}
}
}
}
return hints
}

108
test/lsp_inlay_hint_test.go Normal file
View File

@@ -0,0 +1,108 @@
package integration
import (
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/lsp"
"github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-community/marte-dev-tools/internal/validator"
)
func TestLSPInlayHint(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
#let N : int= 10 + 5
+DS = {
Class = FileReader
Signals = {
Sig1 = { Type = uint32 NumberOfElements = 10 }
}
}
+GAM = {
Class = IOGAM
Expr = 10 + 20
InputSignals = {
Sig1 = { DataSource = DS }
}
}
+Other = {
Class = Controller
Ref = DS
VarRef = @N + 1
}
`
uri := "file://inlay.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, _ := p.Parse()
lsp.Tree.AddFile("inlay.marte", cfg)
lsp.Tree.ResolveReferences()
v := validator.NewValidator(lsp.Tree, ".")
v.ValidateProject()
params := lsp.InlayHintParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Range: lsp.Range{
Start: lsp.Position{Line: 0, Character: 0},
End: lsp.Position{Line: 20, Character: 0},
},
}
res := lsp.HandleInlayHint(params)
if len(res) == 0 {
t.Fatal("Expected inlay hints, got 0")
}
foundTypeHint := false
foundDSClassHint := false
foundGeneralRefHint := false
foundExprHint := false
foundVarRefHint := false
foundLetHint := false
for _, hint := range res {
t.Logf("Hint: '%s' at Line %d, Col %d", hint.Label, hint.Position.Line, hint.Position.Character)
if hint.Label == "::uint32[10x1]" {
foundTypeHint = true
}
if hint.Label == "FileReader::" && hint.Position.Line == 12 { // Sig1 line (DS)
foundDSClassHint = true
}
if hint.Label == "FileReader::" && hint.Position.Line == 17 { // Ref = DS line
foundGeneralRefHint = true
}
if hint.Label == " => 30" {
foundExprHint = true
}
if hint.Label == "(=> 15)" {
foundVarRefHint = true
}
if hint.Label == " => 15" && hint.Position.Line == 1 { // #let N line
foundLetHint = true
}
}
if !foundTypeHint {
t.Error("Did not find signal type/size hint")
}
if !foundDSClassHint {
t.Error("Did not find DataSource class hint")
}
if !foundGeneralRefHint {
t.Error("Did not find general object reference hint")
}
if !foundExprHint {
t.Error("Did not find expression evaluation hint")
}
if !foundVarRefHint {
t.Error("Did not find variable reference evaluation hint")
}
if !foundLetHint {
t.Error("Did not find #let expression evaluation hint")
}
}