diff --git a/internal/index/index.go b/internal/index/index.go index 6c8cb89..48cdb77 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -21,9 +21,11 @@ type Reference struct { type ProjectNode struct { Name string // Normalized name RealName string // The actual name used in definition (e.g. +Node) + Doc string // Aggregated documentation Fragments []*Fragment Children map[string]*ProjectNode Parent *ProjectNode + Metadata map[string]string // Store extra info like Class, Type, Size } type Fragment struct { @@ -31,12 +33,14 @@ type Fragment struct { Definitions []parser.Definition IsObject bool ObjectPos parser.Position + Doc string // Documentation for this fragment (if object) } func NewProjectTree() *ProjectTree { return &ProjectTree{ Root: &ProjectNode{ Children: make(map[string]*ProjectNode), + Metadata: make(map[string]string), }, } } @@ -49,7 +53,6 @@ func NormalizeName(name string) string { } func (pt *ProjectTree) RemoveFile(file string) { - // Remove references from this file newRefs := []Reference{} for _, ref := range pt.References { if ref.File != file { @@ -58,7 +61,6 @@ func (pt *ProjectTree) RemoveFile(file string) { } pt.References = newRefs - // Remove fragments from tree pt.removeFileFromNode(pt.Root, file) } @@ -71,13 +73,60 @@ func (pt *ProjectTree) removeFileFromNode(node *ProjectNode, file string) { } node.Fragments = newFragments + // Re-aggregate documentation + node.Doc = "" + for _, frag := range node.Fragments { + if frag.Doc != "" { + if node.Doc != "" { + node.Doc += "\n\n" + } + node.Doc += frag.Doc + } + } + + // Re-aggregate metadata + node.Metadata = make(map[string]string) + pt.rebuildMetadata(node) + for _, child := range node.Children { pt.removeFileFromNode(child, file) } } +func (pt *ProjectTree) rebuildMetadata(node *ProjectNode) { + for _, frag := range node.Fragments { + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok { + pt.extractFieldMetadata(node, f) + } + } + } +} + +func (pt *ProjectTree) extractFieldMetadata(node *ProjectNode, f *parser.Field) { + key := f.Name + val := "" + switch v := f.Value.(type) { + case *parser.StringValue: + val = v.Value + case *parser.ReferenceValue: + val = v.Value + case *parser.IntValue: + val = v.Raw + } + + if val == "" { + return + } + + // Capture relevant fields + if key == "Class" || key == "Type" || key == "NumberOfElements" || key == "NumberOfDimensions" || key == "DataSource" { + node.Metadata[key] = val + } +} + func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { - pt.RemoveFile(file) // Ensure clean state for this file + pt.RemoveFile(file) node := pt.Root if config.Package != nil { @@ -93,6 +142,7 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { RealName: part, Children: make(map[string]*ProjectNode), Parent: node, + Metadata: make(map[string]string), } } node = node.Children[part] @@ -105,10 +155,13 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { } for _, def := range config.Definitions { + doc := pt.findDoc(config.Comments, def.Pos()) + switch d := def.(type) { case *parser.Field: fileFragment.Definitions = append(fileFragment.Definitions, d) pt.indexValue(file, d.Value) + // Metadata update not really relevant for package node usually, but consistency case *parser.ObjectNode: norm := NormalizeName(d.Name) if _, ok := node.Children[norm]; !ok { @@ -117,13 +170,22 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { RealName: d.Name, Children: make(map[string]*ProjectNode), Parent: node, + Metadata: make(map[string]string), } } child := node.Children[norm] if child.RealName == norm && d.Name != norm { child.RealName = d.Name } - pt.addObjectFragment(child, file, d) + + if doc != "" { + if child.Doc != "" { + child.Doc += "\n\n" + } + child.Doc += doc + } + + pt.addObjectFragment(child, file, d, doc, config.Comments) } } @@ -132,18 +194,22 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { } } -func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode) { +func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode, doc string, comments []parser.Comment) { frag := &Fragment{ File: file, IsObject: true, ObjectPos: obj.Position, + Doc: doc, } for _, def := range obj.Subnode.Definitions { + subDoc := pt.findDoc(comments, def.Pos()) + switch d := def.(type) { case *parser.Field: frag.Definitions = append(frag.Definitions, d) pt.indexValue(file, d.Value) + pt.extractFieldMetadata(node, d) case *parser.ObjectNode: norm := NormalizeName(d.Name) if _, ok := node.Children[norm]; !ok { @@ -152,19 +218,66 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa RealName: d.Name, Children: make(map[string]*ProjectNode), Parent: node, + Metadata: make(map[string]string), } } child := node.Children[norm] if child.RealName == norm && d.Name != norm { child.RealName = d.Name } - pt.addObjectFragment(child, file, d) + + if subDoc != "" { + if child.Doc != "" { + child.Doc += "\n\n" + } + child.Doc += subDoc + } + + pt.addObjectFragment(child, file, d, subDoc, comments) } } node.Fragments = append(node.Fragments, frag) } +func (pt *ProjectTree) findDoc(comments []parser.Comment, pos parser.Position) string { + var docBuilder strings.Builder + targetLine := pos.Line - 1 + var docIndices []int + + for i := len(comments) - 1; i >= 0; i-- { + c := comments[i] + if c.Position.Line > pos.Line { + continue + } + if c.Position.Line == pos.Line { + continue + } + + if c.Position.Line == targetLine { + if c.Doc { + docIndices = append(docIndices, i) + targetLine-- + } else { + break + } + } else if c.Position.Line < targetLine { + break + } + } + + for i := len(docIndices) - 1; i >= 0; i-- { + txt := strings.TrimPrefix(comments[docIndices[i]].Text, "//#") + txt = strings.TrimSpace(txt) + if docBuilder.Len() > 0 { + docBuilder.WriteString("\n") + } + docBuilder.WriteString(txt) + } + + return docBuilder.String() +} + func (pt *ProjectTree) indexValue(file string, val parser.Value) { switch v := val.(type) { case *parser.ReferenceValue: @@ -199,7 +312,6 @@ func (pt *ProjectTree) findNode(root *ProjectNode, name string) *ProjectNode { return nil } -// QueryResult holds the result of a query at a position type QueryResult struct { Node *ProjectNode Field *parser.Field @@ -207,38 +319,29 @@ type QueryResult struct { } func (pt *ProjectTree) Query(file string, line, col int) *QueryResult { - // 1. Check References for i := range pt.References { ref := &pt.References[i] if ref.File == file { - // Check if pos is within reference range - // Approx length if line == ref.Position.Line && col >= ref.Position.Column && col < ref.Position.Column+len(ref.Name) { return &QueryResult{Reference: ref} } } } - // 2. Check Definitions (traverse tree) return pt.queryNode(pt.Root, file, line, col) } func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int) *QueryResult { for _, frag := range node.Fragments { if frag.File == file { - // Check Object definition itself if frag.IsObject { - // Object definition usually starts at 'Name'. - // Position is start of Name. if line == frag.ObjectPos.Line && col >= frag.ObjectPos.Column && col < frag.ObjectPos.Column+len(node.RealName) { return &QueryResult{Node: node} } } - // Check definitions in fragment for _, def := range frag.Definitions { if f, ok := def.(*parser.Field); ok { - // Check field name range if line == f.Position.Line && col >= f.Position.Column && col < f.Position.Column+len(f.Name) { return &QueryResult{Field: f} } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 0362dfa..d4a74a3 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -174,7 +174,6 @@ func handleDidChange(params DidChangeTextDocumentParams) { if len(params.ContentChanges) == 0 { return } - // Full sync: text is in ContentChanges[0].Text text := params.ContentChanges[0].Text path := uriToPath(params.TextDocument.URI) p := parser.NewParser(text) @@ -187,7 +186,6 @@ func handleDidChange(params DidChangeTextDocumentParams) { func handleHover(params HoverParams) *Hover { path := uriToPath(params.TextDocument.URI) - // LSP 0-based to Parser 1-based line := params.Position.Line + 1 col := params.Position.Character + 1 @@ -199,28 +197,26 @@ func handleHover(params HoverParams) *Hover { var content string if res.Node != nil { - // Try to find Class field - class := "Unknown" - for _, frag := range res.Node.Fragments { - for _, def := range frag.Definitions { - if f, ok := def.(*parser.Field); ok && f.Name == "Class" { - if s, ok := f.Value.(*parser.StringValue); ok { - class = s.Value - } else if r, ok := f.Value.(*parser.ReferenceValue); ok { - class = r.Value - } - } - } - } - content = fmt.Sprintf("**Object**: `%s`\n\n**Class**: `%s`", res.Node.RealName, class) + content = formatNodeInfo(res.Node) } else if res.Field != nil { content = fmt.Sprintf("**Field**: `%s`", res.Field.Name) } else if res.Reference != nil { targetName := "Unresolved" + fullInfo := "" + targetDoc := "" + if res.Reference.Target != nil { targetName = res.Reference.Target.RealName + targetDoc = res.Reference.Target.Doc + fullInfo = formatNodeInfo(res.Reference.Target) } + content = fmt.Sprintf("**Reference**: `%s` -> `%s`", res.Reference.Name, targetName) + if fullInfo != "" { + content += fmt.Sprintf("\n\n---\n%s", fullInfo) + } else if targetDoc != "" { // Fallback if formatNodeInfo returned empty (unlikely) + content += fmt.Sprintf("\n\n%s", targetDoc) + } } if content == "" { @@ -235,6 +231,42 @@ func handleHover(params HoverParams) *Hover { } } +func formatNodeInfo(node *index.ProjectNode) string { + class := node.Metadata["Class"] + if class == "" { + class = "Unknown" + } + + info := fmt.Sprintf("**Object**: `%s`\n\n**Class**: `%s`", node.RealName, class) + + // Check if it's a Signal (has Type or DataSource) + typ := node.Metadata["Type"] + ds := node.Metadata["DataSource"] + + if typ != "" || ds != "" { + sigInfo := "\n" + if typ != "" { + sigInfo += fmt.Sprintf("**Type**: `%s` ", typ) + } + if ds != "" { + sigInfo += fmt.Sprintf("**DataSource**: `%s` ", ds) + } + + // Size + dims := node.Metadata["NumberOfDimensions"] +elems := node.Metadata["NumberOfElements"] + if dims != "" || elems != "" { + sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims) + } + info += sigInfo + } + + if node.Doc != "" { + info += fmt.Sprintf("\n\n%s", node.Doc) + } + return info +} + func respond(id interface{}, result interface{}) { msg := JsonRpcMessage{ Jsonrpc: "2.0", diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 03b0b2c..b723908 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -62,11 +62,16 @@ func (v *Validator) validateNode(node *index.ProjectNode) { // Root node usually doesn't have a name or is implicit if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') { hasClass := false + hasType := false for _, frag := range node.Fragments { for _, def := range frag.Definitions { - if f, ok := def.(*parser.Field); ok && f.Name == "Class" { - hasClass = true - break + if f, ok := def.(*parser.Field); ok { + if f.Name == "Class" { + hasClass = true + } + if f.Name == "Type" { + hasType = true + } } } if hasClass { @@ -74,7 +79,7 @@ func (v *Validator) validateNode(node *index.ProjectNode) { } } - if !hasClass { + if !hasClass && !hasType { // Report error on the first fragment's position pos := parser.Position{Line: 1, Column: 1} file := "" @@ -84,7 +89,7 @@ func (v *Validator) validateNode(node *index.ProjectNode) { } v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, - Message: fmt.Sprintf("Node %s is an object and must contain a 'Class' field", node.RealName), + Message: fmt.Sprintf("Node %s is an object and must contain a 'Class' field (or be a Signal with 'Type')", node.RealName), Position: pos, File: file, }) diff --git a/mdt b/mdt index bbff76a..8b863fc 100755 Binary files a/mdt and b/mdt differ diff --git a/specification.md b/specification.md index 52e0587..db44817 100644 --- a/specification.md +++ b/specification.md @@ -74,6 +74,7 @@ The LSP server should provide the following capabilities: - **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object. - **Constraint**: These nodes *must* contain a field named `Class` within their subnode definition. +- **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field. - **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. - **Structure**: A configuration is composed by one or more definitions. diff --git a/test/integration_test.go b/test/integration_test.go index bae05ad..9780154 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -79,6 +79,30 @@ func TestCheckDuplicate(t *testing.T) { } } +func TestSignalNoClassValidation(t *testing.T) { + inputFile := "integration/signal_no_class.marte" + content, err := ioutil.ReadFile(inputFile) + if err != nil { + t.Fatalf("Failed to read %s: %v", inputFile, err) + } + + p := parser.NewParser(string(content)) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile(inputFile, config) + + v := validator.NewValidator(idx) + v.ValidateProject() + + if len(v.Diagnostics) > 0 { + t.Errorf("Expected no errors for signal without Class, but got: %v", v.Diagnostics) + } +} + func TestFmtCommand(t *testing.T) { inputFile := "integration/fmt.marte" content, err := ioutil.ReadFile(inputFile)