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
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 {

View File

@@ -92,6 +92,8 @@ type VersionedTextDocumentIdentifier struct {
}
type TextDocumentContentChangeEvent struct {
Range *Range `json:"range,omitempty"`
RangeLength int `json:"rangeLength,omitempty"`
Text string `json:"text"`
}
@@ -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)
}
}
}

View File

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

View File

@@ -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 {
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
}

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(`
package schema
#Classes: {
InOutReader: { #direction: "INOUT" }
InOutReader: { #meta: direction: "INOUT" }
}
`)
val := lsp.GlobalSchema.Context.CompileBytes(custom)

View File

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

View File

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