aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/work/hover.go
blob: 1a1b299fd761bfde0f501ba1a4fc05b3b849d98f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package work

import (
	"bytes"
	"context"
	"fmt"

	"golang.org/x/mod/modfile"
	"golang.org/x/tools/gopls/internal/lsp/protocol"
	"golang.org/x/tools/gopls/internal/lsp/source"
	"golang.org/x/tools/internal/event"
)

func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
	// We only provide hover information for the view's go.work file.
	if fh.URI() != snapshot.WorkFile() {
		return nil, nil
	}

	ctx, done := event.Start(ctx, "work.Hover")
	defer done()

	// Get the position of the cursor.
	pw, err := snapshot.ParseWork(ctx, fh)
	if err != nil {
		return nil, fmt.Errorf("getting go.work file handle: %w", err)
	}
	offset, err := pw.Mapper.PositionOffset(position)
	if err != nil {
		return nil, fmt.Errorf("computing cursor offset: %w", err)
	}

	// Confirm that the cursor is inside a use statement, and then find
	// the position of the use statement's directory path.
	use, pathStart, pathEnd := usePath(pw, offset)

	// The cursor position is not on a use statement.
	if use == nil {
		return nil, nil
	}

	// Get the mod file denoted by the use.
	modfh, err := snapshot.GetFile(ctx, modFileURI(pw, use))
	if err != nil {
		return nil, fmt.Errorf("getting modfile handle: %w", err)
	}
	pm, err := snapshot.ParseMod(ctx, modfh)
	if err != nil {
		return nil, fmt.Errorf("getting modfile handle: %w", err)
	}
	mod := pm.File.Module.Mod

	// Get the range to highlight for the hover.
	rng, err := pw.Mapper.OffsetRange(pathStart, pathEnd)
	if err != nil {
		return nil, err
	}
	options := snapshot.View().Options()
	return &protocol.Hover{
		Contents: protocol.MarkupContent{
			Kind:  options.PreferredContentFormat,
			Value: mod.Path,
		},
		Range: rng,
	}, nil
}

func usePath(pw *source.ParsedWorkFile, offset int) (use *modfile.Use, pathStart, pathEnd int) {
	for _, u := range pw.File.Use {
		path := []byte(u.Path)
		s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte
		i := bytes.Index(pw.Mapper.Content[s:e], path)
		if i == -1 {
			// This should not happen.
			continue
		}
		// Shift the start position to the location of the
		// module directory within the use statement.
		pathStart, pathEnd = s+i, s+i+len(path)
		if pathStart <= offset && offset <= pathEnd {
			return u, pathStart, pathEnd
		}
	}
	return nil, 0, 0
}