Compare commits
4 Commits
77fe3e9cac
...
e3c84fcf60
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3c84fcf60 | ||
|
|
4a515fd6c3 | ||
|
|
14cba1b530 | ||
|
|
462c832651 |
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/formatter"
|
||||
@@ -46,10 +47,10 @@ type CompletionList struct {
|
||||
Items []CompletionItem `json:"items"`
|
||||
}
|
||||
|
||||
var tree = index.NewProjectTree()
|
||||
var documents = make(map[string]string)
|
||||
var projectRoot string
|
||||
var globalSchema *schema.Schema
|
||||
var Tree = index.NewProjectTree()
|
||||
var Documents = make(map[string]string)
|
||||
var ProjectRoot string
|
||||
var GlobalSchema *schema.Schema
|
||||
|
||||
type JsonRpcMessage struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
@@ -183,7 +184,7 @@ func RunServer() {
|
||||
continue
|
||||
}
|
||||
|
||||
handleMessage(msg)
|
||||
HandleMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +214,7 @@ func readMessage(reader *bufio.Reader) (*JsonRpcMessage, error) {
|
||||
return &msg, err
|
||||
}
|
||||
|
||||
func handleMessage(msg *JsonRpcMessage) {
|
||||
func HandleMessage(msg *JsonRpcMessage) {
|
||||
switch msg.Method {
|
||||
case "initialize":
|
||||
var params InitializeParams
|
||||
@@ -226,13 +227,13 @@ func handleMessage(msg *JsonRpcMessage) {
|
||||
}
|
||||
|
||||
if root != "" {
|
||||
projectRoot = root
|
||||
ProjectRoot = root
|
||||
logger.Printf("Scanning workspace: %s\n", root)
|
||||
if err := tree.ScanDirectory(root); err != nil {
|
||||
if err := Tree.ScanDirectory(root); err != nil {
|
||||
logger.Printf("ScanDirectory failed: %v\n", err)
|
||||
}
|
||||
tree.ResolveReferences()
|
||||
globalSchema = schema.LoadFullSchema(projectRoot)
|
||||
Tree.ResolveReferences()
|
||||
GlobalSchema = schema.LoadFullSchema(ProjectRoot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,18 +258,18 @@ func handleMessage(msg *JsonRpcMessage) {
|
||||
case "textDocument/didOpen":
|
||||
var params DidOpenTextDocumentParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
handleDidOpen(params)
|
||||
HandleDidOpen(params)
|
||||
}
|
||||
case "textDocument/didChange":
|
||||
var params DidChangeTextDocumentParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
handleDidChange(params)
|
||||
HandleDidChange(params)
|
||||
}
|
||||
case "textDocument/hover":
|
||||
var params HoverParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
logger.Printf("Hover: %s:%d", params.TextDocument.URI, params.Position.Line)
|
||||
res := handleHover(params)
|
||||
res := HandleHover(params)
|
||||
if res != nil {
|
||||
logger.Printf("Res: %v", res.Contents)
|
||||
} else {
|
||||
@@ -282,22 +283,22 @@ func handleMessage(msg *JsonRpcMessage) {
|
||||
case "textDocument/definition":
|
||||
var params DefinitionParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, handleDefinition(params))
|
||||
respond(msg.ID, HandleDefinition(params))
|
||||
}
|
||||
case "textDocument/references":
|
||||
var params ReferenceParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, handleReferences(params))
|
||||
respond(msg.ID, HandleReferences(params))
|
||||
}
|
||||
case "textDocument/completion":
|
||||
var params CompletionParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, handleCompletion(params))
|
||||
respond(msg.ID, HandleCompletion(params))
|
||||
}
|
||||
case "textDocument/formatting":
|
||||
var params DocumentFormattingParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, handleFormatting(params))
|
||||
respond(msg.ID, HandleFormatting(params))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,9 +307,9 @@ func uriToPath(uri string) string {
|
||||
return strings.TrimPrefix(uri, "file://")
|
||||
}
|
||||
|
||||
func handleDidOpen(params DidOpenTextDocumentParams) {
|
||||
func HandleDidOpen(params DidOpenTextDocumentParams) {
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
documents[params.TextDocument.URI] = params.TextDocument.Text
|
||||
Documents[params.TextDocument.URI] = params.TextDocument.Text
|
||||
p := parser.NewParser(params.TextDocument.Text)
|
||||
config, err := p.Parse()
|
||||
|
||||
@@ -319,18 +320,18 @@ func handleDidOpen(params DidOpenTextDocumentParams) {
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
tree.AddFile(path, config)
|
||||
tree.ResolveReferences()
|
||||
Tree.AddFile(path, config)
|
||||
Tree.ResolveReferences()
|
||||
runValidation(params.TextDocument.URI)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDidChange(params DidChangeTextDocumentParams) {
|
||||
func HandleDidChange(params DidChangeTextDocumentParams) {
|
||||
if len(params.ContentChanges) == 0 {
|
||||
return
|
||||
}
|
||||
text := params.ContentChanges[0].Text
|
||||
documents[params.TextDocument.URI] = text
|
||||
Documents[params.TextDocument.URI] = text
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
p := parser.NewParser(text)
|
||||
config, err := p.Parse()
|
||||
@@ -342,15 +343,15 @@ func handleDidChange(params DidChangeTextDocumentParams) {
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
tree.AddFile(path, config)
|
||||
tree.ResolveReferences()
|
||||
Tree.AddFile(path, config)
|
||||
Tree.ResolveReferences()
|
||||
runValidation(params.TextDocument.URI)
|
||||
}
|
||||
}
|
||||
|
||||
func handleFormatting(params DocumentFormattingParams) []TextEdit {
|
||||
func HandleFormatting(params DocumentFormattingParams) []TextEdit {
|
||||
uri := params.TextDocument.URI
|
||||
text, ok := documents[uri]
|
||||
text, ok := Documents[uri]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -382,7 +383,7 @@ func handleFormatting(params DocumentFormattingParams) []TextEdit {
|
||||
}
|
||||
|
||||
func runValidation(uri string) {
|
||||
v := validator.NewValidator(tree, projectRoot)
|
||||
v := validator.NewValidator(Tree, ProjectRoot)
|
||||
v.ValidateProject()
|
||||
v.CheckUnused()
|
||||
|
||||
@@ -391,7 +392,7 @@ func runValidation(uri string) {
|
||||
|
||||
// Collect all known files to ensure we clear diagnostics for fixed files
|
||||
knownFiles := make(map[string]bool)
|
||||
collectFiles(tree.Root, knownFiles)
|
||||
collectFiles(Tree.Root, knownFiles)
|
||||
|
||||
// Initialize all known files with empty diagnostics
|
||||
for f := range knownFiles {
|
||||
@@ -500,12 +501,12 @@ func mustMarshal(v any) json.RawMessage {
|
||||
return b
|
||||
}
|
||||
|
||||
func handleHover(params HoverParams) *Hover {
|
||||
func HandleHover(params HoverParams) *Hover {
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
line := params.Position.Line + 1
|
||||
col := params.Position.Character + 1
|
||||
|
||||
res := tree.Query(path, line, col)
|
||||
res := Tree.Query(path, line, col)
|
||||
if res == nil {
|
||||
logger.Printf("No object/node/reference found")
|
||||
return nil
|
||||
@@ -552,10 +553,10 @@ func handleHover(params HoverParams) *Hover {
|
||||
}
|
||||
}
|
||||
|
||||
func handleCompletion(params CompletionParams) *CompletionList {
|
||||
func HandleCompletion(params CompletionParams) *CompletionList {
|
||||
uri := params.TextDocument.URI
|
||||
path := uriToPath(uri)
|
||||
text, ok := documents[uri]
|
||||
text, ok := Documents[uri]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -575,22 +576,30 @@ func handleCompletion(params CompletionParams) *CompletionList {
|
||||
|
||||
// Case 1: Assigning a value (Ends with "=" or "= ")
|
||||
if strings.Contains(prefix, "=") {
|
||||
parts := strings.Split(prefix, "=")
|
||||
key := strings.TrimSpace(parts[len(parts)-2])
|
||||
lastIdx := strings.LastIndex(prefix, "=")
|
||||
beforeEqual := prefix[:lastIdx]
|
||||
|
||||
// Find the last identifier before '='
|
||||
key := ""
|
||||
re := regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9_\-]*`)
|
||||
matches := re.FindAllString(beforeEqual, -1)
|
||||
if len(matches) > 0 {
|
||||
key = matches[len(matches)-1]
|
||||
}
|
||||
|
||||
if key == "Class" {
|
||||
return suggestClasses()
|
||||
}
|
||||
|
||||
container := tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
|
||||
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
|
||||
if container != nil {
|
||||
return suggestFieldValues(container, key)
|
||||
return suggestFieldValues(container, key, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Case 2: Typing a key inside an object
|
||||
container := tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
|
||||
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
|
||||
if container != nil {
|
||||
return suggestFields(container)
|
||||
}
|
||||
@@ -599,11 +608,11 @@ func handleCompletion(params CompletionParams) *CompletionList {
|
||||
}
|
||||
|
||||
func suggestClasses() *CompletionList {
|
||||
if globalSchema == nil {
|
||||
if GlobalSchema == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
classesVal := globalSchema.Value.LookupPath(cue.ParsePath("#Classes"))
|
||||
classesVal := GlobalSchema.Value.LookupPath(cue.ParsePath("#Classes"))
|
||||
if classesVal.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -638,11 +647,11 @@ func suggestFields(container *index.ProjectNode) *CompletionList {
|
||||
}}}
|
||||
}
|
||||
|
||||
if globalSchema == nil {
|
||||
if GlobalSchema == nil {
|
||||
return nil
|
||||
}
|
||||
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s", cls))
|
||||
classVal := globalSchema.Value.LookupPath(classPath)
|
||||
classVal := GlobalSchema.Value.LookupPath(classPath)
|
||||
if classVal.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -700,19 +709,31 @@ func suggestFields(container *index.ProjectNode) *CompletionList {
|
||||
return &CompletionList{Items: items}
|
||||
}
|
||||
|
||||
func suggestFieldValues(container *index.ProjectNode, field string) *CompletionList {
|
||||
func suggestFieldValues(container *index.ProjectNode, field string, path string) *CompletionList {
|
||||
var root *index.ProjectNode
|
||||
if iso, ok := Tree.IsolatedFiles[path]; ok {
|
||||
root = iso
|
||||
} else {
|
||||
root = Tree.Root
|
||||
}
|
||||
|
||||
if field == "DataSource" {
|
||||
return suggestObjects("DataSource")
|
||||
return suggestObjects(root, "DataSource")
|
||||
}
|
||||
if field == "Functions" {
|
||||
return suggestObjects("GAM")
|
||||
return suggestObjects(root, "GAM")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func suggestObjects(filter string) *CompletionList {
|
||||
func suggestObjects(root *index.ProjectNode, filter string) *CompletionList {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
var items []CompletionItem
|
||||
tree.Walk(func(node *index.ProjectNode) {
|
||||
|
||||
var walk func(*index.ProjectNode)
|
||||
walk = func(node *index.ProjectNode) {
|
||||
match := false
|
||||
if filter == "GAM" {
|
||||
if isGAM(node) {
|
||||
@@ -726,12 +747,18 @@ func suggestObjects(filter string) *CompletionList {
|
||||
|
||||
if match {
|
||||
items = append(items, CompletionItem{
|
||||
Label: node.RealName,
|
||||
Label: node.Name,
|
||||
Kind: 6, // Variable
|
||||
Detail: node.Metadata["Class"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
for _, child := range node.Children {
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
|
||||
walk(root)
|
||||
return &CompletionList{Items: items}
|
||||
}
|
||||
|
||||
@@ -752,12 +779,12 @@ func isDataSource(node *index.ProjectNode) bool {
|
||||
return hasSignals
|
||||
}
|
||||
|
||||
func handleDefinition(params DefinitionParams) any {
|
||||
func HandleDefinition(params DefinitionParams) any {
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
line := params.Position.Line + 1
|
||||
col := params.Position.Character + 1
|
||||
|
||||
res := tree.Query(path, line, col)
|
||||
res := Tree.Query(path, line, col)
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -792,12 +819,12 @@ func handleDefinition(params DefinitionParams) any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleReferences(params ReferenceParams) []Location {
|
||||
func HandleReferences(params ReferenceParams) []Location {
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
line := params.Position.Line + 1
|
||||
col := params.Position.Character + 1
|
||||
|
||||
res := tree.Query(path, line, col)
|
||||
res := Tree.Query(path, line, col)
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -835,7 +862,7 @@ func handleReferences(params ReferenceParams) []Location {
|
||||
}
|
||||
|
||||
// 1. References from index (Aliases)
|
||||
for _, ref := range tree.References {
|
||||
for _, ref := range Tree.References {
|
||||
if ref.Target == canonical {
|
||||
locations = append(locations, Location{
|
||||
URI: "file://" + ref.File,
|
||||
@@ -848,7 +875,7 @@ func handleReferences(params ReferenceParams) []Location {
|
||||
}
|
||||
|
||||
// 2. References from Node Targets (Direct References)
|
||||
tree.Walk(func(node *index.ProjectNode) {
|
||||
Tree.Walk(func(node *index.ProjectNode) {
|
||||
if node.Target == canonical {
|
||||
for _, frag := range node.Fragments {
|
||||
if frag.IsObject {
|
||||
@@ -902,9 +929,9 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
||||
|
||||
// Find references
|
||||
var refs []string
|
||||
for _, ref := range tree.References {
|
||||
for _, ref := range Tree.References {
|
||||
if ref.Target == node {
|
||||
container := tree.GetNodeContaining(ref.File, ref.Position)
|
||||
container := Tree.GetNodeContaining(ref.File, ref.Position)
|
||||
if container != nil {
|
||||
threadName := ""
|
||||
stateName := ""
|
||||
|
||||
@@ -207,6 +207,7 @@ func (p *Parser) parseSubnode() (Subnode, bool) {
|
||||
}
|
||||
if t.Type == TokenEOF {
|
||||
p.addError(t.Position, "unexpected EOF, expected }")
|
||||
sub.EndPosition = t.Position
|
||||
return sub, false
|
||||
}
|
||||
def, ok := p.parseDefinition()
|
||||
|
||||
231
test/lsp_completion_test.go
Normal file
231
test/lsp_completion_test.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/schema"
|
||||
)
|
||||
|
||||
func TestHandleCompletion(t *testing.T) {
|
||||
setup := func() {
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.ProjectRoot = "."
|
||||
lsp.GlobalSchema = schema.NewSchema()
|
||||
}
|
||||
|
||||
uri := "file://test.marte"
|
||||
path := "test.marte"
|
||||
|
||||
t.Run("Suggest Classes", func(t *testing.T) {
|
||||
setup()
|
||||
content := "+Obj = { Class = "
|
||||
lsp.Documents[uri] = content
|
||||
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 0, Character: len(content)},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil || len(list.Items) == 0 {
|
||||
t.Fatal("Expected class suggestions, got none")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "RealTimeApplication" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected RealTimeApplication in class suggestions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest Fields", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
+MyApp = {
|
||||
Class = RealTimeApplication
|
||||
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
// Position at line 3 (empty line inside MyApp)
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 3, Character: 4},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil || len(list.Items) == 0 {
|
||||
t.Fatal("Expected field suggestions, got none")
|
||||
}
|
||||
|
||||
foundData := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "Data" {
|
||||
foundData = true
|
||||
if item.Detail != "Mandatory" {
|
||||
t.Errorf("Expected Data to be Mandatory, got %s", item.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundData {
|
||||
t.Error("Expected 'Data' in field suggestions for RealTimeApplication")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Suggest References (DataSource)", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
$App = {
|
||||
$Data = {
|
||||
+InDS = {
|
||||
Class = FileReader
|
||||
+Signals = {
|
||||
Sig1 = { Type = uint32 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
+InputSignals = {
|
||||
S1 = { DataSource = }
|
||||
}
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Position at end of "DataSource = "
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 14, Character: 28},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
if list == nil || len(list.Items) == 0 {
|
||||
t.Fatal("Expected DataSource suggestions, got none")
|
||||
}
|
||||
|
||||
foundDS := false
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "InDS" {
|
||||
foundDS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundDS {
|
||||
t.Error("Expected 'InDS' in suggestions for DataSource field")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Filter Existing Fields", func(t *testing.T) {
|
||||
setup()
|
||||
content := `
|
||||
+MyThread = {
|
||||
Class = RealTimeThread
|
||||
Functions = { }
|
||||
|
||||
}
|
||||
`
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile(path, cfg)
|
||||
|
||||
// Position at line 4
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Position: lsp.Position{Line: 4, Character: 4},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "Functions" || item.Label == "Class" {
|
||||
t.Errorf("Did not expect already defined field %s in suggestions", item.Label)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Scope-aware suggestions", func(t *testing.T) {
|
||||
setup()
|
||||
// Define a project DataSource in one file
|
||||
cfg1, _ := parser.NewParser("#package MYPROJ.Data\n+ProjectDS = { Class = FileReader +Signals = { S1 = { Type = int32 } } }").Parse()
|
||||
lsp.Tree.AddFile("project_ds.marte", cfg1)
|
||||
|
||||
// Define an isolated file
|
||||
contentIso := "+MyGAM = { Class = IOGAM +InputSignals = { S1 = { DataSource = } } }"
|
||||
lsp.Documents["file://iso.marte"] = contentIso
|
||||
cfg2, _ := parser.NewParser(contentIso).Parse()
|
||||
lsp.Tree.AddFile("iso.marte", cfg2)
|
||||
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Completion in isolated file
|
||||
params := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://iso.marte"},
|
||||
Position: lsp.Position{Line: 0, Character: strings.Index(contentIso, "DataSource = ") + len("DataSource = ") + 1},
|
||||
}
|
||||
|
||||
list := lsp.HandleCompletion(params)
|
||||
foundProjectDS := false
|
||||
if list != nil {
|
||||
for _, item := range list.Items {
|
||||
if item.Label == "ProjectDS" {
|
||||
foundProjectDS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if foundProjectDS {
|
||||
t.Error("Did not expect ProjectDS in isolated file suggestions")
|
||||
}
|
||||
|
||||
// Completion in a project file
|
||||
lineContent := "+MyGAM = { Class = IOGAM +InputSignals = { S1 = { DataSource = Dummy } } }"
|
||||
contentPrj := "#package MYPROJ.App\n" + lineContent
|
||||
lsp.Documents["file://prj.marte"] = contentPrj
|
||||
pPrj := parser.NewParser(contentPrj)
|
||||
cfg3, err := pPrj.Parse()
|
||||
if err != nil {
|
||||
t.Logf("Parser error in contentPrj: %v", err)
|
||||
}
|
||||
lsp.Tree.AddFile("prj.marte", cfg3)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
paramsPrj := lsp.CompletionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://prj.marte"},
|
||||
Position: lsp.Position{Line: 1, Character: strings.Index(lineContent, "Dummy")},
|
||||
}
|
||||
|
||||
listPrj := lsp.HandleCompletion(paramsPrj)
|
||||
foundProjectDS = false
|
||||
if listPrj != nil {
|
||||
for _, item := range listPrj.Items {
|
||||
if item.Label == "ProjectDS" {
|
||||
foundProjectDS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundProjectDS {
|
||||
t.Error("Expected ProjectDS in project file suggestions")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package lsp
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
@@ -24,50 +25,38 @@ func TestInitProjectScan(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// File 2: Reference
|
||||
// +Source = { Class = C Link = Target }
|
||||
// Link = Target starts at index ...
|
||||
// #package Test.Common (21 chars including newline)
|
||||
// +Source = { Class = C Link = Target }
|
||||
// 012345678901234567890123456789012345
|
||||
// Previous offset was 29.
|
||||
// Now add 21?
|
||||
// #package Test.Common\n
|
||||
// +Source = ...
|
||||
// So add 21 to Character? Or Line 1?
|
||||
// It's on Line 1 (0-based 1).
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "ref.marte"), []byte("#package Test.Common\n+Source = { Class = C Link = Target }"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 2. Initialize
|
||||
tree = index.NewProjectTree() // Reset global tree
|
||||
lsp.Tree = index.NewProjectTree() // Reset global tree
|
||||
|
||||
initParams := InitializeParams{RootPath: tmpDir}
|
||||
initParams := lsp.InitializeParams{RootPath: tmpDir}
|
||||
paramsBytes, _ := json.Marshal(initParams)
|
||||
|
||||
msg := &JsonRpcMessage{
|
||||
msg := &lsp.JsonRpcMessage{
|
||||
Method: "initialize",
|
||||
Params: paramsBytes,
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
handleMessage(msg)
|
||||
lsp.HandleMessage(msg)
|
||||
|
||||
// Query the reference in ref.marte at "Target"
|
||||
// Target starts at index 29 (0-based) on Line 1
|
||||
defParams := DefinitionParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: "file://" + filepath.Join(tmpDir, "ref.marte")},
|
||||
Position: Position{Line: 1, Character: 29},
|
||||
defParams := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + filepath.Join(tmpDir, "ref.marte")},
|
||||
Position: lsp.Position{Line: 1, Character: 29},
|
||||
}
|
||||
|
||||
res := handleDefinition(defParams)
|
||||
res := lsp.HandleDefinition(defParams)
|
||||
if res == nil {
|
||||
t.Fatal("Definition not found via LSP after initialization")
|
||||
}
|
||||
|
||||
locs, ok := res.([]Location)
|
||||
locs, ok := res.([]lsp.Location)
|
||||
if !ok {
|
||||
t.Fatalf("Expected []Location, got %T", res)
|
||||
t.Fatalf("Expected []lsp.Location, got %T", res)
|
||||
}
|
||||
|
||||
if len(locs) == 0 {
|
||||
@@ -83,7 +72,7 @@ func TestInitProjectScan(t *testing.T) {
|
||||
|
||||
func TestHandleDefinition(t *testing.T) {
|
||||
// Reset tree for test
|
||||
tree = index.NewProjectTree()
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
|
||||
content := `
|
||||
+MyObject = {
|
||||
@@ -100,28 +89,28 @@ func TestHandleDefinition(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
tree.AddFile(path, config)
|
||||
tree.ResolveReferences()
|
||||
lsp.Tree.AddFile(path, config)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
t.Logf("Refs: %d", len(tree.References))
|
||||
for _, r := range tree.References {
|
||||
t.Logf("Refs: %d", len(lsp.Tree.References))
|
||||
for _, r := range lsp.Tree.References {
|
||||
t.Logf(" %s at %d:%d", r.Name, r.Position.Line, r.Position.Column)
|
||||
}
|
||||
|
||||
// Test Go to Definition on MyObject reference
|
||||
params := DefinitionParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: Position{Line: 6, Character: 15}, // "MyObject" in RefField = MyObject
|
||||
params := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: lsp.Position{Line: 6, Character: 15}, // "MyObject" in RefField = MyObject
|
||||
}
|
||||
|
||||
result := handleDefinition(params)
|
||||
result := lsp.HandleDefinition(params)
|
||||
if result == nil {
|
||||
t.Fatal("handleDefinition returned nil")
|
||||
t.Fatal("HandleDefinition returned nil")
|
||||
}
|
||||
|
||||
locations, ok := result.([]Location)
|
||||
locations, ok := result.([]lsp.Location)
|
||||
if !ok {
|
||||
t.Fatalf("Expected []Location, got %T", result)
|
||||
t.Fatalf("Expected []lsp.Location, got %T", result)
|
||||
}
|
||||
|
||||
if len(locations) != 1 {
|
||||
@@ -135,7 +124,7 @@ func TestHandleDefinition(t *testing.T) {
|
||||
|
||||
func TestHandleReferences(t *testing.T) {
|
||||
// Reset tree for test
|
||||
tree = index.NewProjectTree()
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
|
||||
content := `
|
||||
+MyObject = {
|
||||
@@ -155,17 +144,17 @@ func TestHandleReferences(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
tree.AddFile(path, config)
|
||||
tree.ResolveReferences()
|
||||
lsp.Tree.AddFile(path, config)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Test Find References for MyObject (triggered from its definition)
|
||||
params := ReferenceParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: Position{Line: 1, Character: 1}, // "+MyObject"
|
||||
Context: ReferenceContext{IncludeDeclaration: true},
|
||||
params := lsp.ReferenceParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + path},
|
||||
Position: lsp.Position{Line: 1, Character: 1}, // "+MyObject"
|
||||
Context: lsp.ReferenceContext{IncludeDeclaration: true},
|
||||
}
|
||||
|
||||
locations := handleReferences(params)
|
||||
locations := lsp.HandleReferences(params)
|
||||
if len(locations) != 3 { // 1 declaration + 2 references
|
||||
t.Fatalf("Expected 3 locations, got %d", len(locations))
|
||||
}
|
||||
@@ -181,15 +170,15 @@ Field=1
|
||||
`
|
||||
uri := "file:///test.marte"
|
||||
|
||||
// Open (populate documents map)
|
||||
documents[uri] = content
|
||||
// Open (populate Documents map)
|
||||
lsp.Documents[uri] = content
|
||||
|
||||
// Format
|
||||
params := DocumentFormattingParams{
|
||||
TextDocument: TextDocumentIdentifier{URI: uri},
|
||||
params := lsp.DocumentFormattingParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
}
|
||||
|
||||
edits := handleFormatting(params)
|
||||
edits := lsp.HandleFormatting(params)
|
||||
|
||||
if len(edits) != 1 {
|
||||
t.Fatalf("Expected 1 edit, got %d", len(edits))
|
||||
Reference in New Issue
Block a user