diff --git a/internal/index/index.go b/internal/index/index.go index a93a8ce..bd985a0 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -13,6 +13,7 @@ type ProjectTree struct { References []Reference IsolatedFiles map[string]*ProjectNode GlobalPragmas map[string][]string + NodeMap map[string][]*ProjectNode } func (pt *ProjectTree) ScanDirectory(rootPath string) error { @@ -385,7 +386,19 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) { } } +func (pt *ProjectTree) RebuildIndex() { + pt.NodeMap = make(map[string][]*ProjectNode) + visitor := func(n *ProjectNode) { + pt.NodeMap[n.Name] = append(pt.NodeMap[n.Name], n) + if n.RealName != n.Name { + pt.NodeMap[n.RealName] = append(pt.NodeMap[n.RealName], n) + } + } + pt.Walk(visitor) +} + func (pt *ProjectTree) ResolveReferences() { + pt.RebuildIndex() for i := range pt.References { ref := &pt.References[i] if isoNode, ok := pt.IsolatedFiles[ref.File]; ok { @@ -397,14 +410,21 @@ func (pt *ProjectTree) ResolveReferences() { } func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode { + if pt.NodeMap == nil { + pt.RebuildIndex() + } + if strings.Contains(name, ".") { parts := strings.Split(name, ".") rootName := parts[0] - var candidates []*ProjectNode - pt.findAllNodes(root, rootName, &candidates) + candidates := pt.NodeMap[rootName] for _, cand := range candidates { + if !pt.isDescendant(cand, root) { + continue + } + curr := cand valid := true for i := 1; i < len(parts); i++ { @@ -426,26 +446,33 @@ func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(* return nil } - if root.RealName == name || root.Name == name { - if predicate == nil || predicate(root) { - return root + candidates := pt.NodeMap[name] + for _, cand := range candidates { + if !pt.isDescendant(cand, root) { + continue } - } - for _, child := range root.Children { - if res := pt.FindNode(child, name, predicate); res != nil { - return res + if predicate == nil || predicate(cand) { + return cand } } return nil } -func (pt *ProjectTree) findAllNodes(root *ProjectNode, name string, results *[]*ProjectNode) { - if root.RealName == name || root.Name == name { - *results = append(*results, root) +func (pt *ProjectTree) isDescendant(node, root *ProjectNode) bool { + if node == root { + return true } - for _, child := range root.Children { - pt.findAllNodes(child, name, results) + if root == nil { + return true } + curr := node + for curr != nil { + if curr == root { + return true + } + curr = curr.Parent + } + return false } type QueryResult struct { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index d5ec5b2..870bc82 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -92,7 +92,9 @@ type VersionedTextDocumentIdentifier struct { } type TextDocumentContentChangeEvent struct { - Text string `json:"text"` + Range *Range `json:"range,omitempty"` + RangeLength int `json:"rangeLength,omitempty"` + Text string `json:"text"` } type HoverParams struct { @@ -253,7 +255,7 @@ func HandleMessage(msg *JsonRpcMessage) { respond(msg.ID, map[string]any{ "capabilities": map[string]any{ - "textDocumentSync": 1, // Full sync + "textDocumentSync": 2, // Incremental sync "hoverProvider": true, "definitionProvider": true, "referencesProvider": true, @@ -347,28 +349,76 @@ func HandleDidOpen(params DidOpenTextDocumentParams) { } func HandleDidChange(params DidChangeTextDocumentParams) { - if len(params.ContentChanges) == 0 { - return + uri := params.TextDocument.URI + text, ok := Documents[uri] + if !ok { + // If not found, rely on full sync being first or error } - text := params.ContentChanges[0].Text - Documents[params.TextDocument.URI] = text - path := uriToPath(params.TextDocument.URI) + + for _, change := range params.ContentChanges { + if change.Range == nil { + text = change.Text + } else { + text = applyContentChange(text, change) + } + } + + Documents[uri] = text + path := uriToPath(uri) p := parser.NewParser(text) config, err := p.Parse() if err != nil { - publishParserError(params.TextDocument.URI, err) + publishParserError(uri, err) } else { - publishParserError(params.TextDocument.URI, nil) + publishParserError(uri, nil) } if config != nil { Tree.AddFile(path, config) Tree.ResolveReferences() - runValidation(params.TextDocument.URI) + runValidation(uri) } } +func applyContentChange(text string, change TextDocumentContentChangeEvent) string { + startOffset := offsetAt(text, change.Range.Start) + endOffset := offsetAt(text, change.Range.End) + + if startOffset == -1 || endOffset == -1 { + return text + } + + return text[:startOffset] + change.Text + text[endOffset:] +} + +func offsetAt(text string, pos Position) int { + line := 0 + col := 0 + for i, r := range text { + if line == pos.Line && col == pos.Character { + return i + } + if line > pos.Line { + break + } + if r == '\n' { + line++ + col = 0 + } else { + if r >= 0x10000 { + col += 2 + } else { + col++ + } + } + } + if line == pos.Line && col == pos.Character { + return len(text) + } + return -1 +} + func HandleFormatting(params DocumentFormattingParams) []TextEdit { uri := params.TextDocument.URI text, ok := Documents[uri] @@ -646,7 +696,7 @@ func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList { dir := "NIL" if GlobalSchema != nil { - classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.#direction", cls)) + classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", cls)) val := GlobalSchema.Value.LookupPath(classPath) if val.Err() == nil { var s string @@ -1161,7 +1211,20 @@ func formatNodeInfo(node *index.ProjectNode) string { curr := container for curr != nil { if isGAM(curr) { - gams = append(gams, curr.RealName) + suffix := "" + p := container + for p != nil && p != curr { + if p.Name == "InputSignals" { + suffix = " (Input)" + break + } + if p.Name == "OutputSignals" { + suffix = " (Output)" + break + } + p = p.Parent + } + gams = append(gams, curr.RealName+suffix) break } curr = curr.Parent @@ -1175,7 +1238,11 @@ func formatNodeInfo(node *index.ProjectNode) string { 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) + suffix := " (Input)" + if n.Parent.Name == "OutputSignals" { + suffix = " (Output)" + } + gams = append(gams, n.Parent.Parent.RealName+suffix) } } } diff --git a/internal/schema/marte.cue b/internal/schema/marte.cue index 2a7209a..5c2d49e 100644 --- a/internal/schema/marte.cue +++ b/internal/schema/marte.cue @@ -43,8 +43,8 @@ package schema ... } TimingDataSource: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } IOGAM: { @@ -67,84 +67,84 @@ package schema FileDataSource: { Filename: string Format?: string - #multithreaded: bool | *false - #direction: "INOUT" + #meta: multithreaded: bool | *false + #meta: direction: "INOUT" ... } LoggerDataSource: { - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } DANStream: { Timeout?: int - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } EPICSCAInput: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } EPICSCAOutput: { - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } EPICSPVAInput: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } EPICSPVAOutput: { - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } SDNSubscriber: { Address: string Port: int Interface?: string - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } SDNPublisher: { Address: string Port: int Interface?: string - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } UDPReceiver: { Port: int Address?: string - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } UDPSender: { Destination: string - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } FileReader: { Filename: string Format?: string Interpolate?: string - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } FileWriter: { Filename: string Format?: string StoreOnTrigger?: int - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } OrderedClass: { @@ -187,8 +187,8 @@ package schema TriggeredIOGAM: {...} WaveformGAM: {...} DAN: { - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } LinuxTimer: { @@ -199,13 +199,13 @@ package schema CPUMask?: int TimeProvider?: {...} Signals: {...} - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } LinkDataSource: { - #multithreaded: bool | *false - #direction: "INOUT" + #meta: multithreaded: bool | *false + #meta: direction: "INOUT" ... } MDSReader: { @@ -213,8 +213,8 @@ package schema ShotNumber: int Frequency: float | int Signals: {...} - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } MDSWriter: { @@ -230,74 +230,74 @@ package schema NumberOfPostTriggers?: int Signals: {...} Messages?: {...} - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } NI1588TimeStamp: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } NI6259ADC: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } NI6259DAC: { - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } NI6259DIO: { - #multithreaded: bool | *false - #direction: "INOUT" + #meta: multithreaded: bool | *false + #meta: direction: "INOUT" ... } NI6368ADC: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } NI6368DAC: { - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } NI6368DIO: { - #multithreaded: bool | *false - #direction: "INOUT" + #meta: multithreaded: bool | *false + #meta: direction: "INOUT" ... } NI9157CircularFifoReader: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } NI9157MxiDataSource: { - #multithreaded: bool | *false - #direction: "INOUT" + #meta: multithreaded: bool | *false + #meta: direction: "INOUT" ... } OPCUADSInput: { - #multithreaded: bool | *false - #direction: "IN" + #meta: multithreaded: bool | *false + #meta: direction: "IN" ... } OPCUADSOutput: { - #multithreaded: bool | *false - #direction: "OUT" + #meta: multithreaded: bool | *false + #meta: direction: "OUT" ... } RealTimeThreadAsyncBridge: { - #direction: "INOUT" - #multithreaded: bool | true + #meta: direction: "INOUT" + #meta: multithreaded: bool | true ... } RealTimeThreadSynchronisation: {...} UARTDataSource: { - #multithreaded: bool | *false - #direction: "INOUT" + #meta: multithreaded: bool | *false + #meta: direction: "INOUT" ... } BaseLib2Wrapper: {...} @@ -307,8 +307,8 @@ package schema OPCUA: {...} SysLogger: {...} GAMDataSource: { - #multithreaded: bool | *false - #direction: "INOUT" + #meta: multithreaded: bool | *false + #meta: direction: "INOUT" ... } } diff --git a/internal/validator/validator.go b/internal/validator/validator.go index d6ce5e0..d2daf68 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -315,8 +315,8 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di dsClass := v.getNodeClass(dsNode) if dsClass != "" { // Lookup class definition in Schema - // path: #Classes.ClassName.direction - path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#direction", dsClass)) + // path: #Classes.ClassName.#meta.direction + path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", dsClass)) val := v.Schema.Value.LookupPath(path) if val.Err() == nil { @@ -875,10 +875,12 @@ func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNo } func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool { - fields := v.getFields(ds) - if mt, ok := fields["#multithreaded"]; ok && len(mt) > 0 { - val := v.getFieldValue(mt[0]) - return val == "true" + if meta, ok := ds.Children["#meta"]; ok { + fields := v.getFields(meta) + if mt, ok := fields["multithreaded"]; ok && len(mt) > 0 { + val := v.getFieldValue(mt[0]) + return val == "true" + } } return false } diff --git a/test/index_test.go b/test/index_test.go new file mode 100644 index 0000000..a33af08 --- /dev/null +++ b/test/index_test.go @@ -0,0 +1,66 @@ +package integration + +import ( + "testing" + + "github.com/marte-community/marte-dev-tools/internal/index" +) + +func TestNodeMap(t *testing.T) { + pt := index.NewProjectTree() + root := pt.Root + + // Create structure: +A -> +B -> +C + nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root} + root.Children["A"] = nodeA + + nodeB := &index.ProjectNode{Name: "B", RealName: "+B", Children: make(map[string]*index.ProjectNode), Parent: nodeA} + nodeA.Children["B"] = nodeB + + nodeC := &index.ProjectNode{Name: "C", RealName: "+C", Children: make(map[string]*index.ProjectNode), Parent: nodeB} + nodeB.Children["C"] = nodeC + + // Rebuild Index + pt.RebuildIndex() + + // Find by Name + found := pt.FindNode(root, "C", nil) + if found != nodeC { + t.Errorf("FindNode(C) failed. Got %v, want %v", found, nodeC) + } + + // Find by RealName + found = pt.FindNode(root, "+C", nil) + if found != nodeC { + t.Errorf("FindNode(+C) failed. Got %v, want %v", found, nodeC) + } + + // Find by Path + found = pt.FindNode(root, "A.B.C", nil) + if found != nodeC { + t.Errorf("FindNode(A.B.C) failed. Got %v, want %v", found, nodeC) + } + + // Find by Path with RealName + found = pt.FindNode(root, "+A.+B.+C", nil) + if found != nodeC { + t.Errorf("FindNode(+A.+B.+C) failed. Got %v, want %v", found, nodeC) + } +} + +func TestResolveReferencesWithMap(t *testing.T) { + pt := index.NewProjectTree() + root := pt.Root + + nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root} + root.Children["A"] = nodeA + + ref := index.Reference{Name: "A", File: "test.marte"} + pt.References = append(pt.References, ref) + + pt.ResolveReferences() + + if pt.References[0].Target != nodeA { + t.Error("ResolveReferences failed to resolve A") + } +} diff --git a/test/lsp_completion_signals_robustness_test.go b/test/lsp_completion_signals_robustness_test.go index 284ee10..99ca3d7 100644 --- a/test/lsp_completion_signals_robustness_test.go +++ b/test/lsp_completion_signals_robustness_test.go @@ -20,7 +20,7 @@ func TestSuggestSignalsRobustness(t *testing.T) { custom := []byte(` package schema #Classes: { - InOutReader: { #direction: "INOUT" } + InOutReader: { #meta: direction: "INOUT" } } `) val := lsp.GlobalSchema.Context.CompileBytes(custom) diff --git a/test/lsp_validation_threading_test.go b/test/lsp_validation_threading_test.go index 423e894..7f2be59 100644 --- a/test/lsp_validation_threading_test.go +++ b/test/lsp_validation_threading_test.go @@ -26,8 +26,10 @@ func TestLSPValidationThreading(t *testing.T) { Class = ReferenceContainer +SharedDS = { Class = GAMDataSource - #direction = "INOUT" - #multithreaded = false + #meta = { + direction = "INOUT" + multithreaded = false + } Signals = { Sig1 = { Type = uint32 } } diff --git a/test/validator_datasource_threading_test.go b/test/validator_datasource_threading_test.go index 0a43a77..f3dde82 100644 --- a/test/validator_datasource_threading_test.go +++ b/test/validator_datasource_threading_test.go @@ -15,16 +15,20 @@ func TestDataSourceThreadingValidation(t *testing.T) { Class = ReferenceContainer +SharedDS = { Class = GAMDataSource - #direction = "INOUT" - #multithreaded = false + #meta = { + direction = "INOUT" + multithreaded = false + } Signals = { Sig1 = { Type = uint32 } } } +MultiDS = { Class = GAMDataSource - #direction = "INOUT" - #multithreaded = true + #meta = { + direction = "INOUT" + multithreaded = true + } Signals = { Sig1 = { Type = uint32 } }