Added hover with expression and improved implicit signal referencing and validation
This commit is contained in:
@@ -576,6 +576,8 @@ func HandleHover(params HoverParams) *Hover {
|
||||
return nil
|
||||
}
|
||||
|
||||
container := Tree.GetNodeContaining(path, parser.Position{Line: line, Column: col})
|
||||
|
||||
var content string
|
||||
|
||||
if res.Node != nil {
|
||||
@@ -589,7 +591,7 @@ func HandleHover(params HoverParams) *Hover {
|
||||
} else if res.Variable != nil {
|
||||
content = fmt.Sprintf("**Variable**: `%s`\nType: `%s`", res.Variable.Name, res.Variable.TypeExpr)
|
||||
if res.Variable.DefaultValue != nil {
|
||||
content += fmt.Sprintf("\nDefault: `%s`", valueToString(res.Variable.DefaultValue))
|
||||
content += fmt.Sprintf("\nDefault: `%s`", valueToString(res.Variable.DefaultValue, container))
|
||||
}
|
||||
} else if res.Reference != nil {
|
||||
targetName := "Unresolved"
|
||||
@@ -605,7 +607,7 @@ func HandleHover(params HoverParams) *Hover {
|
||||
targetName = v.Name
|
||||
fullInfo = fmt.Sprintf("**Variable**: `@%s`\nType: `%s`", v.Name, v.TypeExpr)
|
||||
if v.DefaultValue != nil {
|
||||
fullInfo += fmt.Sprintf("\nDefault: `%s`", valueToString(v.DefaultValue))
|
||||
fullInfo += fmt.Sprintf("\nDefault: `%s`", valueToString(v.DefaultValue, container))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,7 +631,8 @@ func HandleHover(params HoverParams) *Hover {
|
||||
}
|
||||
}
|
||||
|
||||
func valueToString(val parser.Value) string {
|
||||
func valueToString(val parser.Value, ctx *index.ProjectNode) string {
|
||||
val = evaluate(val, ctx)
|
||||
switch v := val.(type) {
|
||||
case *parser.StringValue:
|
||||
if v.Quoted {
|
||||
@@ -649,7 +652,7 @@ func valueToString(val parser.Value) string {
|
||||
case *parser.ArrayValue:
|
||||
elements := []string{}
|
||||
for _, e := range v.Elements {
|
||||
elements = append(elements, valueToString(e))
|
||||
elements = append(elements, valueToString(e, ctx))
|
||||
}
|
||||
return fmt.Sprintf("{ %s }", strings.Join(elements, " "))
|
||||
default:
|
||||
@@ -1292,6 +1295,55 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
||||
info += fmt.Sprintf("\n\n%s", node.Doc)
|
||||
}
|
||||
|
||||
// Check if Implicit Signal peers exist
|
||||
if ds, _ := getSignalInfo(node); ds != nil {
|
||||
peers := findSignalPeers(node)
|
||||
|
||||
// 1. Explicit Definition Fields
|
||||
var defNode *index.ProjectNode
|
||||
for _, p := range peers {
|
||||
if p.Parent != nil && p.Parent.Name == "Signals" {
|
||||
defNode = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if defNode != nil {
|
||||
for _, frag := range defNode.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok {
|
||||
key := f.Name
|
||||
if key != "Type" && key != "NumberOfElements" && key != "NumberOfDimensions" && key != "Class" {
|
||||
val := valueToString(f.Value, defNode)
|
||||
info += fmt.Sprintf("\n**%s**: `%s`", key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extraInfo := ""
|
||||
for _, p := range peers {
|
||||
if (p.Parent.Name == "InputSignals" || p.Parent.Name == "OutputSignals") && isGAM(p.Parent.Parent) {
|
||||
gamName := p.Parent.Parent.RealName
|
||||
for _, frag := range p.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok {
|
||||
key := f.Name
|
||||
if key != "DataSource" && key != "Alias" && key != "Type" && key != "Class" && key != "NumberOfElements" && key != "NumberOfDimensions" {
|
||||
val := valueToString(f.Value, p)
|
||||
extraInfo += fmt.Sprintf("\n- **%s** (%s): `%s`", key, gamName, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if extraInfo != "" {
|
||||
info += "\n\n**Usage Details**:" + extraInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Find references
|
||||
var refs []string
|
||||
for _, ref := range Tree.References {
|
||||
@@ -1440,6 +1492,81 @@ func HandleRename(params RenameParams) *WorkspaceEdit {
|
||||
}
|
||||
|
||||
if targetNode != nil {
|
||||
// Special handling for Signals (Implicit/Explicit)
|
||||
if ds, _ := getSignalInfo(targetNode); ds != nil {
|
||||
peers := findSignalPeers(targetNode)
|
||||
seenPeers := make(map[*index.ProjectNode]bool)
|
||||
|
||||
for _, peer := range peers {
|
||||
if seenPeers[peer] {
|
||||
continue
|
||||
}
|
||||
seenPeers[peer] = true
|
||||
|
||||
// Rename Peer Definition
|
||||
prefix := ""
|
||||
if len(peer.RealName) > 0 {
|
||||
first := peer.RealName[0]
|
||||
if first == '+' || first == '$' {
|
||||
prefix = string(first)
|
||||
}
|
||||
}
|
||||
normNewName := strings.TrimLeft(params.NewName, "+$")
|
||||
finalDefName := prefix + normNewName
|
||||
|
||||
hasAlias := false
|
||||
for _, frag := range peer.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok && f.Name == "Alias" {
|
||||
hasAlias = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAlias {
|
||||
for _, frag := range peer.Fragments {
|
||||
if frag.IsObject {
|
||||
rng := Range{
|
||||
Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1},
|
||||
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(peer.RealName)},
|
||||
}
|
||||
addEdit(frag.File, rng, finalDefName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rename References to this Peer
|
||||
for _, ref := range Tree.References {
|
||||
if ref.Target == peer {
|
||||
// Handle qualified names
|
||||
if strings.Contains(ref.Name, ".") {
|
||||
if strings.HasSuffix(ref.Name, "."+peer.Name) {
|
||||
prefixLen := len(ref.Name) - len(peer.Name)
|
||||
rng := Range{
|
||||
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + prefixLen},
|
||||
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
|
||||
}
|
||||
addEdit(ref.File, rng, normNewName)
|
||||
} else if ref.Name == peer.Name {
|
||||
rng := Range{
|
||||
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
|
||||
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
|
||||
}
|
||||
addEdit(ref.File, rng, normNewName)
|
||||
}
|
||||
} else {
|
||||
rng := Range{
|
||||
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
|
||||
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
|
||||
}
|
||||
addEdit(ref.File, rng, normNewName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &WorkspaceEdit{Changes: changes}
|
||||
}
|
||||
|
||||
// 1. Rename Definitions
|
||||
prefix := ""
|
||||
if len(targetNode.RealName) > 0 {
|
||||
@@ -1566,7 +1693,7 @@ func suggestVariables(container *index.ProjectNode) *CompletionList {
|
||||
|
||||
doc := ""
|
||||
if info.Def.DefaultValue != nil {
|
||||
doc = fmt.Sprintf("Default: %s", valueToString(info.Def.DefaultValue))
|
||||
doc = fmt.Sprintf("Default: %s", valueToString(info.Def.DefaultValue, container))
|
||||
}
|
||||
|
||||
items = append(items, CompletionItem{
|
||||
@@ -1581,3 +1708,190 @@ func suggestVariables(container *index.ProjectNode) *CompletionList {
|
||||
}
|
||||
return &CompletionList{Items: items}
|
||||
}
|
||||
|
||||
func getSignalInfo(node *index.ProjectNode) (*index.ProjectNode, string) {
|
||||
if node.Parent == nil {
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Case 1: Definition
|
||||
if node.Parent.Name == "Signals" && isDataSource(node.Parent.Parent) {
|
||||
return node.Parent.Parent, node.RealName
|
||||
}
|
||||
|
||||
// Case 2: Usage
|
||||
if (node.Parent.Name == "InputSignals" || node.Parent.Name == "OutputSignals") && isGAM(node.Parent.Parent) {
|
||||
dsName := ""
|
||||
sigName := node.RealName
|
||||
|
||||
// Scan fields
|
||||
for _, frag := range node.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok {
|
||||
if f.Name == "DataSource" {
|
||||
if v, ok := f.Value.(*parser.StringValue); ok {
|
||||
dsName = v.Value
|
||||
}
|
||||
if v, ok := f.Value.(*parser.ReferenceValue); ok {
|
||||
dsName = v.Value
|
||||
}
|
||||
}
|
||||
if f.Name == "Alias" {
|
||||
if v, ok := f.Value.(*parser.StringValue); ok {
|
||||
sigName = v.Value
|
||||
}
|
||||
if v, ok := f.Value.(*parser.ReferenceValue); ok {
|
||||
sigName = v.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dsName != "" {
|
||||
dsNode := Tree.ResolveName(node, dsName, isDataSource)
|
||||
return dsNode, sigName
|
||||
}
|
||||
}
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func findSignalPeers(target *index.ProjectNode) []*index.ProjectNode {
|
||||
dsNode, sigName := getSignalInfo(target)
|
||||
if dsNode == nil || sigName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var peers []*index.ProjectNode
|
||||
|
||||
// Add definition if exists (and not already target)
|
||||
if signals, ok := dsNode.Children["Signals"]; ok {
|
||||
if def, ok := signals.Children[index.NormalizeName(sigName)]; ok {
|
||||
peers = append(peers, def)
|
||||
}
|
||||
}
|
||||
|
||||
// Find usages
|
||||
Tree.Walk(func(n *index.ProjectNode) {
|
||||
d, s := getSignalInfo(n)
|
||||
if d == dsNode && s == sigName {
|
||||
peers = append(peers, n)
|
||||
}
|
||||
})
|
||||
|
||||
return peers
|
||||
}
|
||||
|
||||
func evaluate(val parser.Value, ctx *index.ProjectNode) parser.Value {
|
||||
switch v := val.(type) {
|
||||
case *parser.VariableReferenceValue:
|
||||
name := strings.TrimLeft(v.Name, "@$")
|
||||
if info := Tree.ResolveVariable(ctx, name); info != nil {
|
||||
if info.Def.DefaultValue != nil {
|
||||
return evaluate(info.Def.DefaultValue, ctx)
|
||||
}
|
||||
}
|
||||
return v
|
||||
case *parser.BinaryExpression:
|
||||
left := evaluate(v.Left, ctx)
|
||||
right := evaluate(v.Right, ctx)
|
||||
return compute(left, v.Operator, right)
|
||||
case *parser.UnaryExpression:
|
||||
right := evaluate(v.Right, ctx)
|
||||
return computeUnary(v.Operator, right)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func compute(left parser.Value, op parser.Token, right parser.Value) parser.Value {
|
||||
if op.Type == parser.TokenConcat {
|
||||
s1 := valueToString(left, nil)
|
||||
s2 := valueToString(right, nil)
|
||||
return &parser.StringValue{Value: s1 + s2, Quoted: true}
|
||||
}
|
||||
|
||||
toInt := func(v parser.Value) (int64, bool) {
|
||||
if idx, ok := v.(*parser.IntValue); ok {
|
||||
return idx.Value, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
toFloat := func(v parser.Value) (float64, bool) {
|
||||
if f, ok := v.(*parser.FloatValue); ok {
|
||||
return f.Value, true
|
||||
}
|
||||
if idx, ok := v.(*parser.IntValue); ok {
|
||||
return float64(idx.Value), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
lI, lIsI := toInt(left)
|
||||
rI, rIsI := toInt(right)
|
||||
|
||||
if lIsI && rIsI {
|
||||
var res int64
|
||||
switch op.Type {
|
||||
case parser.TokenPlus:
|
||||
res = lI + rI
|
||||
case parser.TokenMinus:
|
||||
res = lI - rI
|
||||
case parser.TokenStar:
|
||||
res = lI * rI
|
||||
case parser.TokenSlash:
|
||||
if rI != 0 {
|
||||
res = lI / rI
|
||||
}
|
||||
case parser.TokenPercent:
|
||||
if rI != 0 {
|
||||
res = lI % rI
|
||||
}
|
||||
case parser.TokenAmpersand:
|
||||
res = lI & rI
|
||||
case parser.TokenPipe:
|
||||
res = lI | rI
|
||||
case parser.TokenCaret:
|
||||
res = lI ^ rI
|
||||
}
|
||||
return &parser.IntValue{Value: res, Raw: fmt.Sprintf("%d", res)}
|
||||
}
|
||||
|
||||
lF, lIsF := toFloat(left)
|
||||
rF, rIsF := toFloat(right)
|
||||
|
||||
if lIsF || rIsF {
|
||||
var res float64
|
||||
switch op.Type {
|
||||
case parser.TokenPlus:
|
||||
res = lF + rF
|
||||
case parser.TokenMinus:
|
||||
res = lF - rF
|
||||
case parser.TokenStar:
|
||||
res = lF * rF
|
||||
case parser.TokenSlash:
|
||||
res = lF / rF
|
||||
}
|
||||
return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)}
|
||||
}
|
||||
|
||||
return left
|
||||
}
|
||||
|
||||
func computeUnary(op parser.Token, val parser.Value) parser.Value {
|
||||
switch op.Type {
|
||||
case parser.TokenMinus:
|
||||
if i, ok := val.(*parser.IntValue); ok {
|
||||
return &parser.IntValue{Value: -i.Value, Raw: fmt.Sprintf("%d", -i.Value)}
|
||||
}
|
||||
if f, ok := val.(*parser.FloatValue); ok {
|
||||
return &parser.FloatValue{Value: -f.Value, Raw: fmt.Sprintf("%g", -f.Value)}
|
||||
}
|
||||
case parser.TokenSymbol:
|
||||
if op.Value == "!" {
|
||||
if b, ok := val.(*parser.BoolValue); ok {
|
||||
return &parser.BoolValue{Value: !b.Value}
|
||||
}
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ func (v *Validator) ValidateProject() {
|
||||
v.CheckUnused()
|
||||
v.CheckDataSourceThreading()
|
||||
v.CheckINOUTOrdering()
|
||||
v.CheckSignalConsistency()
|
||||
v.CheckVariables()
|
||||
v.CheckUnresolvedVariables()
|
||||
}
|
||||
@@ -1233,6 +1234,93 @@ func (v *Validator) getDataSourceDirection(ds *index.ProjectNode) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *Validator) CheckSignalConsistency() {
|
||||
// Map: DataSourceNode -> SignalName -> List of Signals
|
||||
signals := make(map[*index.ProjectNode]map[string][]*index.ProjectNode)
|
||||
|
||||
// Helper to collect signals
|
||||
collect := func(node *index.ProjectNode) {
|
||||
if !isGAM(node) {
|
||||
return
|
||||
}
|
||||
// Check Input and Output
|
||||
for _, dir := range []string{"InputSignals", "OutputSignals"} {
|
||||
if container, ok := node.Children[dir]; ok {
|
||||
for _, sig := range container.Children {
|
||||
fields := v.getFields(sig)
|
||||
var dsNode *index.ProjectNode
|
||||
var sigName string
|
||||
|
||||
// Resolve DS
|
||||
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
||||
dsName := v.getFieldValue(dsFields[0], sig)
|
||||
if dsName != "" {
|
||||
dsNode = v.resolveReference(dsName, sig, isDataSource)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve Name (Alias or RealName)
|
||||
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
|
||||
sigName = v.getFieldValue(aliasFields[0], sig)
|
||||
} else {
|
||||
sigName = sig.RealName
|
||||
}
|
||||
|
||||
if dsNode != nil && sigName != "" {
|
||||
sigName = index.NormalizeName(sigName)
|
||||
if signals[dsNode] == nil {
|
||||
signals[dsNode] = make(map[string][]*index.ProjectNode)
|
||||
}
|
||||
signals[dsNode][sigName] = append(signals[dsNode][sigName], sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.Tree.Walk(collect)
|
||||
|
||||
// Check Consistency
|
||||
for ds, sigMap := range signals {
|
||||
for sigName, usages := range sigMap {
|
||||
if len(usages) <= 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check Type consistency
|
||||
var firstType string
|
||||
var firstNode *index.ProjectNode
|
||||
|
||||
for _, u := range usages {
|
||||
// Get Type
|
||||
typeVal := ""
|
||||
fields := v.getFields(u)
|
||||
if typeFields, ok := fields["Type"]; ok && len(typeFields) > 0 {
|
||||
typeVal = v.getFieldValue(typeFields[0], u)
|
||||
}
|
||||
|
||||
if typeVal == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if firstNode == nil {
|
||||
firstType = typeVal
|
||||
firstNode = u
|
||||
} else {
|
||||
if typeVal != firstType {
|
||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||
Level: LevelError,
|
||||
Message: fmt.Sprintf("Signal Type Mismatch: Signal '%s' (in DS '%s') is defined as '%s' in '%s' but as '%s' in '%s'", sigName, ds.RealName, firstType, firstNode.Parent.Parent.RealName, typeVal, u.Parent.Parent.RealName),
|
||||
Position: v.getNodePosition(u),
|
||||
File: v.getNodeFile(u),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) CheckVariables() {
|
||||
if v.Schema == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user