bootstrapped
This commit is contained in:
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user