Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show links to possible map calls on hover #706

Merged
merged 2 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/lsp/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
8 changes: 6 additions & 2 deletions internal/lsp/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
74 changes: 66 additions & 8 deletions internal/lsp/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,52 @@ 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 {
p := syntax.Begin(def.Node)
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' {
Expand All @@ -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
}
Expand All @@ -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 = ""
Expand All @@ -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()])
Expand Down Expand Up @@ -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
Expand All @@ -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
}
69 changes: 60 additions & 9 deletions internal/lsp/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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,
Expand All @@ -77,8 +129,7 @@ func testHover(t *testing.T, text string, position protocol.Position, capability
},
}

hover, err := lsp.ProcessHover(&params, &ttcn3.DB{}, capability)
hover, err := lsp.ProcessHover(&params, suite.DB, capability)
assert.Equal(t, err, nil)

return hover
}
79 changes: 79 additions & 0 deletions internal/lsp/map_connector_finder.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading