Compare commits

..

12 Commits

Author SHA1 Message Date
Martino Ferrari
71a3c40108 Better LSP error handling 2026-01-27 08:58:38 +01:00
Martino Ferrari
aedc715ef3 Better code 2026-01-27 00:04:36 +01:00
Martino Ferrari
73cfc43f4b Updated readme. 2026-01-26 23:27:01 +01:00
Martino Ferrari
599beb6f4f updated license 2026-01-26 14:25:47 +01:00
Martino Ferrari
30a105df63 updated readme 2026-01-26 14:24:36 +01:00
Martino Ferrari
04196d8a1f Implement better completion 2026-01-25 15:21:38 +01:00
Martino Ferrari
02274f1bbf Implemented suggestion / autocompletion for signal in GAM 2026-01-25 00:28:50 +01:00
Martino Ferrari
12ed4cfbd2 reverse symbol renaming for signals 2026-01-25 00:18:40 +01:00
Martino Ferrari
bbeb344d19 Improved indexing, hover documentation and implemente renaming 2026-01-25 00:13:07 +01:00
Martino Ferrari
eeb4f5da2e added gam referencing 2026-01-24 23:47:59 +01:00
Martino Ferrari
8e13020d50 better signal hover message 2026-01-24 21:37:08 +01:00
Martino Ferrari
c9cc67f663 Minimal changes 2026-01-24 15:33:23 +01:00
17 changed files with 1134 additions and 45 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 MARTe Community Copyright (c) 2026 Martino G. Ferrari <manda.mgf@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -4,11 +4,22 @@
## Features ## Features
- **Portability**: A single statically compiled executable compatible with any Linux 3.2+ machine (as well as possible to compile and run on Windows and Mac OS X)
- **LSP Server**: Real-time syntax checking, validation, autocomplete, hover documentation, and navigation (Go to Definition/References). - **LSP Server**: Real-time syntax checking, validation, autocomplete, hover documentation, and navigation (Go to Definition/References).
- **Builder**: Merges multiple configuration files into a single, ordered output file. - **Builder**: Merges multiple configuration files into a single, ordered output file.
- **Formatter**: Standardizes configuration file formatting. - **Formatter**: Standardizes configuration file formatting.
- **Validator**: Advanced semantic validation using [CUE](https://cuelang.org/) schemas, ensuring type safety and structural correctness. - **Validator**: Advanced semantic validation using [CUE](https://cuelang.org/) schemas, ensuring type safety and structural correctness.
### MARTe extended configuration language
Few additional features have been added to the standard MARTe configuration language:
- Multi file configuration support
- Multi file definition merging
- File level namespace / node
- Doc-strings support
- Pragmas for warning suppression / documentation
## Installation ## Installation
### From Source ### From Source
@@ -47,6 +58,7 @@ go install github.com/marte-community/marte-dev-tools/cmd/mdt@latest
## MARTe Configuration ## MARTe Configuration
The tools support the MARTe configuration format with extended features: The tools support the MARTe configuration format with extended features:
- **Objects**: `+Node = { Class = ... }` - **Objects**: `+Node = { Class = ... }`
- **Signals**: `Signal = { Type = ... }` - **Signals**: `Signal = { Type = ... }`
- **Namespaces**: `#package PROJECT.NODE` for organizing multi-file projects. - **Namespaces**: `#package PROJECT.NODE` for organizing multi-file projects.
@@ -59,6 +71,7 @@ Validation is fully schema-driven using CUE.
- **Custom Schema**: Add a `.marte_schema.cue` file to your project root to extend or override definitions. - **Custom Schema**: Add a `.marte_schema.cue` file to your project root to extend or override definitions.
**Example `.marte_schema.cue`:** **Example `.marte_schema.cue`:**
```cue ```cue
package schema package schema
@@ -83,14 +96,17 @@ Use comments starting with `//!` to control validation behavior:
## Development ## Development
### Building ### Building
```bash ```bash
go build ./cmd/mdt go build ./cmd/mdt
``` ```
### Running Tests ### Running Tests
```bash ```bash
go test ./... go test ./...
``` ```
## License ## License
MIT MIT

View File

@@ -5,7 +5,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/marte-community/marte-dev-tools/internal/logger"
"github.com/marte-community/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
@@ -120,8 +119,11 @@ func (pt *ProjectTree) removeFileFromNode(node *ProjectNode, file string) {
node.Metadata = make(map[string]string) node.Metadata = make(map[string]string)
pt.rebuildMetadata(node) pt.rebuildMetadata(node)
for _, child := range node.Children { for name, child := range node.Children {
pt.removeFileFromNode(child, file) pt.removeFileFromNode(child, file)
if len(child.Fragments) == 0 && len(child.Children) == 0 {
delete(node.Children, name)
}
} }
} }
@@ -181,13 +183,8 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
node := pt.Root node := pt.Root
parts := strings.Split(config.Package.URI, ".") parts := strings.Split(config.Package.URI, ".")
// Skip first part as per spec (Project Name is namespace only)
startIdx := 0 for i := 0; i < len(parts); i++ {
if len(parts) > 0 {
startIdx = 1
}
for i := startIdx; i < len(parts); i++ {
part := strings.TrimSpace(parts[i]) part := strings.TrimSpace(parts[i])
if part == "" { if part == "" {
continue continue
@@ -458,9 +455,7 @@ type QueryResult struct {
} }
func (pt *ProjectTree) Query(file string, line, col int) *QueryResult { func (pt *ProjectTree) Query(file string, line, col int) *QueryResult {
logger.Printf("File: %s:%d:%d", file, line, col)
for i := range pt.References { for i := range pt.References {
logger.Printf("%s", pt.Root.Name)
ref := &pt.References[i] ref := &pt.References[i]
if ref.File == file { if ref.File == file {
if line == ref.Position.Line && col >= ref.Position.Column && col < ref.Position.Column+len(ref.Name) { if line == ref.Position.Line && col >= ref.Position.Column && col < ref.Position.Column+len(ref.Name) {

View File

@@ -20,12 +20,10 @@ import (
"cuelang.org/go/cue" "cuelang.org/go/cue"
) )
type CompletionParams struct { type CompletionParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"` TextDocument TextDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"` Position Position `json:"position"`
Context CompletionContext `json:"context,omitempty"` Context CompletionContext `json:"context"`
} }
type CompletionContext struct { type CompletionContext struct {
@@ -161,6 +159,16 @@ type DocumentFormattingParams struct {
Options FormattingOptions `json:"options"` Options FormattingOptions `json:"options"`
} }
type RenameParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"`
NewName string `json:"newName"`
}
type WorkspaceEdit struct {
Changes map[string][]TextEdit `json:"changes"`
}
type FormattingOptions struct { type FormattingOptions struct {
TabSize int `json:"tabSize"` TabSize int `json:"tabSize"`
InsertSpaces bool `json:"insertSpaces"` InsertSpaces bool `json:"insertSpaces"`
@@ -171,7 +179,6 @@ type TextEdit struct {
NewText string `json:"newText"` NewText string `json:"newText"`
} }
func RunServer() { func RunServer() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
@@ -215,6 +222,12 @@ func readMessage(reader *bufio.Reader) (*JsonRpcMessage, error) {
} }
func HandleMessage(msg *JsonRpcMessage) { func HandleMessage(msg *JsonRpcMessage) {
defer func() {
if r := recover(); r != nil {
logger.Printf("Panic in HandleMessage: %v", r)
}
}()
switch msg.Method { switch msg.Method {
case "initialize": case "initialize":
var params InitializeParams var params InitializeParams
@@ -244,6 +257,7 @@ func HandleMessage(msg *JsonRpcMessage) {
"definitionProvider": true, "definitionProvider": true,
"referencesProvider": true, "referencesProvider": true,
"documentFormattingProvider": true, "documentFormattingProvider": true,
"renameProvider": true,
"completionProvider": map[string]any{ "completionProvider": map[string]any{
"triggerCharacters": []string{"=", " "}, "triggerCharacters": []string{"=", " "},
}, },
@@ -300,6 +314,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, HandleFormatting(params)) respond(msg.ID, HandleFormatting(params))
} }
case "textDocument/rename":
var params RenameParams
if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, HandleRename(params))
}
} }
} }
@@ -382,7 +401,7 @@ func HandleFormatting(params DocumentFormattingParams) []TextEdit {
} }
} }
func runValidation(uri string) { func runValidation(_ string) {
v := validator.NewValidator(Tree, ProjectRoot) v := validator.NewValidator(Tree, ProjectRoot)
v.ValidateProject() v.ValidateProject()
v.CheckUnused() v.CheckUnused()
@@ -567,10 +586,7 @@ func HandleCompletion(params CompletionParams) *CompletionList {
} }
lineStr := lines[params.Position.Line] lineStr := lines[params.Position.Line]
col := params.Position.Character col := min(params.Position.Character, len(lineStr))
if col > len(lineStr) {
col = len(lineStr)
}
prefix := lineStr[:col] prefix := lineStr[:col]
@@ -601,12 +617,88 @@ func HandleCompletion(params CompletionParams) *CompletionList {
// Case 2: Typing a key inside an object // Case 2: Typing a key inside an object
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1}) container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
if container != nil { if container != nil {
if container.Parent != nil && isGAM(container.Parent) {
if container.Name == "InputSignals" {
return suggestGAMSignals(container, "Input")
}
if container.Name == "OutputSignals" {
return suggestGAMSignals(container, "Output")
}
}
return suggestFields(container) return suggestFields(container)
} }
return nil return nil
} }
func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
var items []CompletionItem
processNode := func(node *index.ProjectNode) {
if !isDataSource(node) {
return
}
cls := node.Metadata["Class"]
if cls == "" {
return
}
dir := "NIL"
if GlobalSchema != nil {
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", cls))
val := GlobalSchema.Value.LookupPath(classPath)
if val.Err() == nil {
var s string
if err := val.Decode(&s); err == nil {
dir = s
}
}
}
compatible := false
switch direction {
case "Input":
compatible = dir == "IN" || dir == "INOUT"
case "Output":
compatible = dir == "OUT" || dir == "INOUT"
default:
compatible = false
}
if !compatible {
return
}
signalsContainer := node.Children["Signals"]
if signalsContainer == nil {
return
}
for _, sig := range signalsContainer.Children {
dsName := node.Name
sigName := sig.Name
label := fmt.Sprintf("%s:%s", dsName, sigName)
insertText := fmt.Sprintf("%s = {\n DataSource = %s \n}", sigName, dsName)
items = append(items, CompletionItem{
Label: label,
Kind: 6, // Variable
Detail: "Signal from " + dsName,
InsertText: insertText,
InsertTextFormat: 2, // Snippet
})
}
}
Tree.Walk(processNode)
if len(items) > 0 {
return &CompletionList{Items: items}
}
return nil
}
func suggestClasses() *CompletionList { func suggestClasses() *CompletionList {
if GlobalSchema == nil { if GlobalSchema == nil {
return nil return nil
@@ -783,11 +875,11 @@ func suggestCUEEnums(container *index.ProjectNode, field string) *CompletionList
if err != nil { if err != nil {
continue continue
} }
// Ensure strings are quoted // Ensure strings are quoted
if v.Kind() == cue.StringKind && !strings.HasPrefix(str, "\"") { if v.Kind() == cue.StringKind && !strings.HasPrefix(str, "\"") {
str = fmt.Sprintf("\"%s\"", str) str = fmt.Sprintf("\"%s\"", str)
} }
items = append(items, CompletionItem{ items = append(items, CompletionItem{
Label: str, Label: str,
@@ -811,14 +903,11 @@ func suggestObjects(root *index.ProjectNode, filter string) *CompletionList {
var walk func(*index.ProjectNode) var walk func(*index.ProjectNode)
walk = func(node *index.ProjectNode) { walk = func(node *index.ProjectNode) {
match := false match := false
if filter == "GAM" { switch filter {
if isGAM(node) { case "GAM":
match = true match = isGAM(node)
} case "DataSource":
} else if filter == "DataSource" { match = isDataSource(node)
if isDataSource(node) {
match = true
}
} }
if match { if match {
@@ -981,6 +1070,14 @@ func formatNodeInfo(node *index.ProjectNode) string {
typ := node.Metadata["Type"] typ := node.Metadata["Type"]
ds := node.Metadata["DataSource"] ds := node.Metadata["DataSource"]
if ds == "" {
if node.Parent != nil && node.Parent.Name == "Signals" {
if node.Parent.Parent != nil {
ds = node.Parent.Parent.Name
}
}
}
if typ != "" || ds != "" { if typ != "" || ds != "" {
sigInfo := "\n" sigInfo := "\n"
if typ != "" { if typ != "" {
@@ -1053,14 +1150,192 @@ func formatNodeInfo(node *index.ProjectNode) string {
} }
} }
// Find GAM usages
var gams []string
// 1. Check References (explicit text references)
for _, ref := range Tree.References {
if ref.Target == node {
container := Tree.GetNodeContaining(ref.File, ref.Position)
if container != nil {
curr := container
for curr != nil {
if isGAM(curr) {
gams = append(gams, curr.RealName)
break
}
curr = curr.Parent
}
}
}
}
// 2. Check Direct Usages (Nodes targeting this node)
Tree.Walk(func(n *index.ProjectNode) {
if n.Target == node {
if n.Parent != nil && (n.Parent.Name == "InputSignals" || n.Parent.Name == "OutputSignals") {
if n.Parent.Parent != nil && isGAM(n.Parent.Parent) {
gams = append(gams, n.Parent.Parent.RealName)
}
}
}
})
if len(gams) > 0 {
uniqueGams := make(map[string]bool)
info += "\n\n**Used in GAMs**:\n"
for _, g := range gams {
if !uniqueGams[g] {
uniqueGams[g] = true
info += fmt.Sprintf("- %s\n", g)
}
}
}
return info return info
} }
func HandleRename(params RenameParams) *WorkspaceEdit {
path := uriToPath(params.TextDocument.URI)
line := params.Position.Line + 1
col := params.Position.Character + 1
res := Tree.Query(path, line, col)
if res == nil {
return nil
}
var targetNode *index.ProjectNode
var targetField *parser.Field
if res.Node != nil {
if res.Node.Target != nil {
targetNode = res.Node.Target
} else {
targetNode = res.Node
}
} else if res.Field != nil {
targetField = res.Field
} else if res.Reference != nil {
if res.Reference.Target != nil {
targetNode = res.Reference.Target
} else {
return nil
}
}
changes := make(map[string][]TextEdit)
addEdit := func(file string, rng Range, newText string) {
uri := "file://" + file
changes[uri] = append(changes[uri], TextEdit{Range: rng, NewText: newText})
}
if targetNode != nil {
// 1. Rename Definitions
prefix := ""
if len(targetNode.RealName) > 0 {
first := targetNode.RealName[0]
if first == '+' || first == '$' {
prefix = string(first)
}
}
normNewName := strings.TrimLeft(params.NewName, "+$")
finalDefName := prefix + normNewName
for _, frag := range targetNode.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(targetNode.RealName)},
}
addEdit(frag.File, rng, finalDefName)
}
}
// 2. Rename References
for _, ref := range Tree.References {
if ref.Target == targetNode {
// Handle qualified names (e.g. Pkg.Node)
if strings.Contains(ref.Name, ".") {
if strings.HasSuffix(ref.Name, "."+targetNode.Name) {
prefixLen := len(ref.Name) - len(targetNode.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 == targetNode.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)
}
}
}
// 3. Rename Implicit Node References (Signals in GAMs relying on name match)
Tree.Walk(func(n *index.ProjectNode) {
if n.Target == targetNode {
hasAlias := false
for _, frag := range n.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok && f.Name == "Alias" {
hasAlias = true
}
}
}
if !hasAlias {
for _, frag := range n.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(n.RealName)},
}
addEdit(frag.File, rng, normNewName)
}
}
}
}
})
return &WorkspaceEdit{Changes: changes}
} else if targetField != nil {
container := Tree.GetNodeContaining(path, targetField.Position)
if container != nil {
for _, frag := range container.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if f.Name == targetField.Name {
rng := Range{
Start: Position{Line: f.Position.Line - 1, Character: f.Position.Column - 1},
End: Position{Line: f.Position.Line - 1, Character: f.Position.Column - 1 + len(f.Name)},
}
addEdit(frag.File, rng, params.NewName)
}
}
}
}
}
return &WorkspaceEdit{Changes: changes}
}
return nil
}
func respond(id any, result any) { func respond(id any, result any) {
msg := JsonRpcMessage{ msg := map[string]any{
Jsonrpc: "2.0", "jsonrpc": "2.0",
ID: id, "id": id,
Result: result, "result": result,
} }
send(msg) send(msg)
} }

View File

@@ -13,7 +13,7 @@ package schema
StateMachineEvent: { StateMachineEvent: {
NextState!: string NextState!: string
NextStateError!: string NextStateError!: string
Timeout: uint32 Timeout?: uint32
[_= !~"^(Class|NextState|Timeout|NextStateError|[#_$].+)$"]: Message [_= !~"^(Class|NextState|Timeout|NextStateError|[#_$].+)$"]: Message
... ...
} }

View File

@@ -542,11 +542,6 @@ func isValidType(t string) bool {
return false return false
} }
func (v *Validator) checkType(val parser.Value, expectedType string) bool {
// Legacy function, replaced by CUE.
return true
}
func (v *Validator) getFileForField(f *parser.Field, node *index.ProjectNode) string { func (v *Validator) getFileForField(f *parser.Field, node *index.ProjectNode) string {
for _, frag := range node.Fragments { for _, frag := range node.Fragments {
for _, def := range frag.Definitions { for _, def := range frag.Definitions {

View File

@@ -34,6 +34,13 @@ The LSP server should provide the following capabilities:
- **Reference Suggestions**: - **Reference Suggestions**:
- `DataSource` fields suggest available DataSource objects. - `DataSource` fields suggest available DataSource objects.
- `Functions` (in Threads) suggest available GAM objects. - `Functions` (in Threads) suggest available GAM objects.
- **Signal Completion**: Inside `InputSignals` or `OutputSignals` of a GAM:
- Suggests available signals from valid DataSources (filtering by direction: `IN`/`INOUT` for Inputs, `OUT`/`INOUT` for Outputs).
- Format: `SIGNAL_NAME:DATASOURCE_NAME`.
- Auto-inserts: `SIGNAL_NAME = { DataSource = DATASOURCE_NAME }`.
- **Rename Symbol**: Rename an object, field, or reference across the entire project scope.
- Supports renaming of Definitions (`+Name` or `Name`), preserving any modifiers (`+`/`$`).
- Updates all references to the renamed symbol, including qualified references (e.g., `Pkg.Name`).
- **Code Snippets**: Provide snippets for common patterns (e.g., `+Object = { ... }`). - **Code Snippets**: Provide snippets for common patterns (e.g., `+Object = { ... }`).
- **Formatting**: Format the document using the same rules and engine as the `fmt` command. - **Formatting**: Format the document using the same rules and engine as the `fmt` command.

View File

@@ -0,0 +1,58 @@
package integration
import (
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/parser"
)
func TestIndexCleanup(t *testing.T) {
idx := index.NewProjectTree()
file := "cleanup.marte"
content := `
#package Pkg
+Node = { Class = Type }
`
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
idx.AddFile(file, cfg)
// Check node exists
// Root -> Pkg -> Node
pkgNode := idx.Root.Children["Pkg"]
if pkgNode == nil {
t.Fatal("Pkg node should exist")
}
if pkgNode.Children["Node"] == nil {
t.Fatal("Node should exist")
}
// Update file: remove +Node
content2 := `
#package Pkg
// Removed node
`
p2 := parser.NewParser(content2)
cfg2, _ := p2.Parse()
idx.AddFile(file, cfg2)
// Check Node is gone
pkgNode = idx.Root.Children["Pkg"]
if pkgNode == nil {
// Pkg should exist because of #package Pkg
t.Fatal("Pkg node should exist after update")
}
if pkgNode.Children["Node"] != nil {
t.Error("Node should be gone")
}
// Test removing file completely
idx.RemoveFile(file)
if len(idx.Root.Children) != 0 {
t.Errorf("Root should be empty after removing file, got %d children", len(idx.Root.Children))
}
}

View File

@@ -0,0 +1,90 @@
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/schema"
)
func TestSuggestSignalsRobustness(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.ProjectRoot = "."
lsp.GlobalSchema = schema.NewSchema()
// Inject schema with INOUT
custom := []byte(`
package schema
#Classes: {
InOutReader: { direction: "INOUT" }
}
`)
val := lsp.GlobalSchema.Context.CompileBytes(custom)
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
content := `
+DS = {
Class = InOutReader
+Signals = {
Sig = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
}
+OutputSignals = {
}
}
`
uri := "file://robust.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("robust.marte", cfg)
// Check Input (Line 10)
paramsIn := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 10, Character: 8},
}
listIn := lsp.HandleCompletion(paramsIn)
found := false
if listIn != nil {
for _, item := range listIn.Items {
if item.Label == "DS:Sig" {
found = true
}
}
}
if !found {
t.Error("INOUT signal not found in InputSignals")
}
// Check Output (Line 13)
paramsOut := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 13, Character: 8},
}
listOut := lsp.HandleCompletion(paramsOut)
found = false
if listOut != nil {
for _, item := range listOut.Items {
if item.Label == "DS:Sig" {
found = true
}
}
}
if !found {
t.Error("INOUT signal not found in OutputSignals")
}
}

View File

@@ -0,0 +1,128 @@
package integration
import (
"strings"
"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/schema"
)
func TestSuggestSignalsInGAM(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.ProjectRoot = "."
lsp.GlobalSchema = schema.NewSchema()
// Inject schema for directionality
custom := []byte(`
package schema
#Classes: {
FileReader: { direction: "IN" }
FileWriter: { direction: "OUT" }
}
`)
val := lsp.GlobalSchema.Context.CompileBytes(custom)
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
content := `
+InDS = {
Class = FileReader
+Signals = {
InSig = { Type = uint32 }
}
}
+OutDS = {
Class = FileWriter
+Signals = {
OutSig = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
}
+OutputSignals = {
}
}
`
uri := "file://signals.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("signals.marte", cfg)
// 1. Suggest in InputSignals
// Line 16 (empty line inside InputSignals)
paramsIn := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 16, Character: 8},
}
listIn := lsp.HandleCompletion(paramsIn)
if listIn == nil {
t.Fatal("Expected suggestions in InputSignals")
}
foundIn := false
foundOut := false
for _, item := range listIn.Items {
if item.Label == "InDS:InSig" {
foundIn = true
// Normalize spaces for check
insert := strings.ReplaceAll(item.InsertText, " ", "")
expected := "InSig={DataSource=InDS}"
if !strings.Contains(insert, expected) && !strings.Contains(item.InsertText, "InSig = {") {
// Snippet might differ slightly, but should contain essentials
t.Errorf("InsertText mismatch: %s", item.InsertText)
}
}
if item.Label == "OutDS:OutSig" {
foundOut = true
}
}
if !foundIn {
t.Error("Did not find InDS:InSig")
}
if foundOut {
t.Error("Should not find OutDS:OutSig in InputSignals")
}
// 2. Suggest in OutputSignals
// Line 19
paramsOut := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 19, Character: 8},
}
listOut := lsp.HandleCompletion(paramsOut)
if listOut == nil {
t.Fatal("Expected suggestions in OutputSignals")
}
foundIn = false
foundOut = false
for _, item := range listOut.Items {
if item.Label == "InDS:InSig" {
foundIn = true
}
if item.Label == "OutDS:OutSig" {
foundOut = true
}
}
if foundIn {
t.Error("Should not find InDS:InSig in OutputSignals")
}
if !foundOut {
t.Error("Did not find OutDS:OutSig in OutputSignals")
}
}

