diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 901f044f..dce9dc9a 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -427,7 +427,7 @@ func Complete(suite *Suite, pos int, nodes []syntax.Node, ownModName string) []p return list } func CompletePredefinedFunctions() []protocol.CompletionItem { - var ret []protocol.CompletionItem + ret := make([]protocol.CompletionItem, 0, len(PredefinedFunctions)) for _, v := range PredefinedFunctions { markup := protocol.MarkupContent{Kind: "markdown", Value: v.Documentation} ret = append(ret, protocol.CompletionItem{ diff --git a/internal/lsp/general.go b/internal/lsp/general.go index af334e26..eefa2a67 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -12,8 +12,12 @@ import ( errors "golang.org/x/xerrors" ) -type PlainTextHover struct{} -type MarkdownHover struct{} +type PlainTextHover struct { + mapOrConnectRefs string +} +type MarkdownHover struct { + mapOrConnectLinks string +} func (s *Server) registerSemanticTokensIfNoDynReg() *protocol.SemanticTokensRegistrationOptions { if s.clientCapability.HasDynRegForSemTok { diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index fffc5d58..aeb6e06f 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -13,10 +13,37 @@ import ( "github.com/nokia/ntt/ttcn3/syntax" ) +// removeDuplicateNodes +// NOTE: this function is a workaround for a scoping problem which +// occurs inside `tree.LookupWithDB`. +// following test is provoking the situation: +// TestPlainTextHoverForPortDefFromDecl (hover_test.go) +func removeDuplicateNodes(nodes []*ttcn3.Node) []*ttcn3.Node { + uNodes := make(map[*syntax.Ident]bool, len(nodes)) + res := make([]*ttcn3.Node, 0, len(nodes)) + for _, n := range nodes { + if _, ok := uNodes[n.Ident]; !ok { + uNodes[n.Ident] = true + res = append(res, n) + } + } + return res +} + func (md *MarkdownHover) Print(sign string, comment string, posRef string) protocol.MarkupContent { // make line breaks conform to markdown spec comment = strings.ReplaceAll(comment, "\n", " \n") - return protocol.MarkupContent{Kind: "markdown", Value: "```typescript\n" + string(sign) + "\n```\n - - -\n" + comment + "\n - - -\n" + posRef} + res := "```typescript\n" + string(sign) + "\n```\n" + if len(comment) > 0 { + res += " - - -\n" + comment + } + if len(md.mapOrConnectLinks) > 0 { + res += "_possible map / connect statements_\n - - -\n" + md.mapOrConnectLinks + } + if len(posRef) > 0 { + res += "\n - - -\n" + posRef + } + return protocol.MarkupContent{Kind: "markdown", Value: res} } func (md *MarkdownHover) LinkForNode(def *ttcn3.Node) string { @@ -24,6 +51,14 @@ func (md *MarkdownHover) LinkForNode(def *ttcn3.Node) string { return fmt.Sprintf("[module %s](%s#L%dC%d)", def.ModuleOf(def.Node).Name.String(), def.Filename(), p.Line, p.Column) } +func (md *MarkdownHover) GatherMapOrConnectLinks(fileName string, line uint32, uri protocol.DocumentURI) { + md.mapOrConnectLinks += fmt.Sprintf("[%s:%d](%s#L%d) \n", fileName, line+1, uri, line+1) +} + +func (md *MarkdownHover) Reset() { + md.mapOrConnectLinks = "" +} + func (pt *PlainTextHover) Print(sign string, comment string, posRef string) protocol.MarkupContent { val := sign if len(val) > 0 && val[len(val)-1] != '\n' { @@ -36,6 +71,9 @@ func (pt *PlainTextHover) Print(sign string, comment string, posRef string) prot val += "\n" } } + if len(pt.mapOrConnectRefs) > 0 { + val += "possible map / connect statements\n_________________________________\n" + pt.mapOrConnectRefs + } if len(posRef) > 0 { val += strings.Repeat("_", len(posRef)+2) + "\n" + posRef } @@ -46,6 +84,14 @@ func (pt *PlainTextHover) LinkForNode(def *ttcn3.Node) string { return fmt.Sprintf("[module %s]", def.ModuleOf(def.Node).Name.String()) } +func (pt *PlainTextHover) GatherMapOrConnectLinks(fileName string, line uint32, uri protocol.DocumentURI) { + pt.mapOrConnectRefs += fmt.Sprintf("%s:%d\n", fileName, line+1) +} + +func (pt *PlainTextHover) Reset() { + pt.mapOrConnectRefs = "" +} + func getSignature(def *ttcn3.Node) string { var sig bytes.Buffer var prefix = "" @@ -54,18 +100,18 @@ func getSignature(def *ttcn3.Node) string { switch node := def.Node.(type) { case *syntax.FuncDecl: sig.WriteString(node.Kind.String() + " " + node.Name.String()) - sig.Write(content[node.Params.Pos() : node.Params.End()]) + sig.Write(content[node.Params.Pos():node.Params.End()]) if node.RunsOn != nil { sig.WriteString("\n ") - sig.Write(content[node.RunsOn.Pos() : node.RunsOn.End()]) + sig.Write(content[node.RunsOn.Pos():node.RunsOn.End()]) } if node.System != nil { sig.WriteString("\n ") - sig.Write(content[node.System.Pos() : node.System.End()]) + sig.Write(content[node.System.Pos():node.System.End()]) } if node.Return != nil { sig.WriteString("\n ") - sig.Write(content[node.Return.Pos() : node.Return.End()]) + sig.Write(content[node.Return.Pos():node.Return.End()]) } case *syntax.ValueDecl, *syntax.TemplateDecl, *syntax.FormalPar, *syntax.StructTypeDecl, *syntax.ComponentTypeDecl, *syntax.EnumTypeDecl, *syntax.PortTypeDecl: sig.Write(content[def.Node.Pos()-1 : def.Node.End()]) @@ -105,14 +151,26 @@ func ProcessHover(params *protocol.HoverParams, db *ttcn3.DB, capability HoverCo return nil, nil } - for _, def := range tree.LookupWithDB(x, db) { + for _, def := range removeDuplicateNodes(tree.LookupWithDB(x, db)) { defFound = true - comment = syntax.Doc(def.Node) signature = getSignature(def) if tree.Root != def.Root { posRef = capability.LinkForNode(def) } + + if firstTok := def.Node.FirstTok(); firstTok == nil { + continue + } else { + if node, ok := def.Node.(*syntax.ValueDecl); ok { + if node.Kind.Kind() == syntax.PORT { + locations := FindMapConnectStatementForPortIdMatchingTheName(db, syntax.Name(x)) + for _, l := range locations { + capability.GatherMapOrConnectLinks(l.URI.SpanURI().Filename(), l.Range.Start.Line, l.URI) + } + } + } + } } if !defFound { // look for predefined functions @@ -128,6 +186,6 @@ func ProcessHover(params *protocol.HoverParams, db *ttcn3.DB, capability HoverCo hoverContents := capability.Print(signature, comment, posRef) hover := &protocol.Hover{Contents: hoverContents} - + capability.Reset() return hover, nil } diff --git a/internal/lsp/hover_test.go b/internal/lsp/hover_test.go index a6b0a800..1988659f 100644 --- a/internal/lsp/hover_test.go +++ b/internal/lsp/hover_test.go @@ -7,6 +7,7 @@ import ( "github.com/nokia/ntt/internal/fs" "github.com/nokia/ntt/internal/lsp" "github.com/nokia/ntt/internal/lsp/protocol" + "github.com/nokia/ntt/project" "github.com/nokia/ntt/ttcn3" "github.com/stretchr/testify/assert" ) @@ -54,20 +55,71 @@ func TestMarkdownHoverForFunction(t *testing.T) { " runs on Component\n" + " system System\n" + " return integer\n" + - "```\n" + - " - - -\n" + - "\n" + - " - - -\n" + "```\n" + + assert.Equal(t, expected, actual.Contents.Value) +} + +func TestPlainTextHoverForPortDefFromDecl(t *testing.T) { + actual := testHover(t, ` + module Test { + type component Component { + port P p1; + } + function myfunc(integer x) + runs on Component + { + map(system:p1, mtc:p1); + p1.receive; + } + }`, + protocol.Position{Line: 3, Character: 24}, + &lsp.PlainTextHover{}) + + expected := + " port P p1\n" + + "possible map / connect statements\n" + + "_________________________________\n" + + "/TestPlainTextHoverForPortDefFromDecl.ttcn3:9\n" + + assert.Equal(t, expected, actual.Contents.Value) +} + +func TestPlainTextHoverForPortDefFromUsage(t *testing.T) { + actual := testHover(t, ` + module Test { + type component Component { + port P p1; + } + function myfunc(integer x) + runs on Component + { + map(system:p1, mtc:p1); + p1.receive; + } + }`, + protocol.Position{Line: 9, Character: 16}, + &lsp.PlainTextHover{}) + + expected := + " port P p1\n" + + "possible map / connect statements\n" + + "_________________________________\n" + + "/TestPlainTextHoverForPortDefFromUsage.ttcn3:9\n" assert.Equal(t, expected, actual.Contents.Value) } func testHover(t *testing.T, text string, position protocol.Position, capability lsp.HoverContentProvider) *protocol.Hover { t.Helper() - - file := fmt.Sprintf("%s.ttcn3", t.Name()) + suite := &lsp.Suite{ + Config: &project.Config{}, + DB: &ttcn3.DB{}, + } + file := fmt.Sprintf("file://%s.ttcn3", t.Name()) fs.SetContent(file, []byte(text)) - + suite.Config.Sources = append(suite.Config.Sources, file) + suite.DB.Index(suite.Config.Sources...) params := protocol.HoverParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ Position: position, @@ -77,8 +129,7 @@ func testHover(t *testing.T, text string, position protocol.Position, capability }, } - hover, err := lsp.ProcessHover(¶ms, &ttcn3.DB{}, capability) + hover, err := lsp.ProcessHover(¶ms, suite.DB, capability) assert.Equal(t, err, nil) - return hover } diff --git a/internal/lsp/map_connector_finder.go b/internal/lsp/map_connector_finder.go new file mode 100644 index 00000000..6c8edd95 --- /dev/null +++ b/internal/lsp/map_connector_finder.go @@ -0,0 +1,79 @@ +package lsp + +import ( + "sort" + + "github.com/nokia/ntt/internal/lsp/protocol" + "github.com/nokia/ntt/ttcn3" + "github.com/nokia/ntt/ttcn3/syntax" +) + +func isDuplicate(list []protocol.Location, loc protocol.Location) bool { + for _, l := range list { + if l == loc { + return true + } + } + return false +} +func invokedFromMapOrConnect(n syntax.Node, syn *ttcn3.Tree, list []protocol.Location) []protocol.Location { + p := n + + for { + p = syn.ParentOf(p) + if p == nil { + break + } + node, ok := p.(*syntax.CallExpr) + if !ok { + continue + } + idNode, ok := node.Fun.(*syntax.Ident) + if !ok { + continue + } + if idNode.String() == "map" || idNode.String() == "connect" { + loc := location(syntax.Span{Begin: syn.Position(node.Pos()), End: syn.Position(node.End()), Filename: syn.Filename()}) + if !isDuplicate(list, loc) { + list = append(list, loc) + } + break + } + } + return list +} + +func findMapConnectStatementForPortIdMatchingTheNameFromFile(file string, idName string) []protocol.Location { + list := make([]protocol.Location, 0, 4) + syn := ttcn3.ParseFile(file) + syn.Root.Inspect(func(n syntax.Node) bool { + if n == nil { + // called on node exit + return false + } + + switch node := n.(type) { + case *syntax.Ident: + if idName == node.String() { + list = invokedFromMapOrConnect(n, syn, list) + } + return false + default: + return true + } + }) + return list +} + +func FindMapConnectStatementForPortIdMatchingTheName(db *ttcn3.DB, name string) []protocol.Location { + candidates := make([]string, 0, len(db.Uses)) + locs := make([]protocol.Location, 0, len(db.Uses)) + for file := range db.Uses[name] { + candidates = append(candidates, file) + } + sort.Strings(candidates) + for _, file := range candidates { + locs = append(locs, findMapConnectStatementForPortIdMatchingTheNameFromFile(file, name)...) + } + return locs +} diff --git a/internal/lsp/map_connector_finder_test.go b/internal/lsp/map_connector_finder_test.go new file mode 100644 index 00000000..d12d965e --- /dev/null +++ b/internal/lsp/map_connector_finder_test.go @@ -0,0 +1,64 @@ +package lsp_test + +import ( + "fmt" + "testing" + + "github.com/nokia/ntt/internal/fs" + "github.com/nokia/ntt/internal/lsp" + "github.com/nokia/ntt/internal/lsp/protocol" + "github.com/nokia/ntt/ttcn3" + "github.com/stretchr/testify/assert" +) + +func TestFindAllMaps(t *testing.T) { + + name1 := fmt.Sprintf("test://%s_input1", t.Name()) + name2 := fmt.Sprintf("test://%s_input2", t.Name()) + name3 := fmt.Sprintf("test://%s_input3", t.Name()) + + input1 := `module Test + { + type port Pt1 message {inout integer}; + type port Pt2 message {inout charstring}; + type component C1 { + port Pt1 p + } + type component C2 { + port Pt2 p, + port Pt1, pt1 + } + function f() runs on C1 { + map(mtc:p, system:p) + } + }` + input2 := `module A + { + import from TestFindAllMaps_Module_0 all; + function fA() runs on C2 { + map(mtc:p, system:p) + map(mtc:pt1, system:pt1) + } + }` + input3 := `module B + { + function fB() runs on C2 { + map(system:p, mtc:p) // w/o import statement, this occurence shouldn't be found + } + }` + + fs.SetContent(name1, []byte(input1)) + fs.SetContent(name2, []byte(input2)) + fs.SetContent(name3, []byte(input3)) + + db := &ttcn3.DB{} + db.Index(name1, name2, name3) + + // Lookup `Msg` + list := lsp.FindMapConnectStatementForPortIdMatchingTheName(db, "p") + assert.Equal(t, []protocol.Location{ + {URI: "test://TestFindAllMaps_input1", Range: protocol.Range{Start: protocol.Position{12, 3}, End: protocol.Position{12, 23}}}, + {URI: "test://TestFindAllMaps_input2", Range: protocol.Range{Start: protocol.Position{4, 4}, End: protocol.Position{4, 24}}}, + {URI: "test://TestFindAllMaps_input3", Range: protocol.Range{Start: protocol.Position{3, 4}, End: protocol.Position{3, 24}}}, + }, list) +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index c8e48d46..9783001e 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -68,6 +68,8 @@ func (s serverState) String() string { type HoverContentProvider interface { Print(sign string, comment string, posRef string) protocol.MarkupContent LinkForNode(def *ttcn3.Node) string + GatherMapOrConnectLinks(fileName string, line uint32, uri protocol.DocumentURI) + Reset() } type ClientCapability struct { HoverContent HoverContentProvider