Skip to content

Commit 4c756e3

Browse files
Support hover on all tokens (#152)
* Support hover on all tokens Closes #8 Closes #9 Closes #10 Next step will be to support docsonnet: #109 to get some proper descriptions * switch case
1 parent 8cf3395 commit 4c756e3

File tree

3 files changed

+194
-25
lines changed

3 files changed

+194
-25
lines changed

pkg/server/cache.go

+48-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package server
33
import (
44
"errors"
55
"fmt"
6+
"os"
7+
"strings"
68
"sync"
79

810
"github.com/google/go-jsonnet/ast"
@@ -43,8 +45,6 @@ type cache struct {
4345
}
4446

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

7373
return doc, nil
7474
}
75+
76+
func (c *cache) getContents(uri protocol.DocumentURI, position protocol.Range) (string, error) {
77+
text := ""
78+
doc, err := c.get(uri)
79+
if err == nil {
80+
text = doc.item.Text
81+
} else {
82+
// Read the file from disk (TODO: cache this)
83+
bytes, err := os.ReadFile(uri.SpanURI().Filename())
84+
if err != nil {
85+
return "", err
86+
}
87+
text = string(bytes)
88+
}
89+
90+
lines := strings.Split(text, "\n")
91+
if int(position.Start.Line) >= len(lines) {
92+
return "", fmt.Errorf("line %d out of range", position.Start.Line)
93+
}
94+
if int(position.Start.Character) >= len(lines[position.Start.Line]) {
95+
return "", fmt.Errorf("character %d out of range", position.Start.Character)
96+
}
97+
if int(position.End.Line) >= len(lines) {
98+
return "", fmt.Errorf("line %d out of range", position.End.Line)
99+
}
100+
if int(position.End.Character) >= len(lines[position.End.Line]) {
101+
return "", fmt.Errorf("character %d out of range", position.End.Character)
102+
}
103+
104+
contentBuilder := strings.Builder{}
105+
for i := position.Start.Line; i <= position.End.Line; i++ {
106+
switch i {
107+
case position.Start.Line:
108+
contentBuilder.WriteString(lines[i][position.Start.Character:])
109+
case position.End.Line:
110+
contentBuilder.WriteString(lines[i][:position.End.Character])
111+
default:
112+
contentBuilder.WriteString(lines[i])
113+
}
114+
if i != position.End.Line {
115+
contentBuilder.WriteRune('\n')
116+
}
117+
}
118+
119+
return contentBuilder.String(), nil
120+
}

pkg/server/hover.go

+53-22
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,7 @@ func (s *Server) Hover(_ context.Context, params *protocol.HoverParams) (*protoc
3535
return nil, nil
3636
}
3737

38-
node := stack.Pop()
39-
40-
// // DEBUG
41-
// var node2 ast.Node
42-
// if !stack.IsEmpty() {
43-
// _, node2 = stack.Pop()
44-
// }
45-
// r := protocol.Range{
46-
// Start: protocol.Position{
47-
// Line: uint32(node.Loc().Begin.Line) - 1,
48-
// Character: uint32(node.Loc().Begin.Column) - 1,
49-
// },
50-
// End: protocol.Position{
51-
// Line: uint32(node.Loc().End.Line) - 1,
52-
// Character: uint32(node.Loc().End.Column) - 1,
53-
// },
54-
// }
55-
// return &protocol.Hover{Range: r,
56-
// Contents: protocol.MarkupContent{Kind: protocol.PlainText,
57-
// Value: fmt.Sprintf("%v: %+v\n\n%v: %+v", reflect.TypeOf(node), node, reflect.TypeOf(node2), node2)},
58-
// }, nil
38+
node := stack.Peek()
5939

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

87-
return nil, nil
67+
definitionParams := &protocol.DefinitionParams{
68+
TextDocumentPositionParams: params.TextDocumentPositionParams,
69+
}
70+
definitions, err := findDefinition(doc.ast, definitionParams, s.getVM(doc.item.URI.SpanURI().Filename()))
71+
if err != nil {
72+
log.Debugf("Hover: error finding definition: %s", err)
73+
return nil, nil
74+
}
75+
76+
if len(definitions) == 0 {
77+
return nil, nil
78+
}
79+
80+
// Show the contents at the target range
81+
// If there are multiple definitions, show the filenames+line numbers
82+
contentBuilder := strings.Builder{}
83+
for _, def := range definitions {
84+
if len(definitions) > 1 {
85+
header := fmt.Sprintf("%s:%d", def.TargetURI, def.TargetRange.Start.Line+1)
86+
if def.TargetRange.Start.Line != def.TargetRange.End.Line {
87+
header += fmt.Sprintf("-%d", def.TargetRange.End.Line+1)
88+
}
89+
contentBuilder.WriteString(fmt.Sprintf("## `%s`\n", header))
90+
}
91+
92+
targetContent, err := s.cache.getContents(def.TargetURI, def.TargetRange)
93+
if err != nil {
94+
log.Debugf("Hover: error reading target content: %s", err)
95+
return nil, nil
96+
}
97+
// Limit the content to 5 lines
98+
if strings.Count(targetContent, "\n") > 5 {
99+
targetContent = strings.Join(strings.Split(targetContent, "\n")[:5], "\n") + "\n..."
100+
}
101+
contentBuilder.WriteString(fmt.Sprintf("```jsonnet\n%s\n```\n", targetContent))
102+
103+
if len(definitions) > 1 {
104+
contentBuilder.WriteString("\n")
105+
}
106+
}
107+
108+
result := &protocol.Hover{
109+
Contents: protocol.MarkupContent{
110+
Kind: protocol.Markdown,
111+
Value: contentBuilder.String(),
112+
},
113+
}
114+
if loc := node.Loc(); loc != nil {
115+
result.Range = position.RangeASTToProtocol(*loc)
116+
}
117+
118+
return result, nil
88119
}

