Compare commits

...

2 Commits

Author SHA1 Message Date
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
4 changed files with 300 additions and 0 deletions

View File

@@ -614,12 +614,90 @@ func HandleCompletion(params CompletionParams) *CompletionList {
// Case 2: Typing a key inside an object
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
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 nil
}
func suggestGAMSignals(container *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 := "INOUT"
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
if direction == "Input" {
if dir == "IN" || dir == "INOUT" {
compatible = true
}
} else if direction == "Output" {
if dir == "OUT" || dir == "INOUT" {
compatible = true
}
}
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", sigName, dsName)
insertText := fmt.Sprintf("%s = { DataSource = %s }", 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 {
if GlobalSchema == nil {
return nil

View File

@@ -34,6 +34,10 @@ The LSP server should provide the following capabilities:
- **Reference Suggestions**:
- `DataSource` fields suggest available DataSource 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`).

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 == "Sig:DS" {
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 == "Sig:DS" {
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 == "InSig:InDS" {
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 == "OutSig:OutDS" {
foundOut = true
}
}
if !foundIn {
t.Error("Did not find InSig:InDS")
}
if foundOut {
t.Error("Should not find OutSig:OutDS 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 == "InSig:InDS" {
foundIn = true
}
if item.Label == "OutSig:OutDS" {
foundOut = true
}
}
if foundIn {
t.Error("Should not find InSig:InDS in OutputSignals")
}
if !foundOut {
t.Error("Did not find OutSig:OutDS in OutputSignals")
}
}