diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 46f2a5b..8a9178e 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -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 { + s, err := val.String() + if 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 diff --git a/test/lsp_completion_signals_test.go b/test/lsp_completion_signals_test.go new file mode 100644 index 0000000..47a4a9d --- /dev/null +++ b/test/lsp_completion_signals_test.go @@ -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") + } +}