Implemented inlay hints
This commit is contained in:
@@ -409,6 +409,11 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
|
||||
File: file,
|
||||
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:
|
||||
for _, elem := range v.Elements {
|
||||
pt.indexValue(file, elem)
|
||||
@@ -644,7 +649,7 @@ func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableI
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
if ctx == nil {
|
||||
if pt.Root != nil {
|
||||
if v, ok := pt.Root.Variables[name]; ok {
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -97,15 +97,30 @@ type TextDocumentContentChangeEvent struct {
|
||||
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 {
|
||||
TextDocument TextDocumentIdentifier `json:"textDocument"`
|
||||
Position Position `json:"position"`
|
||||
}
|
||||
|
||||
type TextDocumentIdentifier struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type DefinitionParams struct {
|
||||
TextDocument TextDocumentIdentifier `json:"textDocument"`
|
||||
Position Position `json:"position"`
|
||||
@@ -121,19 +136,17 @@ type ReferenceContext struct {
|
||||
IncludeDeclaration bool `json:"includeDeclaration"`
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
URI string `json:"uri"`
|
||||
type InlayHintParams struct {
|
||||
TextDocument TextDocumentIdentifier `json:"textDocument"`
|
||||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Start Position `json:"start"`
|
||||
End Position `json:"end"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
type InlayHint struct {
|
||||
Position Position `json:"position"`
|
||||
Label string `json:"label"`
|
||||
Kind int `json:"kind,omitempty"` // 1: Parameter, 2: Type
|
||||
PaddingLeft bool `json:"paddingLeft,omitempty"`
|
||||
PaddingRight bool `json:"paddingRight,omitempty"`
|
||||
}
|
||||
|
||||
type Hover struct {
|
||||
@@ -264,6 +277,7 @@ func HandleMessage(msg *JsonRpcMessage) {
|
||||
"referencesProvider": true,
|
||||
"documentFormattingProvider": true,
|
||||
"renameProvider": true,
|
||||
"inlayHintProvider": true,
|
||||
"completionProvider": map[string]any{
|
||||
"triggerCharacters": []string{"=", " ", "@"},
|
||||
},
|
||||
@@ -325,6 +339,11 @@ func HandleMessage(msg *JsonRpcMessage) {
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, HandleRename(params))
|
||||
}
|
||||
case "textDocument/inlayHint":
|
||||
var params InlayHintParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, HandleInlayHint(params))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1946,3 +1965,161 @@ func computeUnary(op parser.Token, val parser.Value) parser.Value {
|
||||
}
|
||||
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
108
test/lsp_inlay_hint_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user