Improved performances and hover

This commit is contained in:
Martino Ferrari
2026-01-27 15:14:47 +01:00
parent 213fc81cfb
commit 15afdc91f4
8 changed files with 274 additions and 106 deletions

View File

@@ -13,6 +13,7 @@ type ProjectTree struct {
References []Reference References []Reference
IsolatedFiles map[string]*ProjectNode IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string GlobalPragmas map[string][]string
NodeMap map[string][]*ProjectNode
} }
func (pt *ProjectTree) ScanDirectory(rootPath string) error { 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() { func (pt *ProjectTree) ResolveReferences() {
pt.RebuildIndex()
for i := range pt.References { for i := range pt.References {
ref := &pt.References[i] ref := &pt.References[i]
if isoNode, ok := pt.IsolatedFiles[ref.File]; ok { 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 { func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode {
if pt.NodeMap == nil {
pt.RebuildIndex()
}
if strings.Contains(name, ".") { if strings.Contains(name, ".") {
parts := strings.Split(name, ".") parts := strings.Split(name, ".")
rootName := parts[0] rootName := parts[0]
var candidates []*ProjectNode candidates := pt.NodeMap[rootName]
pt.findAllNodes(root, rootName, &candidates)
for _, cand := range candidates { for _, cand := range candidates {
if !pt.isDescendant(cand, root) {
continue
}
curr := cand curr := cand
valid := true valid := true
for i := 1; i < len(parts); i++ { for i := 1; i < len(parts); i++ {
@@ -426,26 +446,33 @@ func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*
return nil return nil
} }
if root.RealName == name || root.Name == name { candidates := pt.NodeMap[name]
if predicate == nil || predicate(root) { for _, cand := range candidates {
return root if !pt.isDescendant(cand, root) {
continue
} }
} if predicate == nil || predicate(cand) {
for _, child := range root.Children { return cand
if res := pt.FindNode(child, name, predicate); res != nil {
return res
} }
} }
return nil return nil
} }
func (pt *ProjectTree) findAllNodes(root *ProjectNode, name string, results *[]*ProjectNode) { func (pt *ProjectTree) isDescendant(node, root *ProjectNode) bool {
if root.RealName == name || root.Name == name { if node == root {
*results = append(*results, root) return true
} }
for _, child := range root.Children { if root == nil {
pt.findAllNodes(child, name, results) return true
} }
curr := node
for curr != nil {
if curr == root {
return true
}
curr = curr.Parent
}
return false
} }
type QueryResult struct { type QueryResult struct {

View File

@@ -92,7 +92,9 @@ type VersionedTextDocumentIdentifier struct {
} }
type TextDocumentContentChangeEvent 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 { type HoverParams struct {
@@ -253,7 +255,7 @@ func HandleMessage(msg *JsonRpcMessage) {
respond(msg.ID, map[string]any{ respond(msg.ID, map[string]any{
"capabilities": map[string]any{ "capabilities": map[string]any{
"textDocumentSync": 1, // Full sync "textDocumentSync": 2, // Incremental sync
"hoverProvider": true, "hoverProvider": true,
"definitionProvider": true, "definitionProvider": true,
"referencesProvider": true, "referencesProvider": true,
@@ -347,28 +349,76 @@ func HandleDidOpen(params DidOpenTextDocumentParams) {
} }
func HandleDidChange(params DidChangeTextDocumentParams) { func HandleDidChange(params DidChangeTextDocumentParams) {
if len(params.ContentChanges) == 0 { uri := params.TextDocument.URI
return 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 for _, change := range params.ContentChanges {
path := uriToPath(params.TextDocument.URI) if change.Range == nil {
text = change.Text
} else {
text = applyContentChange(text, change)
}
}
Documents[uri] = text
path := uriToPath(uri)
p := parser.NewParser(text) p := parser.NewParser(text)
config, err := p.Parse() config, err := p.Parse()
if err != nil { if err != nil {
publishParserError(params.TextDocument.URI, err) publishParserError(uri, err)
} else { } else {
publishParserError(params.TextDocument.URI, nil) publishParserError(uri, nil)
} }
if config != nil { if config != nil {
Tree.AddFile(path, config) Tree.AddFile(path, config)
Tree.ResolveReferences() 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 { func HandleFormatting(params DocumentFormattingParams) []TextEdit {
uri := params.TextDocument.URI uri := params.TextDocument.URI
text, ok := Documents[uri] text, ok := Documents[uri]
@@ -646,7 +696,7 @@ func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
dir := "NIL" dir := "NIL"
if GlobalSchema != 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) val := GlobalSchema.Value.LookupPath(classPath)
if val.Err() == nil { if val.Err() == nil {
var s string var s string
@@ -1161,7 +1211,20 @@ func formatNodeInfo(node *index.ProjectNode) string {
curr := container curr := container
for curr != nil { for curr != nil {
if isGAM(curr) { 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 break
} }
curr = curr.Parent curr = curr.Parent
@@ -1175,7 +1238,11 @@ func formatNodeInfo(node *index.ProjectNode) string {
if n.Target == node { if n.Target == node {
if n.Parent != nil && (n.Parent.Name == "InputSignals" || n.Parent.Name == "OutputSignals") { if n.Parent != nil && (n.Parent.Name == "InputSignals" || n.Parent.Name == "OutputSignals") {
if n.Parent.Parent != nil && isGAM(n.Parent.Parent) { 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)
} }
} }
} }

View File

@@ -43,8 +43,8 @@ package schema
... ...
} }
TimingDataSource: { TimingDataSource: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
IOGAM: { IOGAM: {
@@ -67,84 +67,84 @@ package schema
FileDataSource: { FileDataSource: {
Filename: string Filename: string
Format?: string Format?: string
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "INOUT" #meta: direction: "INOUT"
... ...
} }
LoggerDataSource: { LoggerDataSource: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
DANStream: { DANStream: {
Timeout?: int Timeout?: int
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
EPICSCAInput: { EPICSCAInput: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
EPICSCAOutput: { EPICSCAOutput: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
EPICSPVAInput: { EPICSPVAInput: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
EPICSPVAOutput: { EPICSPVAOutput: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
SDNSubscriber: { SDNSubscriber: {
Address: string Address: string
Port: int Port: int
Interface?: string Interface?: string
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
SDNPublisher: { SDNPublisher: {
Address: string Address: string
Port: int Port: int
Interface?: string Interface?: string
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
UDPReceiver: { UDPReceiver: {
Port: int Port: int
Address?: string Address?: string
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
UDPSender: { UDPSender: {
Destination: string Destination: string
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
FileReader: { FileReader: {
Filename: string Filename: string
Format?: string Format?: string
Interpolate?: string Interpolate?: string
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
FileWriter: { FileWriter: {
Filename: string Filename: string
Format?: string Format?: string
StoreOnTrigger?: int StoreOnTrigger?: int
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
OrderedClass: { OrderedClass: {
@@ -187,8 +187,8 @@ package schema
TriggeredIOGAM: {...} TriggeredIOGAM: {...}
WaveformGAM: {...} WaveformGAM: {...}
DAN: { DAN: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
LinuxTimer: { LinuxTimer: {
@@ -199,13 +199,13 @@ package schema
CPUMask?: int CPUMask?: int
TimeProvider?: {...} TimeProvider?: {...}
Signals: {...} Signals: {...}
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
LinkDataSource: { LinkDataSource: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "INOUT" #meta: direction: "INOUT"
... ...
} }
MDSReader: { MDSReader: {
@@ -213,8 +213,8 @@ package schema
ShotNumber: int ShotNumber: int
Frequency: float | int Frequency: float | int
Signals: {...} Signals: {...}
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
MDSWriter: { MDSWriter: {
@@ -230,74 +230,74 @@ package schema
NumberOfPostTriggers?: int NumberOfPostTriggers?: int
Signals: {...} Signals: {...}
Messages?: {...} Messages?: {...}
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
NI1588TimeStamp: { NI1588TimeStamp: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
NI6259ADC: { NI6259ADC: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
NI6259DAC: { NI6259DAC: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
NI6259DIO: { NI6259DIO: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "INOUT" #meta: direction: "INOUT"
... ...
} }
NI6368ADC: { NI6368ADC: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
NI6368DAC: { NI6368DAC: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
NI6368DIO: { NI6368DIO: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "INOUT" #meta: direction: "INOUT"
... ...
} }
NI9157CircularFifoReader: { NI9157CircularFifoReader: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
NI9157MxiDataSource: { NI9157MxiDataSource: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "INOUT" #meta: direction: "INOUT"
... ...
} }
OPCUADSInput: { OPCUADSInput: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "IN" #meta: direction: "IN"
... ...
} }
OPCUADSOutput: { OPCUADSOutput: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "OUT" #meta: direction: "OUT"
... ...
} }
RealTimeThreadAsyncBridge: { RealTimeThreadAsyncBridge: {
#direction: "INOUT" #meta: direction: "INOUT"
#multithreaded: bool | true #meta: multithreaded: bool | true
... ...
} }
RealTimeThreadSynchronisation: {...} RealTimeThreadSynchronisation: {...}
UARTDataSource: { UARTDataSource: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "INOUT" #meta: direction: "INOUT"
... ...
} }
BaseLib2Wrapper: {...} BaseLib2Wrapper: {...}
@@ -307,8 +307,8 @@ package schema
OPCUA: {...} OPCUA: {...}
SysLogger: {...} SysLogger: {...}
GAMDataSource: { GAMDataSource: {
#multithreaded: bool | *false #meta: multithreaded: bool | *false
#direction: "INOUT" #meta: direction: "INOUT"
... ...
} }
} }

View File

@@ -315,8 +315,8 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
dsClass := v.getNodeClass(dsNode) dsClass := v.getNodeClass(dsNode)
if dsClass != "" { if dsClass != "" {
// Lookup class definition in Schema // Lookup class definition in Schema
// path: #Classes.ClassName.direction // path: #Classes.ClassName.#meta.direction
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#direction", dsClass)) path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", dsClass))
val := v.Schema.Value.LookupPath(path) val := v.Schema.Value.LookupPath(path)
if val.Err() == nil { 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 { func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool {
fields := v.getFields(ds) if meta, ok := ds.Children["#meta"]; ok {
if mt, ok := fields["#multithreaded"]; ok && len(mt) > 0 { fields := v.getFields(meta)
val := v.getFieldValue(mt[0]) if mt, ok := fields["multithreaded"]; ok && len(mt) > 0 {
return val == "true" val := v.getFieldValue(mt[0])
return val == "true"
}
} }
return false return false
} }

66
test/index_test.go Normal file
View File

@@ -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")
}
}

View File

@@ -20,7 +20,7 @@ func TestSuggestSignalsRobustness(t *testing.T) {
custom := []byte(` custom := []byte(`
package schema package schema
#Classes: { #Classes: {
InOutReader: { #direction: "INOUT" } InOutReader: { #meta: direction: "INOUT" }
} }
`) `)
val := lsp.GlobalSchema.Context.CompileBytes(custom) val := lsp.GlobalSchema.Context.CompileBytes(custom)

View File

@@ -26,8 +26,10 @@ func TestLSPValidationThreading(t *testing.T) {
Class = ReferenceContainer Class = ReferenceContainer
+SharedDS = { +SharedDS = {
Class = GAMDataSource Class = GAMDataSource
#direction = "INOUT" #meta = {
#multithreaded = false direction = "INOUT"
multithreaded = false
}
Signals = { Signals = {
Sig1 = { Type = uint32 } Sig1 = { Type = uint32 }
} }

View File

@@ -15,16 +15,20 @@ func TestDataSourceThreadingValidation(t *testing.T) {
Class = ReferenceContainer Class = ReferenceContainer
+SharedDS = { +SharedDS = {
Class = GAMDataSource Class = GAMDataSource
#direction = "INOUT" #meta = {
#multithreaded = false direction = "INOUT"
multithreaded = false
}
Signals = { Signals = {
Sig1 = { Type = uint32 } Sig1 = { Type = uint32 }
} }
} }
+MultiDS = { +MultiDS = {
Class = GAMDataSource Class = GAMDataSource
#direction = "INOUT" #meta = {
#multithreaded = true direction = "INOUT"
multithreaded = true
}
Signals = { Signals = {
Sig1 = { Type = uint32 } Sig1 = { Type = uint32 }
} }