74
test/lsp_crash_test.go Normal file
View File

@@ -0,0 +1,74 @@
package integration
import (
"strings"
"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"
)
func TestLSPCrashOnUndefinedReference(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+App = {
Class = RealTimeApplication
+State = {
Class = RealTimeState
+Thread = {
Class = RealTimeThread
Functions = { UndefinedGAM }
}
}
}
`
uri := "file://crash.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("crash.marte", cfg)
lsp.Tree.ResolveReferences()
// Line 7: " Functions = { UndefinedGAM }"
// 12 spaces + "Functions" (9) + " = { " (5) = 26 chars prefix.
// UndefinedGAM starts at 26.
params := lsp.DefinitionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 7, Character: 27},
}
// This should NOT panic
defer func() {
if r := recover(); r != nil {
t.Errorf("Recovered from panic: %v", r)
}
}()
res := lsp.HandleDefinition(params)
if res != nil {
t.Error("Expected nil for undefined reference definition")
}
// 2. Hover
hParams := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 7, Character: 27},
}
hover := lsp.HandleHover(hParams)
if hover == nil {
t.Error("Expected hover for unresolved reference")
} else {
content := hover.Contents.(lsp.MarkupContent).Value
if !strings.Contains(content, "Unresolved") {
t.Errorf("Expected 'Unresolved' in hover, got: %s", content)
}
}
}

View File

@@ -0,0 +1,81 @@
package integration
import (
"strings"
"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"
)
func TestHoverDataSourceName(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS1 = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM1 = {
Class = IOGAM
+InputSignals = {
S1 = {
DataSource = DS1
Alias = Sig1
}
}
}
`
uri := "file://test_ds.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatalf("Parse error: %v", err)
}
lsp.Tree.AddFile("test_ds.marte", cfg)
lsp.Tree.ResolveReferences()
// Test 1: Explicit Signal (Sig1)
// Position: "Sig1" at line 5 (0-based 4)
// Line 4: " Sig1 = { Type = uint32 }"
// Col: 8
params1 := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: 9},
}
hover1 := lsp.HandleHover(params1)
if hover1 == nil {
t.Fatal("Expected hover for Sig1")
}
content1 := hover1.Contents.(lsp.MarkupContent).Value
// Expectation: explicit signal shows owner datasource
if !strings.Contains(content1, "**DataSource**: `+DS1`") && !strings.Contains(content1, "**DataSource**: `DS1`") {
t.Errorf("Expected DataSource: +DS1 in hover for Sig1, got: %s", content1)
}
// Test 2: Implicit Signal (S1)
// Position: "S1" at line 11 (0-based 10)
params2 := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 10, Character: 9},
}
hover2 := lsp.HandleHover(params2)
if hover2 == nil {
t.Fatal("Expected hover for S1")
}
content2 := hover2.Contents.(lsp.MarkupContent).Value
// Expectation: implicit signal shows referenced datasource
if !strings.Contains(content2, "**DataSource**: `DS1`") {
t.Errorf("Expected DataSource: DS1 in hover for S1, got: %s", content2)
}
}

