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

Add hover to rule #37

Merged
merged 2 commits into from
Jan 22, 2022
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
36 changes: 36 additions & 0 deletions langserver/hover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package langserver

import (
"context"
"encoding/json"

"github.com/sourcegraph/go-lsp"
"github.com/sourcegraph/jsonrpc2"
)

func (h *handler) handleTextDocumentHover(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) {
if req.Params == nil {
return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
}

var params lsp.TextDocumentPositionParams
if err := json.Unmarshal(*req.Params, &params); err != nil {
return nil, err
}

return h.documentIdent(ctx, params.TextDocument.URI, params.Position)
}

func (h *handler) documentIdent(ctx context.Context, uri lsp.DocumentURI, position lsp.Position) (lsp.Hover, error) {
loc := h.toOPALocation(position, uri)
documentResults, err := h.project.TermDocument(loc)
if err != nil {
return lsp.Hover{}, err
}

result := make([]lsp.MarkedString, len(documentResults))
for i, d := range documentResults {
result[i] = lsp.MarkedString{Language: d.Language, Value: d.Content}
}
return lsp.Hover{Contents: result}, nil
}
1 change: 1 addition & 0 deletions langserver/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func (h *handler) handleInitialize(ctx context.Context, conn *jsonrpc2.Conn, req
},
DocumentFormattingProvider: true,
DefinitionProvider: true,
HoverProvider: true,
CompletionProvider: &lsp.CompletionOptions{
TriggerCharacters: []string{"*"},
ResolveProvider: true,
Expand Down
35 changes: 13 additions & 22 deletions langserver/internal/source/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ const (
ImportItem
)

const (
BuiltinDetail = `built-in function

See https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions`
)

func (p *Project) ListCompletionItems(location *ast.Location) ([]CompletionItem, error) {
term, err := p.SearchTargetTerm(location)
if err != nil {
Expand Down Expand Up @@ -267,11 +261,9 @@ func (p *Project) listBuiltinFunction(term *ast.Term) []CompletionItem {
continue
}
result = append(result, CompletionItem{
Label: b.Name,
Kind: BuiltinFunctionItem,
Detail: fmt.Sprintf(`%s%s

%s`, b.Name, b.Decl.FuncArgs().String(), BuiltinDetail),
Label: b.Name,
Kind: BuiltinFunctionItem,
Detail: createDocForBuiltinFunction(b),
InsertText: fmt.Sprintf("%s%s", b.Name, b.Decl.FuncArgs().String()),
})
}
Expand All @@ -286,11 +278,9 @@ func (p *Project) listBuiltinFunction(term *ast.Term) []CompletionItem {
if strings.HasPrefix(b.Name, fmt.Sprintf("%s.", val.Value.String())) {
name := strings.TrimLeft(b.Name, fmt.Sprintf("%s.", val.Value.String()))
result = append(result, CompletionItem{
Label: name,
Kind: BuiltinFunctionItem,
Detail: fmt.Sprintf(`%s%s

%s`, b.Name, b.Decl.FuncArgs().String(), BuiltinDetail),
Label: name,
Kind: BuiltinFunctionItem,
Detail: createDocForBuiltinFunction(b),
InsertText: fmt.Sprintf("%s%s", name, b.Decl.FuncArgs().String()),
})
}
Expand Down Expand Up @@ -403,15 +393,16 @@ func (p *Project) createRuleCompletionItem(rule *ast.Rule) CompletionItem {
itemKind = VariableItem
}

detail := string(rule.Loc().Text)
if detail == "default" {
detail = rule.String()
}

return CompletionItem{
Label: rule.Head.Name.String(),
Kind: itemKind,
InsertText: insertText.String(),
Detail: detail,
Detail: createDocForRule(rule),
}
}

func createDocForBuiltinFunction(builtin *ast.Builtin) string {
return fmt.Sprintf(`%s%s

%s`, builtin.Name, builtin.Decl.FuncArgs().String(), BuiltinDetail)
}
93 changes: 93 additions & 0 deletions langserver/internal/source/document.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package source

import (
"strings"

"github.com/open-policy-agent/opa/ast"
)

const (
BuiltinDetail = `built-in function

See https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions`
)

type Document struct {
Content string
Language string
}

func (p *Project) TermDocument(loc *ast.Location) ([]Document, error) {
term, err := p.SearchTargetTerm(loc)
if err != nil {
return nil, err
}
if term == nil {
return nil, nil
}

return p.findTermDocument(term), nil
}

func (p *Project) findTermDocument(term *ast.Term) []Document {
rule := p.findRuleForTerm(term.Loc())
if rule != nil {
target := p.findDefinitionInRule(term, rule)
if target != nil {
return nil
}

for _, b := range ast.DefaultBuiltins {
if b.Infix != "" {
continue
}
if b.Name == term.String() {
return []Document{
{
Content: b.Name + b.Decl.FuncArgs().String(),
Language: "rego",
},
{
Content: BuiltinDetail,
Language: "markdown",
},
}
}
}
}
return p.findTermDocumentInModule(term)
}

func (p *Project) findTermDocumentInModule(term *ast.Term) []Document {
searchPackageName := p.findPolicyRef(term)
searchPolicies := p.cache.FindPolicies(searchPackageName)
if len(searchPolicies) == 0 {
return nil
}

word := term.String()
if strings.Contains(word, ".") {
word = word[strings.Index(word, ".")+1:]
}

result := make([]Document, 0)
for _, mod := range searchPolicies {
for _, rule := range mod.Rules {
if rule.Head.Name.String() == word {
result = append(result, Document{
Content: createDocForRule(rule),
Language: "rego",
})
}
}
}
return result
}

func createDocForRule(rule *ast.Rule) string {
detail := string(rule.Loc().Text)
if detail == "default" {
return rule.String()
}
return detail
}
152 changes: 152 additions & 0 deletions langserver/internal/source/document_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package source_test

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/kitagry/regols/langserver/internal/source"
"github.com/open-policy-agent/opa/ast"
)

func TestProject_TermDocument(t *testing.T) {
tests := map[string]struct {
files map[string]source.File
location *ast.Location
expectDocs []source.Document
}{
"document in same file method": {
files: map[string]source.File{
"src.rego": {
RowText: `package src

violation[msg] {
method(msg)
}

method(msg) {
msg == "hello"
}`,
},
},
location: &ast.Location{
Row: 4,
Col: 2,
Offset: len("package src\n\nviolation[msg] { m"),
Text: []byte("m"),
File: "src.rego",
},
expectDocs: []source.Document{
{
Content: `method(msg) {
msg == "hello"
}`,
Language: "rego",
},
},
},
"default can show all": {
files: map[string]source.File{
"src.rego": {
RowText: `package src

violation[msg] {
item
}

default item = "hello"`,
},
},
location: &ast.Location{
Row: 4,
Col: 2,
Offset: len("package src\n\nviolation[msg] { i"),
Text: []byte("i"),
File: "src.rego",
},
expectDocs: []source.Document{
{
Content: `default item = "hello"`,
Language: "rego",
},
},
},
"builtin function": {
files: map[string]source.File{
"src.rego": {
RowText: `package src

violation[msg] {
sprintf("msg: %s", [msg])
}`,
},
},
location: &ast.Location{
Row: 4,
Col: 2,
Offset: len("package src\n\nviolation[msg] { s"),
Text: []byte("s"),
File: "src.rego",
},
expectDocs: []source.Document{
{
Content: "sprintf(string, array[any])",
Language: "rego",
},
{
Content: `built-in function

See https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions`,
Language: "markdown",
},
},
},
"builtin function with ref": {
files: map[string]source.File{
"src.rego": {
RowText: `package src

violation[msg] {
json.is_valid("{}")
}`,
},
},
location: &ast.Location{
Row: 4,
Col: 7,
Offset: len("package src\n\nviolation[msg] { json.i"),
Text: []byte("i"),
File: "src.rego",
},
expectDocs: []source.Document{
{
Content: "json.is_valid(string)",
Language: "rego",
},
{
Content: `built-in function

See https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions`,
Language: "markdown",
},
},
},
}

for n, tt := range tests {
t.Run(n, func(t *testing.T) {
project, err := source.NewProjectWithFiles(tt.files)
if err != nil {
t.Fatal(err)
}

docs, err := project.TermDocument(tt.location)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tt.expectDocs, docs); diff != "" {
t.Errorf("TermDocument result diff (-expect, +got)\n%s", diff)
}
})
}
}
2 changes: 2 additions & 0 deletions langserver/langserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func (h *handler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2
return h.handleTextDocumentDefinition(ctx, conn, req)
case "textDocument/completion":
return h.handleTextDocumentCompletion(ctx, conn, req)
case "textDocument/hover":
return h.handleTextDocumentHover(ctx, conn, req)
}
return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeMethodNotFound, Message: fmt.Sprintf("method not supported: %s", req.Method)}
}
Expand Down