Skip to content

Commit

Permalink
gopls/internal/lsp: go to definition from embed directive
Browse files Browse the repository at this point in the history
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 <rfindley@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
vikblom authored and findleyr committed Oct 5, 2023
1 parent 2be977e commit db1d1e0
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 8 deletions.
6 changes: 0 additions & 6 deletions gopls/internal/lsp/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package lsp

import (
"context"
"errors"
"fmt"

"golang.org/x/tools/gopls/internal/lsp/protocol"
Expand Down Expand Up @@ -36,11 +35,6 @@ func (s *Server) definition(ctx context.Context, params *protocol.DefinitionPara
case source.Tmpl:
return template.Definition(snapshot, fh, params.Position)
case source.Go:
// Partial support for jumping from linkname directive (position at 2nd argument).
locations, err := source.LinknameDefinition(ctx, snapshot, fh, params.Position)
if !errors.Is(err, source.ErrNoLinkname) {
return locations, err
}
return source.Definition(ctx, snapshot, fh, params.Position)
default:
return nil, fmt.Errorf("can't find definitions for file type %s", kind)
Expand Down
13 changes: 13 additions & 0 deletions gopls/internal/lsp/source/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package source

import (
"context"
"errors"
"fmt"
"go/ast"
"go/token"
Expand Down Expand Up @@ -58,6 +59,18 @@ func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position
return []protocol.Location{loc}, nil
}

// Handle the case where the cursor is in a linkname directive.
locations, err := LinknameDefinition(ctx, snapshot, fh, position)
if !errors.Is(err, ErrNoLinkname) {
return locations, err
}

// Handle the case where the cursor is in an embed directive.
locations, err = EmbedDefinition(pgf.Mapper, position)
if !errors.Is(err, ErrNoEmbed) {
return locations, err
}

// The general case: the cursor is on an identifier.
_, obj, _ := referencedObject(pkg, pgf, pos)
if obj == nil {
Expand Down
56 changes: 56 additions & 0 deletions gopls/internal/lsp/source/embeddirective.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
package source

import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"strconv"
"strings"
"unicode"
Expand All @@ -14,6 +17,59 @@ import (
"golang.org/x/tools/gopls/internal/lsp/protocol"
)

// ErrNoEmbed is returned by EmbedDefinition when no embed
// directive is found at a particular position.
// As such it indicates that other definitions could be worth checking.
var ErrNoEmbed = errors.New("no embed directive found")

var errStopWalk = errors.New("stop walk")

// EmbedDefinition finds a file matching the embed directive at pos in the mapped file.
// If there is no embed directive at pos, returns ErrNoEmbed.
// If multiple files match the embed pattern, one is picked at random.
func EmbedDefinition(m *protocol.Mapper, pos protocol.Position) ([]protocol.Location, error) {
pattern, _ := parseEmbedDirective(m, pos)
if pattern == "" {
return nil, ErrNoEmbed
}

// Find the first matching file.
var match string
dir := filepath.Dir(m.URI.Filename())
err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
if e != nil {
return e
}
rel, err := filepath.Rel(dir, abs)
if err != nil {
return err
}
ok, err := filepath.Match(pattern, rel)
if err != nil {
return err
}
if ok && !d.IsDir() {
match = abs
return errStopWalk
}
return nil
})
if err != nil && !errors.Is(err, errStopWalk) {
return nil, err
}
if match == "" {
return nil, fmt.Errorf("%q does not match any files in %q", pattern, dir)
}

loc := protocol.Location{
URI: protocol.URIFromPath(match),
Range: protocol.Range{
Start: protocol.Position{Line: 0, Character: 0},
},
}
return []protocol.Location{loc}, nil
}

// parseEmbedDirective attempts to parse a go:embed directive argument at pos.
// If successful it return the directive argument and its range, else zero values are returned.
func parseEmbedDirective(m *protocol.Mapper, pos protocol.Position) (string, protocol.Range) {
Expand Down
4 changes: 2 additions & 2 deletions gopls/internal/lsp/source/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ func hoverEmbed(fh FileHandle, rng protocol.Range, pattern string) (protocol.Ran

dir := filepath.Dir(fh.URI().Filename())
var matches []string
err := filepath.WalkDir(dir, func(abs string, _ fs.DirEntry, e error) error {
err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
if e != nil {
return e
}
Expand All @@ -652,7 +652,7 @@ func hoverEmbed(fh FileHandle, rng protocol.Range, pattern string) (protocol.Ran
if err != nil {
return err
}
if ok {
if ok && !d.IsDir() {
matches = append(matches, rel)
}
return nil
Expand Down
40 changes: 40 additions & 0 deletions gopls/internal/regtest/misc/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,43 @@ const _ = b.K
}
})
}

const embedDefinition = `
-- go.mod --
module mod.com
-- main.go --
package main
import (
"embed"
)
//go:embed *.txt
var foo embed.FS
func main() {}
-- skip.sql --
SKIP
-- foo.txt --
FOO
-- skip.bat --
SKIP
`

func TestGoToEmbedDefinition(t *testing.T) {
Run(t, embedDefinition, func(t *testing.T, env *Env) {
env.OpenFile("main.go")

start := env.RegexpSearch("main.go", `\*.txt`)
loc := env.GoToDefinition(start)

name := env.Sandbox.Workdir.URIToPath(loc.URI)
if want := "foo.txt"; name != want {
t.Errorf("GoToDefinition: got file %q, want %q", name, want)
}
})
}
11 changes: 11 additions & 0 deletions gopls/internal/regtest/misc/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,8 @@ BAR
BAZ
-- other.sql --
SKIPPED
-- dir.txt/skip.txt --
SKIPPED
`

func TestHoverEmbedDirective(t *testing.T) {
Expand All @@ -478,5 +480,14 @@ func TestHoverEmbedDirective(t *testing.T) {
t.Errorf("hover: %q does not contain: %q", content, want)
}
}

// A directory should never be matched, even if it happens to have a matching name.
// Content in subdirectories should not match on only one asterisk.
skips := []string{"other.sql", "dir.txt", "skip.txt"}
for _, skip := range skips {
if strings.Contains(content, skip) {
t.Errorf("hover: %q should not contain: %q", content, skip)
}
}
})
}

0 comments on commit db1d1e0

Please sign in to comment.