diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 4ff6f72586..5648dc9297 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -829,6 +829,42 @@ replace }) } +func TestUseGoWorkHover(t *testing.T) { + const files = ` +-- go.work -- +go 1.18 + +use ./foo +use ( + ./bar + ./bar/baz +) +-- foo/go.mod -- +module example.com/foo +-- bar/go.mod -- +module example.com/bar +-- bar/baz/go.mod -- +module example.com/bar/baz +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("go.work") + + tcs := map[string]string{ + `\./foo`: "example.com/foo", + `(?m)\./bar$`: "example.com/bar", + `\./bar/baz`: "example.com/bar/baz", + } + + for hoverRE, want := range tcs { + pos := env.RegexpSearch("go.work", hoverRE) + got, _ := env.Hover("go.work", pos) + if got.Value != want { + t.Errorf(`hover on %q: got %q, want %q`, hoverRE, got, want) + } + } + }) +} + func TestNonWorkspaceFileCreation(t *testing.T) { testenv.NeedsGo1Point(t, 13) diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index e9a7d9a904..d59f5dbdb3 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -11,6 +11,7 @@ import ( "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/internal/lsp/work" ) func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { @@ -26,6 +27,8 @@ func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*prot return source.Hover(ctx, snapshot, fh, params.Position) case source.Tmpl: return template.Hover(ctx, snapshot, fh, params.Position) + case source.Work: + return work.Hover(ctx, snapshot, fh, params.Position) } return nil, nil } diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go index 82ba20ff38..0837e2aaa4 100644 --- a/internal/lsp/mod/hover.go +++ b/internal/lsp/mod/hover.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" errors "golang.org/x/xerrors" ) @@ -85,20 +84,10 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the range to highlight for the hover. - line, col, err := pm.Mapper.Converter.ToPosition(startPos) + rng, err := source.ByteOffsetsToRange(pm.Mapper, fh.URI(), startPos, endPos) if err != nil { return nil, err } - start := span.NewPoint(line, col, startPos) - - line, col, err = pm.Mapper.Converter.ToPosition(endPos) - if err != nil { - return nil, err - } - end := span.NewPoint(line, col, endPos) - - spn = span.New(fh.URI(), start, end) - rng, err := pm.Mapper.Range(spn) if err != nil { return nil, err } diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index e41c1d4658..71892eaa1c 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -567,15 +567,20 @@ func InRange(tok *token.File, pos token.Pos) bool { // LineToRange creates a Range spanning start and end. func LineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) { - line, col, err := m.Converter.ToPosition(start.Byte) + return ByteOffsetsToRange(m, uri, start.Byte, end.Byte) +} + +// ByteOffsetsToRange creates a range spanning start and end. +func ByteOffsetsToRange(m *protocol.ColumnMapper, uri span.URI, start, end int) (protocol.Range, error) { + line, col, err := m.Converter.ToPosition(start) if err != nil { return protocol.Range{}, err } - s := span.NewPoint(line, col, start.Byte) - line, col, err = m.Converter.ToPosition(end.Byte) + s := span.NewPoint(line, col, start) + line, col, err = m.Converter.ToPosition(end) if err != nil { return protocol.Range{}, err } - e := span.NewPoint(line, col, end.Byte) + e := span.NewPoint(line, col, end) return m.Range(span.New(uri, s, e)) } diff --git a/internal/lsp/work/diagnostics.go b/internal/lsp/work/diagnostics.go index d752484fe2..e583e60fd7 100644 --- a/internal/lsp/work/diagnostics.go +++ b/internal/lsp/work/diagnostics.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" + "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/debug/tag" "golang.org/x/tools/internal/lsp/protocol" @@ -57,19 +58,13 @@ func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source // Add diagnostic if a directory does not contain a module. var diagnostics []*source.Diagnostic - workdir := filepath.Dir(pw.URI.Filename()) for _, use := range pw.File.Use { - modroot := filepath.FromSlash(use.Path) - if !filepath.IsAbs(modroot) { - modroot = filepath.Join(workdir, modroot) - } - rng, err := source.LineToRange(pw.Mapper, fh.URI(), use.Syntax.Start, use.Syntax.End) if err != nil { return nil, err } - modfh, err := snapshot.GetFile(ctx, span.URIFromPath(filepath.Join(modroot, "go.mod"))) + modfh, err := snapshot.GetFile(ctx, modFileURI(pw, use)) if err != nil { return nil, err } @@ -85,3 +80,14 @@ func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source } return diagnostics, nil } + +func modFileURI(pw *source.ParsedWorkFile, use *modfile.Use) span.URI { + workdir := filepath.Dir(pw.URI.Filename()) + + modroot := filepath.FromSlash(use.Path) + if !filepath.IsAbs(modroot) { + modroot = filepath.Join(workdir, modroot) + } + + return span.URIFromPath(filepath.Join(modroot, "go.mod")) +} diff --git a/internal/lsp/work/hover.go b/internal/lsp/work/hover.go new file mode 100644 index 0000000000..7cf2b981a6 --- /dev/null +++ b/internal/lsp/work/hover.go @@ -0,0 +1,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" + "go/token" + + "golang.org/x/mod/modfile" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/lsp/source" + errors "golang.org/x/xerrors" +) + +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, errors.Errorf("getting go.work file handle: %w", err) + } + spn, err := pw.Mapper.PointSpan(position) + if err != nil { + return nil, errors.Errorf("computing cursor position: %w", err) + } + hoverRng, err := spn.Range(pw.Mapper.Converter) + if err != nil { + return nil, errors.Errorf("computing hover range: %w", err) + } + + // Confirm that the cursor is inside a use statement, and then find + // the position of the use statement's directory path. + var use *modfile.Use + var pathStart, pathEnd int + for _, u := range pw.File.Use { + dep := []byte(u.Path) + s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte + i := bytes.Index(pw.Mapper.Content[s:e], dep) + 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(dep) + if token.Pos(pathStart) <= hoverRng.Start && hoverRng.Start <= token.Pos(pathEnd) { + use = u + break + } + } + + // 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)) + pm, err := snapshot.ParseMod(ctx, modfh) + if err != nil { + return nil, errors.Errorf("getting modfile handle: %w", err) + } + mod := pm.File.Module.Mod + + // Get the range to highlight for the hover. + rng, err := source.ByteOffsetsToRange(pw.Mapper, fh.URI(), 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 +}