pkg/server/hover_test.go

+93-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"io"
66
"os"
7+
"path/filepath"
78
"testing"
89

910
"github.com/grafana/jsonnet-language-server/pkg/stdlib"
@@ -66,7 +67,7 @@ var (
6667
}
6768
)
6869

69-
func TestHover(t *testing.T) {
70+
func TestHoverOnStdLib(t *testing.T) {
7071
logrus.SetOutput(io.Discard)
7172

7273
var testCases = []struct {
@@ -241,3 +242,94 @@ func TestHover(t *testing.T) {
241242
})
242243
}
243244
}
245+
246+
func TestHover(t *testing.T) {
247+
logrus.SetOutput(io.Discard)
248+
249+
testCases := []struct {
250+
name string
251+
filename string
252+
position protocol.Position
253+
expectedContent protocol.Hover
254+
}{
255+
{
256+
name: "hover on nested attribute",
257+
filename: "testdata/goto-indexes.jsonnet",
258+
position: protocol.Position{Line: 9, Character: 16},
259+
expectedContent: protocol.Hover{
260+
Contents: protocol.MarkupContent{
261+
Kind: protocol.Markdown,
262+
Value: "```jsonnet\nbar: 'innerfoo',\n```\n",
263+
},
264+
Range: protocol.Range{
265+
Start: protocol.Position{Line: 9, Character: 5},
266+
End: protocol.Position{Line: 9, Character: 18},
267+
},
268+
},
269+
},
270+
{
271+
name: "hover on multi-line string",
272+
filename: "testdata/goto-indexes.jsonnet",
273+
position: protocol.Position{Line: 8, Character: 9},
274+
expectedContent: protocol.Hover{
275+
Contents: protocol.MarkupContent{
276+
Kind: protocol.Markdown,
277+
Value: "```jsonnet\nobj = {\n foo: {\n bar: 'innerfoo',\n },\n bar: 'foo',\n}\n```\n",
278+
},
279+
Range: protocol.Range{
280+
Start: protocol.Position{Line: 8, Character: 8},
281+
End: protocol.Position{Line: 8, Character: 11},
282+
},
283+
},
284+
},
285+
}
286+
287+
for _, tc := range testCases {
288+
t.Run(tc.name, func(t *testing.T) {
289+
params := &protocol.HoverParams{
290+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
291+
TextDocument: protocol.TextDocumentIdentifier{
292+
URI: protocol.URIFromPath(tc.filename),
293+
},
294+
Position: tc.position,
295+
},
296+
}
297+
298+
server := NewServer("any", "test version", nil, Configuration{
299+
JPaths: []string{"testdata", filepath.Join(filepath.Dir(tc.filename), "vendor")},
300+
})
301+
serverOpenTestFile(t, server, tc.filename)
302+
response, err := server.Hover(context.Background(), params)
303+
304+
require.NoError(t, err)
305+
assert.Equal(t, &tc.expectedContent, response)
306+
})
307+
}
308+
}
309+
310+
func TestHoverGoToDefinitionTests(t *testing.T) {
311+
logrus.SetOutput(io.Discard)
312+
313+
for _, tc := range definitionTestCases {
314+
t.Run(tc.name, func(t *testing.T) {
315+
params := &protocol.HoverParams{
316+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
317+
TextDocument: protocol.TextDocumentIdentifier{
318+
URI: protocol.URIFromPath(tc.filename),
319+
},
320+
Position: tc.position,
321+
},
322+
}
323+
324+
server := NewServer("any", "test version", nil, Configuration{
325+
JPaths: []string{"testdata", filepath.Join(filepath.Dir(tc.filename), "vendor")},
326+
})
327+
serverOpenTestFile(t, server, tc.filename)
328+
response, err := server.Hover(context.Background(), params)
329+
330+
// We only want to check that it found something. In combination with other tests, we can assume the content is OK.
331+
require.NoError(t, err)
332+
require.NotNil(t, response)
333+
})
334+
}
335+
}

0 commit comments

Comments
 (0)