Skip to content

Commit db1d1e0

Browse files
vikblomfindleyr
authored andcommitted
gopls/internal/lsp: go to definition from embed directive
Enable jump to definition on //go:embed directive arguments. If multiple files match the pattern one is picked at random. Improve the pattern matching for both definition and hover to exclude directories since they are not embeddable in themselves. Updates golang/go#50262 Change-Id: I09da40f195e8edfe661acaacd99f62d9f577e9ea Reviewed-on: https://go-review.googlesource.com/c/tools/+/531775 Reviewed-by: Robert Findley <[email protected]> Reviewed-by: Suzy Mueller <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 2be977e commit db1d1e0

File tree

6 files changed

+122
-8
lines changed

6 files changed

+122
-8
lines changed

gopls/internal/lsp/definition.go

-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package lsp
66

77
import (
88
"context"
9-
"errors"
109
"fmt"
1110

1211
"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -36,11 +35,6 @@ func (s *Server) definition(ctx context.Context, params *protocol.DefinitionPara
3635
case source.Tmpl:
3736
return template.Definition(snapshot, fh, params.Position)
3837
case source.Go:
39-
// Partial support for jumping from linkname directive (position at 2nd argument).
40-
locations, err := source.LinknameDefinition(ctx, snapshot, fh, params.Position)
41-
if !errors.Is(err, source.ErrNoLinkname) {
42-
return locations, err
43-
}
4438
return source.Definition(ctx, snapshot, fh, params.Position)
4539
default:
4640
return nil, fmt.Errorf("can't find definitions for file type %s", kind)

gopls/internal/lsp/source/definition.go

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package source
66

77
import (
88
"context"
9+
"errors"
910
"fmt"
1011
"go/ast"
1112
"go/token"
@@ -58,6 +59,18 @@ func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position
5859
return []protocol.Location{loc}, nil
5960
}
6061

62+
// Handle the case where the cursor is in a linkname directive.
63+
locations, err := LinknameDefinition(ctx, snapshot, fh, position)
64+
if !errors.Is(err, ErrNoLinkname) {
65+
return locations, err
66+
}
67+
68+
// Handle the case where the cursor is in an embed directive.
69+
locations, err = EmbedDefinition(pgf.Mapper, position)
70+
if !errors.Is(err, ErrNoEmbed) {
71+
return locations, err
72+
}
73+
6174
// The general case: the cursor is on an identifier.
6275
_, obj, _ := referencedObject(pkg, pgf, pos)
6376
if obj == nil {

gopls/internal/lsp/source/embeddirective.go

+56
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
package source
66

77
import (
8+
"errors"
89
"fmt"
10+
"io/fs"
11+
"path/filepath"
912
"strconv"
1013
"strings"
1114
"unicode"
@@ -14,6 +17,59 @@ import (
1417
"golang.org/x/tools/gopls/internal/lsp/protocol"
1518
)
1619

20+
// ErrNoEmbed is returned by EmbedDefinition when no embed
21+
// directive is found at a particular position.
22+
// As such it indicates that other definitions could be worth checking.
23+
var ErrNoEmbed = errors.New("no embed directive found")
24+
25+
var errStopWalk = errors.New("stop walk")
26+
27+
// EmbedDefinition finds a file matching the embed directive at pos in the mapped file.
28+
// If there is no embed directive at pos, returns ErrNoEmbed.
29+
// If multiple files match the embed pattern, one is picked at random.
30+
func EmbedDefinition(m *protocol.Mapper, pos protocol.Position) ([]protocol.Location, error) {
31+
pattern, _ := parseEmbedDirective(m, pos)
32+
if pattern == "" {
33+
return nil, ErrNoEmbed
34+
}
35+
36+
// Find the first matching file.
37+
var match string
38+
dir := filepath.Dir(m.URI.Filename())
39+
err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
40+
if e != nil {
41+
return e
42+
}
43+
rel, err := filepath.Rel(dir, abs)
44+
if err != nil {
45+
return err
46+
}
47+
ok, err := filepath.Match(pattern, rel)
48+
if err != nil {
49+
return err
50+
}
51+
if ok && !d.IsDir() {
52+
match = abs
53+
return errStopWalk
54+
}
55+
return nil
56+
})
57+
if err != nil && !errors.Is(err, errStopWalk) {
58+
return nil, err
59+
}
60+
if match == "" {
61+
return nil, fmt.Errorf("%q does not match any files in %q", pattern, dir)
62+
}
63+
64+
loc := protocol.Location{
65+
URI: protocol.URIFromPath(match),
66+
Range: protocol.Range{
67+
Start: protocol.Position{Line: 0, Character: 0},
68+
},
69+
}
70+
return []protocol.Location{loc}, nil
71+
}
72+
1773
// parseEmbedDirective attempts to parse a go:embed directive argument at pos.
1874
// If successful it return the directive argument and its range, else zero values are returned.
1975
func parseEmbedDirective(m *protocol.Mapper, pos protocol.Position) (string, protocol.Range) {

gopls/internal/lsp/source/hover.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ func hoverEmbed(fh FileHandle, rng protocol.Range, pattern string) (protocol.Ran
640640

641641
dir := filepath.Dir(fh.URI().Filename())
642642
var matches []string
643-
err := filepath.WalkDir(dir, func(abs string, _ fs.DirEntry, e error) error {
643+
err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
644644
if e != nil {
645645
return e
646646
}
@@ -652,7 +652,7 @@ func hoverEmbed(fh FileHandle, rng protocol.Range, pattern string) (protocol.Ran
652652
if err != nil {
653653
return err
654654
}
655-
if ok {
655+
if ok && !d.IsDir() {
656656
matches = append(matches, rel)
657657
}
658658
return nil

gopls/internal/regtest/misc/definition_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,43 @@ const _ = b.K
529529
}
530530
})
531531
}
532+
533+
const embedDefinition = `
534+
-- go.mod --
535+
module mod.com
536+
537+
-- main.go --
538+
package main
539+
540+
import (
541+
"embed"
542+
)
543+
544+
//go:embed *.txt
545+
var foo embed.FS
546+
547+
func main() {}
548+
549+
-- skip.sql --
550+
SKIP
551+
552+
-- foo.txt --
553+
FOO
554+
555+
-- skip.bat --
556+
SKIP
557+
`
558+
559+
func TestGoToEmbedDefinition(t *testing.T) {
560+
Run(t, embedDefinition, func(t *testing.T, env *Env) {
561+
env.OpenFile("main.go")
562+
563+
start := env.RegexpSearch("main.go", `\*.txt`)
564+
loc := env.GoToDefinition(start)
565+
566+
name := env.Sandbox.Workdir.URIToPath(loc.URI)
567+
if want := "foo.txt"; name != want {
568+
t.Errorf("GoToDefinition: got file %q, want %q", name, want)
569+
}
570+
})
571+
}

gopls/internal/regtest/misc/hover_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,8 @@ BAR
458458
BAZ
459459
-- other.sql --
460460
SKIPPED
461+
-- dir.txt/skip.txt --
462+
SKIPPED
461463
`
462464

463465
func TestHoverEmbedDirective(t *testing.T) {
@@ -478,5 +480,14 @@ func TestHoverEmbedDirective(t *testing.T) {
478480
t.Errorf("hover: %q does not contain: %q", content, want)
479481
}
480482
}
483+
484+
// A directory should never be matched, even if it happens to have a matching name.
485+
// Content in subdirectories should not match on only one asterisk.
486+
skips := []string{"other.sql", "dir.txt", "skip.txt"}
487+
for _, skip := range skips {
488+
if strings.Contains(content, skip) {
489+
t.Errorf("hover: %q should not contain: %q", content, skip)
490+
}
491+
}
481492
})
482493
}

0 commit comments

Comments
 (0)