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

Support hover on all tokens #152

Merged
merged 2 commits into from
Aug 7, 2024
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
50 changes: 48 additions & 2 deletions pkg/server/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package server
import (
"errors"
"fmt"
"os"
"strings"
"sync"

"github.com/google/go-jsonnet/ast"
Expand Down Expand Up @@ -43,8 +45,6 @@ type cache struct {
}

// put adds or replaces a document in the cache.
// Documents are only replaced if the new document version is greater than the currently
// cached version.
func (c *cache) put(new *document) error {
c.mu.Lock()
defer c.mu.Unlock()
Expand Down Expand Up @@ -72,3 +72,49 @@ func (c *cache) get(uri protocol.DocumentURI) (*document, error) {

return doc, nil
}

func (c *cache) getContents(uri protocol.DocumentURI, position protocol.Range) (string, error) {
text := ""
doc, err := c.get(uri)
if err == nil {
text = doc.item.Text
} else {
// Read the file from disk (TODO: cache this)
bytes, err := os.ReadFile(uri.SpanURI().Filename())
if err != nil {
return "", err
}
text = string(bytes)
}

lines := strings.Split(text, "\n")
if int(position.Start.Line) >= len(lines) {
return "", fmt.Errorf("line %d out of range", position.Start.Line)
}
if int(position.Start.Character) >= len(lines[position.Start.Line]) {
return "", fmt.Errorf("character %d out of range", position.Start.Character)
}
if int(position.End.Line) >= len(lines) {
return "", fmt.Errorf("line %d out of range", position.End.Line)
}
if int(position.End.Character) >= len(lines[position.End.Line]) {
return "", fmt.Errorf("character %d out of range", position.End.Character)
}

contentBuilder := strings.Builder{}
for i := position.Start.Line; i <= position.End.Line; i++ {
switch i {
case position.Start.Line:
contentBuilder.WriteString(lines[i][position.Start.Character:])
case position.End.Line:
contentBuilder.WriteString(lines[i][:position.End.Character])
default:
contentBuilder.WriteString(lines[i])
}
if i != position.End.Line {
contentBuilder.WriteRune('\n')
}
}

return contentBuilder.String(), nil
}
75 changes: 53 additions & 22 deletions pkg/server/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,7 @@ func (s *Server) Hover(_ context.Context, params *protocol.HoverParams) (*protoc
return nil, nil
}

node := stack.Pop()

// // DEBUG
// var node2 ast.Node
// if !stack.IsEmpty() {
// _, node2 = stack.Pop()
// }
// r := protocol.Range{
// Start: protocol.Position{
// Line: uint32(node.Loc().Begin.Line) - 1,
// Character: uint32(node.Loc().Begin.Column) - 1,
// },
// End: protocol.Position{
// Line: uint32(node.Loc().End.Line) - 1,
// Character: uint32(node.Loc().End.Column) - 1,
// },
// }
// return &protocol.Hover{Range: r,
// Contents: protocol.MarkupContent{Kind: protocol.PlainText,
// Value: fmt.Sprintf("%v: %+v\n\n%v: %+v", reflect.TypeOf(node), node, reflect.TypeOf(node2), node2)},
// }, nil
node := stack.Peek()

_, isIndex := node.(*ast.Index)
_, isVar := node.(*ast.Var)
Expand Down Expand Up @@ -84,5 +64,56 @@ func (s *Server) Hover(_ context.Context, params *protocol.HoverParams) (*protoc
}
}

return nil, nil
definitionParams := &protocol.DefinitionParams{
TextDocumentPositionParams: params.TextDocumentPositionParams,
}
definitions, err := findDefinition(doc.ast, definitionParams, s.getVM(doc.item.URI.SpanURI().Filename()))
if err != nil {
log.Debugf("Hover: error finding definition: %s", err)
return nil, nil
}

if len(definitions) == 0 {
return nil, nil
}

// Show the contents at the target range
// If there are multiple definitions, show the filenames+line numbers
contentBuilder := strings.Builder{}
for _, def := range definitions {
if len(definitions) > 1 {
header := fmt.Sprintf("%s:%d", def.TargetURI, def.TargetRange.Start.Line+1)
if def.TargetRange.Start.Line != def.TargetRange.End.Line {
header += fmt.Sprintf("-%d", def.TargetRange.End.Line+1)
}
contentBuilder.WriteString(fmt.Sprintf("## `%s`\n", header))
}

targetContent, err := s.cache.getContents(def.TargetURI, def.TargetRange)
if err != nil {
log.Debugf("Hover: error reading target content: %s", err)
return nil, nil
}
// Limit the content to 5 lines
if strings.Count(targetContent, "\n") > 5 {
targetContent = strings.Join(strings.Split(targetContent, "\n")[:5], "\n") + "\n..."
}
contentBuilder.WriteString(fmt.Sprintf("```jsonnet\n%s\n```\n", targetContent))

if len(definitions) > 1 {
contentBuilder.WriteString("\n")
}
}

result := &protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.Markdown,
Value: contentBuilder.String(),
},
}
if loc := node.Loc(); loc != nil {
result.Range = position.RangeASTToProtocol(*loc)
}