View File

@@ -0,0 +1,75 @@
package integration
import (
"strings"
"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"
)
func TestHoverGAMUsage(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS1 = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM1 = {
Class = IOGAM
+InputSignals = {
S1 = {
DataSource = DS1
Alias = Sig1
}
}
}
+GAM2 = {
Class = IOGAM
+OutputSignals = {
S2 = {
DataSource = DS1
Alias = Sig1
}
}
}
`
uri := "file://test_gam_usage.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("test_gam_usage.marte", cfg)
lsp.Tree.ResolveReferences()
// Query hover for Sig1 (Line 5)
// Line 4: Sig1... (0-based)
params := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: 9},
}
hover := lsp.HandleHover(params)
if hover == nil {
t.Fatal("Expected hover")
}
contentHover := hover.Contents.(lsp.MarkupContent).Value
if !strings.Contains(contentHover, "**Used in GAMs**") {
t.Errorf("Expected 'Used in GAMs' section, got:\n%s", contentHover)
}
if !strings.Contains(contentHover, "- +GAM1") {
t.Error("Expected +GAM1 in usage list")
}
if !strings.Contains(contentHover, "- +GAM2") {
t.Error("Expected +GAM2 in usage list")
}
}

View File

@@ -0,0 +1,89 @@
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 TestRenameImplicitToDefinition(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
// Implicit usage
Sig1 = { DataSource = DS }
}
}
`
uri := "file://rename_imp.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("rename_imp.marte", cfg)
lsp.Tree.ResolveReferences()
// Run validator to link targets
v := validator.NewValidator(lsp.Tree, ".")
v.ValidateProject()
// Rename Implicit Sig1 (Line 11, 0-based 11)
// Line 11: " Sig1 = { DataSource = DS }"
params := lsp.RenameParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 11, Character: 9},
NewName: "NewSig",
}
edit := lsp.HandleRename(params)
if edit == nil {
t.Fatal("Expected edits")
}
edits := edit.Changes[uri]
// Expect:
// 1. Rename Implicit Sig1 (Line 9) -> NewSig
// 2. Rename Definition Sig1 (Line 4) -> NewSig
if len(edits) != 2 {
t.Errorf("Expected 2 edits, got %d", len(edits))
for _, e := range edits {
t.Logf("Edit at line %d", e.Range.Start.Line)
}
}
foundDef := false
foundImp := false
for _, e := range edits {
if e.Range.Start.Line == 4 {
foundDef = true
}
if e.Range.Start.Line == 11 {
foundImp = true
}
}
if !foundDef {
t.Error("Definition not renamed")
}
if !foundImp {
t.Error("Implicit usage not renamed")
}
}

