Compare commits
3 Commits
d4075ff809
...
d2b2750833
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b2750833 | ||
|
|
55ca313b73 | ||
|
|
ff19fef779 |
@@ -182,7 +182,13 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
|
||||
}
|
||||
|
||||
if config.Package == nil {
|
||||
pt.populateNode(pt.Root, file, config)
|
||||
node := &ProjectNode{
|
||||
Children: make(map[string]*ProjectNode),
|
||||
Metadata: make(map[string]string),
|
||||
Variables: make(map[string]VariableInfo),
|
||||
}
|
||||
pt.IsolatedFiles[file] = node
|
||||
pt.populateNode(node, file, config)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ func HandleMessage(msg *JsonRpcMessage) {
|
||||
"documentFormattingProvider": true,
|
||||
"renameProvider": true,
|
||||
"completionProvider": map[string]any{
|
||||
"triggerCharacters": []string{"=", " "},
|
||||
"triggerCharacters": []string{"=", " ", "@"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -675,6 +675,20 @@ func HandleCompletion(params CompletionParams) *CompletionList {
|
||||
|
||||
prefix := lineStr[:col]
|
||||
|
||||
// Case 3: Variable completion
|
||||
varRegex := regexp.MustCompile(`([@$])([a-zA-Z0-9_]*)$`)
|
||||
if matches := varRegex.FindStringSubmatch(prefix); matches != nil {
|
||||
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
|
||||
if container == nil {
|
||||
if iso, ok := Tree.IsolatedFiles[path]; ok {
|
||||
container = iso
|
||||
} else {
|
||||
container = Tree.Root
|
||||
}
|
||||
}
|
||||
return suggestVariables(container)
|
||||
}
|
||||
|
||||
// Case 1: Assigning a value (Ends with "=" or "= ")
|
||||
if strings.Contains(prefix, "=") {
|
||||
lastIdx := strings.LastIndex(prefix, "=")
|
||||
@@ -716,9 +730,16 @@ func HandleCompletion(params CompletionParams) *CompletionList {
|
||||
return nil
|
||||
}
|
||||
|
||||
func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
|
||||
func suggestGAMSignals(container *index.ProjectNode, direction string) *CompletionList {
|
||||
var items []CompletionItem
|
||||
|
||||
// Find scope root
|
||||
root := container
|
||||
for root.Parent != nil {
|
||||
root = root.Parent
|
||||
}
|
||||
|
||||
var walk func(*index.ProjectNode)
|
||||
processNode := func(node *index.ProjectNode) {
|
||||
if !isDataSource(node) {
|
||||
return
|
||||
@@ -776,7 +797,13 @@ func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
|
||||
}
|
||||
}
|
||||
|
||||
Tree.Walk(processNode)
|
||||
walk = func(n *index.ProjectNode) {
|
||||
processNode(n)
|
||||
for _, child := range n.Children {
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
|
||||
if len(items) > 0 {
|
||||
return &CompletionList{Items: items}
|
||||
@@ -894,20 +921,41 @@ func suggestFieldValues(container *index.ProjectNode, field string, path string)
|
||||
root = Tree.Root
|
||||
}
|
||||
|
||||
var items []CompletionItem
|
||||
|
||||
if field == "DataSource" {
|
||||
return suggestObjects(root, "DataSource")
|
||||
if list := suggestObjects(root, "DataSource"); list != nil {
|
||||
items = append(items, list.Items...)
|
||||
}
|
||||
if field == "Functions" {
|
||||
return suggestObjects(root, "GAM")
|
||||
} else if field == "Functions" {
|
||||
if list := suggestObjects(root, "GAM"); list != nil {
|
||||
items = append(items, list.Items...)
|
||||
}
|
||||
if field == "Type" {
|
||||
return suggestSignalTypes()
|
||||
} else if field == "Type" {
|
||||
if list := suggestSignalTypes(); list != nil {
|
||||
items = append(items, list.Items...)
|
||||
}
|
||||
|
||||
} else {
|
||||
if list := suggestCUEEnums(container, field); list != nil {
|
||||
return list
|
||||
items = append(items, list.Items...)
|
||||
}
|
||||
}
|
||||
|
||||
// Add variables
|
||||
vars := suggestVariables(container)
|
||||
if vars != nil {
|
||||
for _, item := range vars.Items {
|
||||
// Create copy to modify label
|
||||
newItem := item
|
||||
newItem.Label = "@" + newItem.Label
|
||||
newItem.InsertText = "@" + item.Label
|
||||
items = append(items, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
return &CompletionList{Items: items}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1505,3 +1553,31 @@ func send(msg any) {
|
||||
body, _ := json.Marshal(msg)
|
||||
fmt.Fprintf(Output, "Content-Length: %d\r\n\r\n%s", len(body), body)
|
||||
}
|
||||
|
||||
func suggestVariables(container *index.ProjectNode) *CompletionList {
|
||||
items := []CompletionItem{}
|
||||
seen := make(map[string]bool)
|
||||
|
||||
curr := container
|
||||
for curr != nil {
|
||||
for name, info := range curr.Variables {
|
||||
if !seen[name] {
|
||||
seen[name] = true
|
||||
|
||||
doc := ""
|
||||
if info.Def.DefaultValue != nil {
|
||||
doc = fmt.Sprintf("Default: %s", valueToString(info.Def.DefaultValue))
|
||||
}
|
||||
|
||||
items = append(items, CompletionItem{
|
||||
Label: name,
|
||||
Kind: 6, // Variable
|
||||
Detail: fmt.Sprintf("Variable (%s)", info.Def.TypeExpr),
|
||||
Documentation: doc,
|
||||
})
|
||||
}
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
return &CompletionList{Items: items}
|
||||
}
|
||||
|
||||
@@ -153,3 +153,12 @@ type BinaryExpression struct {
|
||||
|
||||
func (b *BinaryExpression) Pos() Position { return b.Position }
|
||||
func (b *BinaryExpression) isValue() {}
|
||||
|
||||
type UnaryExpression struct {
|
||||
Position Position
|
||||
Operator Token
|
||||
Right Value
|
||||
}
|
||||
|
||||
func (u *UnaryExpression) Pos() Position { return u.Position }
|
||||
func (u *UnaryExpression) isValue() {}
|
||||
|
||||
@@ -147,18 +147,12 @@ func (l *Lexer) NextToken() Token {
|
||||
case ']':
|
||||
return l.emit(TokenRBracket)
|
||||
case '+':
|
||||
if unicode.IsSpace(l.peek()) {
|
||||
if unicode.IsSpace(l.peek()) || unicode.IsDigit(l.peek()) {
|
||||
return l.emit(TokenPlus)
|
||||
}
|
||||
return l.lexObjectIdentifier()
|
||||
case '-':
|
||||
if unicode.IsDigit(l.peek()) {
|
||||
return l.lexNumber()
|
||||
}
|
||||
if unicode.IsSpace(l.peek()) {
|
||||
return l.emit(TokenMinus)
|
||||
}
|
||||
return l.lexIdentifier()
|
||||
case '*':
|
||||
return l.emit(TokenStar)
|
||||
case '/':
|
||||
@@ -242,14 +236,29 @@ func (l *Lexer) lexString() Token {
|
||||
}
|
||||
|
||||
func (l *Lexer) lexNumber() Token {
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsDigit(r) || unicode.IsLetter(r) || r == '.' || r == '-' || r == '+' {
|
||||
continue
|
||||
// Consume initial digits (already started)
|
||||
l.lexDigits()
|
||||
|
||||
if l.peek() == '.' {
|
||||
l.next()
|
||||
l.lexDigits()
|
||||
}
|
||||
l.backup()
|
||||
|
||||
if r := l.peek(); r == 'e' || r == 'E' {
|
||||
l.next()
|
||||
if p := l.peek(); p == '+' || p == '-' {
|
||||
l.next()
|
||||
}
|
||||
l.lexDigits()
|
||||
}
|
||||
|
||||
return l.emit(TokenNumber)
|
||||
}
|
||||
|
||||
func (l *Lexer) lexDigits() {
|
||||
for unicode.IsDigit(l.peek()) {
|
||||
l.next()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) lexComment() Token {
|
||||
@@ -318,7 +327,7 @@ func (l *Lexer) lexHashIdentifier() Token {
|
||||
func (l *Lexer) lexVariableReference() Token {
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
|
||||
continue
|
||||
}
|
||||
l.backup()
|
||||
|
||||
@@ -299,8 +299,27 @@ func (p *Parser) parseAtom() (Value, bool) {
|
||||
return &ReferenceValue{Position: tok.Position, Value: tok.Value}, true
|
||||
case TokenVariableReference:
|
||||
return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true
|
||||
case TokenMinus:
|
||||
val, ok := p.parseAtom()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &UnaryExpression{Position: tok.Position, Operator: tok, Right: val}, true
|
||||
case TokenObjectIdentifier:
|
||||
return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true
|
||||
case TokenSymbol:
|
||||
if tok.Value == "(" {
|
||||
val, ok := p.parseExpression(0)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if next := p.next(); next.Type != TokenSymbol || next.Value != ")" {
|
||||
p.addError(next.Position, "expected )")
|
||||
return nil, false
|
||||
}
|
||||
return val, true
|
||||
}
|
||||
fallthrough
|
||||
case TokenLBrace:
|
||||
arr := &ArrayValue{Position: tok.Position}
|
||||
for {
|
||||
|
||||
@@ -236,6 +236,108 @@ func (v *Validator) valueToInterface(val parser.Value, ctx *index.ProjectNode) i
|
||||
arr = append(arr, v.valueToInterface(e, ctx))
|
||||
}
|
||||
return arr
|
||||
case *parser.BinaryExpression:
|
||||
left := v.valueToInterface(t.Left, ctx)
|
||||
right := v.valueToInterface(t.Right, ctx)
|
||||
return v.evaluateBinary(left, t.Operator.Type, right)
|
||||
case *parser.UnaryExpression:
|
||||
val := v.valueToInterface(t.Right, ctx)
|
||||
return v.evaluateUnary(t.Operator.Type, val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Validator) evaluateBinary(left interface{}, op parser.TokenType, right interface{}) interface{} {
|
||||
if left == nil || right == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if op == parser.TokenConcat {
|
||||
return fmt.Sprintf("%v%v", left, right)
|
||||
}
|
||||
|
||||
toInt := func(val interface{}) (int64, bool) {
|
||||
switch v := val.(type) {
|
||||
case int64:
|
||||
return v, true
|
||||
case int:
|
||||
return int64(v), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
toFloat := func(val interface{}) (float64, bool) {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
return v, true
|
||||
case int64:
|
||||
return float64(v), true
|
||||
case int:
|
||||
return float64(v), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if l, ok := toInt(left); ok {
|
||||
if r, ok := toInt(right); ok {
|
||||
switch op {
|
||||
case parser.TokenPlus:
|
||||
return l + r
|
||||
case parser.TokenMinus:
|
||||
return l - r
|
||||
case parser.TokenStar:
|
||||
return l * r
|
||||
case parser.TokenSlash:
|
||||
if r != 0 {
|
||||
return l / r
|
||||
}
|
||||
case parser.TokenPercent:
|
||||
if r != 0 {
|
||||
return l % r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if l, ok := toFloat(left); ok {
|
||||
if r, ok := toFloat(right); ok {
|
||||
switch op {
|
||||
case parser.TokenPlus:
|
||||
return l + r
|
||||
case parser.TokenMinus:
|
||||
return l - r
|
||||
case parser.TokenStar:
|
||||
return l * r
|
||||
case parser.TokenSlash:
|
||||
if r != 0 {
|
||||
return l / r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Validator) evaluateUnary(op parser.TokenType, val interface{}) interface{} {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch op {
|
||||
case parser.TokenMinus:
|
||||
switch v := val.(type) {
|
||||
case int64:
|
||||
return -v
|
||||
case float64:
|
||||
return -v
|
||||
}
|
||||
case parser.TokenSymbol: // ! is Symbol?
|
||||
// Parser uses TokenSymbol for ! ?
|
||||
// Lexer: '!' -> Symbol.
|
||||
if b, ok := val.(bool); ok {
|
||||
return !b
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
60
test/expression_parsing_test.go
Normal file
60
test/expression_parsing_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
)
|
||||
|
||||
func TestExpressionParsing(t *testing.T) {
|
||||
content := `
|
||||
#var A: int = 10
|
||||
#var B: int = 2
|
||||
|
||||
+Obj = {
|
||||
// 1. Multiple variables
|
||||
Expr1 = @A + @B + @A
|
||||
|
||||
// 2. Brackets
|
||||
Expr2 = (@A + 2) * @B
|
||||
|
||||
// 3. No space operator (variable name strictness)
|
||||
Expr3 = @A-2
|
||||
}
|
||||
`
|
||||
f, _ := os.CreateTemp("", "expr_test.marte")
|
||||
f.WriteString(content)
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
b := builder.NewBuilder([]string{f.Name()}, nil)
|
||||
|
||||
outF, _ := os.CreateTemp("", "out.marte")
|
||||
defer os.Remove(outF.Name())
|
||||
|
||||
err := b.Build(outF)
|
||||
if err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
outF.Close()
|
||||
|
||||
outContent, _ := os.ReadFile(outF.Name())
|
||||
outStr := string(outContent)
|
||||
|
||||
// Expr1: 10 + 2 + 10 = 22
|
||||
if !strings.Contains(outStr, "Expr1 = 22") {
|
||||
t.Errorf("Expr1 failed. Got:\n%s", outStr)
|
||||
}
|
||||
|
||||
// Expr2: (10 + 2) * 2 = 24
|
||||
if !strings.Contains(outStr, "Expr2 = 24") {
|
||||
t.Errorf("Expr2 failed. Got:\n%s", outStr)
|
||||
}
|
||||
|
||||
// Expr3: 10 - 2 = 8
|
||||
if !strings.Contains(outStr, "Expr3 = 8") {
|
||||
t.Errorf("Expr3 failed. Got:\n%s", outStr)
|
||||
}
|
||||
}
|
||||
39
test/expression_whitespace_test.go
Normal file
39
test/expression_whitespace_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
)
|
||||
|
||||
func TestExpressionWhitespace(t *testing.T) {
|
||||
content := `
|
||||
+Obj = {
|
||||
NoSpace = 2+2
|
||||
WithSpace = 2 + 2
|
||||
}
|
||||
`
|
||||
f, _ := os.CreateTemp("", "expr_ws.marte")
|
||||
f.WriteString(content)
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
b := builder.NewBuilder([]string{f.Name()}, nil)
|
||||
|
||||
outF, _ := os.CreateTemp("", "out.marte")
|
||||
defer os.Remove(outF.Name())
|
||||
b.Build(outF)
|
||||
outF.Close()
|
||||
|
||||
outContent, _ := os.ReadFile(outF.Name())
|
||||
outStr := string(outContent)
|
||||
|
||||
if !strings.Contains(outStr, "NoSpace = 4") {
|
||||
t.Errorf("NoSpace failed. Got:\n%s", outStr)
|
||||
}
|
||||
if !strings.Contains(outStr, "WithSpace = 4") {
|
||||
t.Errorf("WithSpace failed. Got:\n%s", outStr)
|
||||
}
|
||||
}
|
||||
38
test/isolation_test.go
Normal file
38
test/isolation_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
func TestIsolatedFileIsolation(t *testing.T) {
|
||||
pt := index.NewProjectTree()
|
||||
|
||||
// File 1: Project file
|
||||
f1 := "#package P\n+A = { Class = C }"
|
||||
p1 := parser.NewParser(f1)
|
||||
c1, _ := p1.Parse()
|
||||
pt.AddFile("f1.marte", c1)
|
||||
|
||||
// File 2: Isolated file
|
||||
f2 := "+B = { Class = C }"
|
||||
p2 := parser.NewParser(f2)
|
||||
c2, _ := p2.Parse()
|
||||
pt.AddFile("f2.marte", c2)
|
||||
|
||||
pt.ResolveReferences()
|
||||
|
||||
// Try finding A from f2
|
||||
isoNode := pt.IsolatedFiles["f2.marte"]
|
||||
if pt.ResolveName(isoNode, "A", nil) != nil {
|
||||
t.Error("Isolated file f2 should not see global A")
|
||||
}
|
||||
|
||||
// Try finding B from f1
|
||||
pNode := pt.Root.Children["P"]
|
||||
if pt.ResolveName(pNode, "B", nil) != nil {
|
||||
t.Error("Project file f1 should not see isolated B")
|
||||
}
|
||||
}
|
||||
@@ -193,8 +193,8 @@ $App = {
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundProjectDS {
|
||||
t.Error("Expected ProjectDS in isolated file suggestions (now shared root)")
|
||||
if foundProjectDS {
|
||||
t.Error("Did not expect ProjectDS in isolated file suggestions (isolation)")
|
||||
}
|
||||
|
||||
// Completion in a project file
|
||||
@@ -317,4 +317,66 @@ package schema
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest Variables", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
#var MyVar: uint = 10
|
||||
+App = {
|
||||
Field =
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
// 1. Triggered by =
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 12}, // After "Field = "
|
||||
}
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil {
|
||||
t.Fatal("Expected suggestions")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "@MyVar" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected @MyVar in suggestions for =")
|
||||
}
|
||||
|
||||
// 2. Triggered by $
|
||||
// "Field = $"
|
||||
lsp.Documents[uri] = `
|
||||
#var MyVar: uint = 10
|
||||
+App = {
|
||||
Field = $
|
||||
}
|
||||
`
|
||||
params2 := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 13}, // After "Field = $"
|
||||
}
|
||||
list2 := lsp.HandleCompletion(params2)
|
||||
if list2 == nil {
|
||||
t.Fatal("Expected suggestions for $")
|
||||
}
|
||||
found = false
|
||||
for _, item := range list2.Items {
|
||||
if item.Label == "MyVar" { // suggestVariables returns "MyVar"
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected MyVar in suggestions for $")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,9 +45,9 @@ func TestLSPSignalReferences(t *testing.T) {
|
||||
v.ValidateProject()
|
||||
|
||||
// Find definition of MySig in MyDS
|
||||
root := idx.Root
|
||||
root := idx.IsolatedFiles["signal_refs.marte"]
|
||||
if root == nil {
|
||||
t.Fatal("Root node not found")
|
||||
t.Fatal("Root node not found (isolated)")
|
||||
}
|
||||
|
||||
// Traverse to MySig
|
||||
|
||||
@@ -194,7 +194,7 @@ func TestIsolatedFileValidation(t *testing.T) {
|
||||
t.Fatal("Reference SharedObj not found in index")
|
||||
}
|
||||
|
||||
if ref.Target == nil {
|
||||
t.Errorf("Expected reference in root file (iso.marte) to resolve to global SharedObj")
|
||||
if ref.Target != nil {
|
||||
t.Errorf("Isolation failure: reference in isolated file resolved to global object")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user