return result, nil
}
94 changes: 93 additions & 1 deletion pkg/server/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"io"
"os"
"path/filepath"
"testing"

"github.com/grafana/jsonnet-language-server/pkg/stdlib"
Expand Down Expand Up @@ -66,7 +67,7 @@ var (
}
)

func TestHover(t *testing.T) {
func TestHoverOnStdLib(t *testing.T) {
logrus.SetOutput(io.Discard)

var testCases = []struct {
Expand Down Expand Up @@ -241,3 +242,94 @@ func TestHover(t *testing.T) {
})
}
}

func TestHover(t *testing.T) {
logrus.SetOutput(io.Discard)

testCases := []struct {
name string
filename string
position protocol.Position
expectedContent protocol.Hover
}{
{
name: "hover on nested attribute",
filename: "testdata/goto-indexes.jsonnet",
position: protocol.Position{Line: 9, Character: 16},
expectedContent: protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.Markdown,
Value: "```jsonnet\nbar: 'innerfoo',\n```\n",
},
Range: protocol.Range{
Start: protocol.Position{Line: 9, Character: 5},
End: protocol.Position{Line: 9, Character: 18},
},
},
},
{
name: "hover on multi-line string",
filename: "testdata/goto-indexes.jsonnet",
position: protocol.Position{Line: 8, Character: 9},
expectedContent: protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.Markdown,
Value: "```jsonnet\nobj = {\n foo: {\n bar: 'innerfoo',\n },\n bar: 'foo',\n}\n```\n",
},
Range: protocol.Range{
Start: protocol.Position{Line: 8, Character: 8},
End: protocol.Position{Line: 8, Character: 11},
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
params := &protocol.HoverParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.URIFromPath(tc.filename),
},
Position: tc.position,
},
}

server := NewServer("any", "test version", nil, Configuration{
JPaths: []string{"testdata", filepath.Join(filepath.Dir(tc.filename), "vendor")},
})
serverOpenTestFile(t, server, tc.filename)
response, err := server.Hover(context.Background(), params)

require.NoError(t, err)
assert.Equal(t, &tc.expectedContent, response)
})
}
}

func TestHoverGoToDefinitionTests(t *testing.T) {
logrus.SetOutput(io.Discard)

for _, tc := range definitionTestCases {
t.Run(tc.name, func(t *testing.T) {
params := &protocol.HoverParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.URIFromPath(tc.filename),
},
Position: tc.position,
},
}

server := NewServer("any", "test version", nil, Configuration{
JPaths: []string{"testdata", filepath.Join(filepath.Dir(tc.filename), "vendor")},
})
serverOpenTestFile(t, server, tc.filename)
response, err := server.Hover(context.Background(), params)

// We only want to check that it found something. In combination with other tests, we can assume the content is OK.
require.NoError(t, err)
require.NotNil(t, response)
})
}
}
Loading