View File

@@ -0,0 +1,110 @@
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 TestRenameSignalInGAM(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
// Implicit match
Sig1 = { DataSource = DS }
// Explicit Alias
S2 = { DataSource = DS Alias = Sig1 }
}
}
`
uri := "file://rename_sig.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("rename_sig.marte", cfg)
lsp.Tree.ResolveReferences()
// Run validator to populate Targets
v := validator.NewValidator(lsp.Tree, ".")
v.ValidateProject()
// Rename DS.Sig1 to NewSig
// Sig1 is at Line 5.
// Line 0: empty
// Line 1: +DS
// Line 2: Class
// Line 3: +Signals
// Line 4: Sig1
params := lsp.RenameParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: 9}, // Sig1
NewName: "NewSig",
}
edit := lsp.HandleRename(params)
if edit == nil {
t.Fatal("Expected edits")
}
edits := edit.Changes[uri]
// Expect:
// 1. Definition of Sig1 in DS (Line 5) -> NewSig
// 2. Definition of Sig1 in GAM (Line 10) -> NewSig (Implicit match)
// 3. Alias reference in S2 (Line 12) -> NewSig
// Line 10: Sig1 = ... (0-based 9)
// Line 12: S2 = ... Alias = Sig1 (0-based 11)
expectedCount := 3
if len(edits) != expectedCount {
t.Errorf("Expected %d edits, got %d", expectedCount, len(edits))
for _, e := range edits {
t.Logf("Edit: %s at %d", e.NewText, e.Range.Start.Line)
}
}
// Check Implicit Signal Rename
foundImplicit := false
for _, e := range edits {
if e.Range.Start.Line == 11 {
if e.NewText == "NewSig" {
foundImplicit = true
}
}
}
if !foundImplicit {
t.Error("Did not find implicit signal rename")
}
// Check Alias Rename
foundAlias := false
for _, e := range edits {
if e.Range.Start.Line == 13 {
// Alias = Sig1. Range should cover Sig1.
if e.NewText == "NewSig" {
foundAlias = true
}
}
}
if !foundAlias {
t.Error("Did not find alias reference rename")
}
}

92
test/lsp_rename_test.go Normal file
View File

@@ -0,0 +1,92 @@
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"
)
func TestHandleRename(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
#package Some
+MyNode = {
Class = Type
}
+Consumer = {
Link = MyNode
PkgLink = Some.MyNode
}
`
uri := "file://rename.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("rename.marte", cfg)
lsp.Tree.ResolveReferences()
// Rename +MyNode to NewNode
// +MyNode is at Line 2 (after #package)
// Line 0: empty
// Line 1: #package
// Line 2: +MyNode
params := lsp.RenameParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 2, Character: 4}, // +MyNode
NewName: "NewNode",
}
edit := lsp.HandleRename(params)
if edit == nil {
t.Fatal("Expected edits")
}
edits := edit.Changes[uri]
if len(edits) != 3 {
t.Errorf("Expected 3 edits (Def, Link, PkgLink), got %d", len(edits))
}
// Verify Definition change (+MyNode -> +NewNode)
foundDef := false
for _, e := range edits {
if e.NewText == "+NewNode" {
foundDef = true
if e.Range.Start.Line != 2 {
t.Errorf("Definition edit line wrong: %d", e.Range.Start.Line)
}
}
}
if !foundDef {
t.Error("Did not find definition edit +NewNode")
}
// Verify Link change (MyNode -> NewNode)
foundLink := false
for _, e := range edits {
if e.NewText == "NewNode" && e.Range.Start.Line == 6 { // Link = MyNode
foundLink = true
}
}
if !foundLink {
t.Error("Did not find Link edit")
}
// Verify PkgLink change (Some.MyNode -> Some.NewNode)
foundPkg := false
for _, e := range edits {
if e.NewText == "NewNode" && e.Range.Start.Line == 7 { // PkgLink = Some.MyNode
foundPkg = true
}
}
if !foundPkg {
t.Error("Did not find PkgLink edit")
}
}

View File

@@ -107,7 +107,11 @@ func TestHierarchicalPackageMerge(t *testing.T) {
} }
// We can also inspect the tree to verify FieldX is there (optional, but good for confidence) // We can also inspect the tree to verify FieldX is there (optional, but good for confidence)
baseNode := idx.Root.Children["Base"] projNode := idx.Root.Children["Proj"]
if projNode == nil {
t.Fatal("Proj node not found")
}
baseNode := projNode.Children["Base"]
if baseNode == nil { if baseNode == nil {
t.Fatal("Base node not found") t.Fatal("Base node not found")
} }