Skip to content

Commit a13793e

Browse files
committed
gopls/internal/lsp: add quick-fixes to manage the go.work file
Update OrphanedFileDiagnostics to provide suggested fixes for diagnostics related to modules that are not activated by a relevant go.work file. Also remove the Snapshot.openFiles method, which was completely redundant with Snapshot.overlays. Fixes golang/go#53880 Change-Id: I7e7aed97fb0b93415fe3dc383b6daea15241f31b Reviewed-on: https://go-review.googlesource.com/c/tools/+/494738 Reviewed-by: Mouffull <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 12a0517 commit a13793e

File tree

18 files changed

+717
-79
lines changed

18 files changed

+717
-79
lines changed

gopls/doc/commands.md

+15
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,21 @@ Args:
289289
}
290290
```
291291

292+
### **run `go work [args...]`, and apply the resulting go.work**
293+
Identifier: `gopls.run_go_work_command`
294+
295+
edits to the current go.work file.
296+
297+
Args:
298+
299+
```
300+
{
301+
"ViewID": string,
302+
"InitFirst": bool,
303+
"Args": []string,
304+
}
305+
```
306+
292307
### **Run govulncheck.**
293308
Identifier: `gopls.run_govulncheck`
294309

gopls/internal/lsp/cache/load.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ func (s *snapshot) workspaceLayoutError(ctx context.Context) (error, []*source.D
354354

355355
// Apply diagnostics about the workspace configuration to relevant open
356356
// files.
357-
openFiles := s.openFiles()
357+
openFiles := s.overlays()
358358

359359
// If the snapshot does not have a valid build configuration, it may be
360360
// that the user has opened a directory that contains multiple modules.

gopls/internal/lsp/cache/session.go

+21-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ type Session struct {
4646
func (s *Session) ID() string { return s.id }
4747
func (s *Session) String() string { return s.id }
4848

49+
// GoCommandRunner returns the gocommand Runner for this session.
50+
func (s *Session) GoCommandRunner() *gocommand.Runner {
51+
return s.gocmdRunner
52+
}
53+
4954
// Options returns a copy of the SessionOptions for this session.
5055
func (s *Session) Options() *source.Options {
5156
s.optionsMu.Lock()
@@ -113,7 +118,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
113118
return nil, nil, func() {}, err
114119
}
115120

116-
wsModFiles, wsModFilesErr := computeWorkspaceModFiles(ctx, info.gomod, info.effectiveGOWORK(), info.effectiveGO111MODULE(), s)
121+
gowork, _ := info.GOWORK()
122+
wsModFiles, wsModFilesErr := computeWorkspaceModFiles(ctx, info.gomod, gowork, info.effectiveGO111MODULE(), s)
117123

118124
// We want a true background context and not a detached context here
119125
// the spans need to be unrelated and no tag values should pollute it.
@@ -199,8 +205,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
199205
return v, snapshot, snapshot.Acquire(), nil
200206
}
201207

202-
// View returns a view with a matching name, if the session has one.
203-
func (s *Session) View(name string) *View {
208+
// ViewByName returns a view with a matching name, if the session has one.
209+
func (s *Session) ViewByName(name string) *View {
204210
s.viewMu.Lock()
205211
defer s.viewMu.Unlock()
206212
for _, view := range s.views {
@@ -211,6 +217,18 @@ func (s *Session) View(name string) *View {
211217
return nil
212218
}
213219

220+
// View returns the view with a matching id, if present.
221+
func (s *Session) View(id string) (*View, error) {
222+
s.viewMu.Lock()
223+
defer s.viewMu.Unlock()
224+
for _, view := range s.views {
225+
if view.ID() == id {
226+
return view, nil
227+
}
228+
}
229+
return nil, fmt.Errorf("no view with ID %q", id)
230+
}
231+
214232
// ViewOf returns a view corresponding to the given URI.
215233
// If the file is not already associated with a view, pick one using some heuristics.
216234
func (s *Session) ViewOf(uri span.URI) (*View, error) {

gopls/internal/lsp/cache/snapshot.go

+126-34
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"golang.org/x/tools/go/packages"
3232
"golang.org/x/tools/go/types/objectpath"
3333
"golang.org/x/tools/gopls/internal/bug"
34+
"golang.org/x/tools/gopls/internal/lsp/command"
3435
"golang.org/x/tools/gopls/internal/lsp/filecache"
3536
"golang.org/x/tools/gopls/internal/lsp/protocol"
3637
"golang.org/x/tools/gopls/internal/lsp/source"
@@ -191,6 +192,16 @@ type snapshot struct {
191192
// detect ignored files.
192193
ignoreFilterOnce sync.Once
193194
ignoreFilter *ignoreFilter
195+
196+
// If non-nil, the result of computing orphaned file diagnostics.
197+
//
198+
// Only the field, not the map itself, is guarded by the mutex. The map must
199+
// not be mutated.
200+
//
201+
// Used to save work across diagnostics+code action passes.
202+
// TODO(rfindley): refactor all of this so there's no need to re-evaluate
203+
// diagnostics during code-action.
204+
orphanedFileDiagnostics map[span.URI]*source.Diagnostic
194205
}
195206

196207
var globalSnapshotID uint64
@@ -293,7 +304,8 @@ func (s *snapshot) ModFiles() []span.URI {
293304
}
294305

295306
func (s *snapshot) WorkFile() span.URI {
296-
return s.view.effectiveGOWORK()
307+
gowork, _ := s.view.GOWORK()
308+
return gowork
297309
}
298310

299311
func (s *snapshot) Templates() map[span.URI]source.FileHandle {
@@ -544,7 +556,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat
544556
// the main (workspace) module. Otherwise, we should use the module for
545557
// the passed-in working dir.
546558
if mode == source.LoadWorkspace {
547-
if s.view.effectiveGOWORK() == "" && s.view.gomod != "" {
559+
if gowork, _ := s.view.GOWORK(); gowork == "" && s.view.gomod != "" {
548560
modURI = s.view.gomod
549561
}
550562
} else {
@@ -929,7 +941,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
929941
}
930942

931943
// If GOWORK is outside the folder, ensure we are watching it.
932-
gowork := s.view.effectiveGOWORK()
944+
gowork, _ := s.view.GOWORK()
933945
if gowork != "" && !source.InDir(s.view.folder.Filename(), gowork.Filename()) {
934946
patterns[gowork.Filename()] = struct{}{}
935947
}
@@ -1351,19 +1363,6 @@ func (s *snapshot) IsOpen(uri span.URI) bool {
13511363
return open
13521364
}
13531365

1354-
func (s *snapshot) openFiles() []*Overlay {
1355-
s.mu.Lock()
1356-
defer s.mu.Unlock()
1357-
1358-
var open []*Overlay
1359-
s.files.Range(func(uri span.URI, fh source.FileHandle) {
1360-
if o, ok := fh.(*Overlay); ok {
1361-
open = append(open, o)
1362-
}
1363-
})
1364-
return open
1365-
}
1366-
13671366
func isFileOpen(fh source.FileHandle) bool {
13681367
_, open := fh.(*Overlay)
13691368
return open
@@ -1588,7 +1587,7 @@ func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error {
15881587
// that exist only in overlays. As a workaround, we search all of the files
15891588
// available in the snapshot and reload their metadata individually using a
15901589
// file= query if the metadata is unavailable.
1591-
open := s.openFiles()
1590+
open := s.overlays()
15921591
var files []*Overlay
15931592
for _, o := range open {
15941593
uri := o.URI()
@@ -1686,10 +1685,26 @@ func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error {
16861685
// TODO(rfindley): reconcile the definition of "orphaned" here with
16871686
// reloadOrphanedFiles. The latter does not include files with
16881687
// command-line-arguments packages.
1689-
func (s *snapshot) OrphanedFileDiagnostics(ctx context.Context) map[span.URI]*source.Diagnostic {
1688+
func (s *snapshot) OrphanedFileDiagnostics(ctx context.Context) (map[span.URI]*source.Diagnostic, error) {
1689+
// Orphaned file diagnostics are queried from code actions to produce
1690+
// quick-fixes (and may be queried many times, once for each file).
1691+
//
1692+
// Because they are non-trivial to compute, record them optimistically to
1693+
// avoid most redundant work.
1694+
//
1695+
// This is a hacky workaround: in the future we should avoid recomputing
1696+
// anything when codeActions provide a diagnostic: simply read the published
1697+
// diagnostic, if it exists.
16901698
s.mu.Lock()
1691-
meta := s.meta
1699+
existing := s.orphanedFileDiagnostics
16921700
s.mu.Unlock()
1701+
if existing != nil {
1702+
return existing, nil
1703+
}
1704+
1705+
if err := s.awaitLoaded(ctx); err != nil {
1706+
return nil, err
1707+
}
16931708

16941709
var files []*Overlay
16951710

@@ -1699,20 +1714,30 @@ searchOverlays:
16991714
if s.IsBuiltin(uri) || s.view.FileKind(o) != source.Go {
17001715
continue
17011716
}
1702-
for _, id := range meta.ids[o.URI()] {
1703-
if !source.IsCommandLineArguments(id) || meta.metadata[id].Standalone {
1717+
md, err := s.MetadataForFile(ctx, uri)
1718+
if err != nil {
1719+
return nil, err
1720+
}
1721+
for _, m := range md {
1722+
if !source.IsCommandLineArguments(m.ID) || m.Standalone {
17041723
continue searchOverlays
17051724
}
17061725
}
17071726
files = append(files, o)
17081727
}
17091728
if len(files) == 0 {
1710-
return nil
1729+
return nil, nil
17111730
}
17121731

1713-
loadedModFiles := make(map[span.URI]struct{})
1714-
ignoredFiles := make(map[span.URI]bool)
1715-
for _, meta := range meta.metadata {
1732+
loadedModFiles := make(map[span.URI]struct{}) // all mod files, including dependencies
1733+
ignoredFiles := make(map[span.URI]bool) // files reported in packages.Package.IgnoredFiles
1734+
1735+
meta, err := s.AllMetadata(ctx)
1736+
if err != nil {
1737+
return nil, err
1738+
}
1739+
1740+
for _, meta := range meta {
17161741
if meta.Module != nil && meta.Module.GoMod != "" {
17171742
gomod := span.URIFromPath(meta.Module.GoMod)
17181743
loadedModFiles[gomod] = struct{}{}
@@ -1740,24 +1765,85 @@ searchOverlays:
17401765
continue
17411766
}
17421767

1768+
var (
1769+
msg string // if non-empty, report a diagnostic with this message
1770+
suggestedFixes []source.SuggestedFix // associated fixes, if any
1771+
)
1772+
17431773
// If we have a relevant go.mod file, check whether the file is orphaned
17441774
// due to its go.mod file being inactive. We could also offer a
17451775
// prescriptive diagnostic in the case that there is no go.mod file, but it
17461776
// is harder to be precise in that case, and less important.
1747-
var msg string
17481777
if goMod, err := nearestModFile(ctx, fh.URI(), s); err == nil && goMod != "" {
17491778
if _, ok := loadedModFiles[goMod]; !ok {
17501779
modDir := filepath.Dir(goMod.Filename())
1751-
if rel, err := filepath.Rel(s.view.folder.Filename(), modDir); err == nil {
1780+
viewDir := s.view.folder.Filename()
1781+
1782+
// When the module is underneath the view dir, we offer
1783+
// "use all modules" quick-fixes.
1784+
inDir := source.InDir(viewDir, modDir)
1785+
1786+
if rel, err := filepath.Rel(viewDir, modDir); err == nil {
17521787
modDir = rel
17531788
}
17541789

17551790
var fix string
17561791
if s.view.goversion >= 18 {
17571792
if s.view.gowork != "" {
17581793
fix = fmt.Sprintf("To fix this problem, you can add this module to your go.work file (%s)", s.view.gowork)
1794+
if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use`", command.RunGoWorkArgs{
1795+
ViewID: s.view.ID(),
1796+
Args: []string{"use", modDir},
1797+
}); err == nil {
1798+
suggestedFixes = append(suggestedFixes, source.SuggestedFix{
1799+
Title: "Use this module in your go.work file",
1800+
Command: &cmd,
1801+
ActionKind: protocol.QuickFix,
1802+
})
1803+
}
1804+
1805+
if inDir {
1806+
if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use -r`", command.RunGoWorkArgs{
1807+
ViewID: s.view.ID(),
1808+
Args: []string{"use", "-r", "."},
1809+
}); err == nil {
1810+
suggestedFixes = append(suggestedFixes, source.SuggestedFix{
1811+
Title: "Use all modules in your workspace",
1812+
Command: &cmd,
1813+
ActionKind: protocol.QuickFix,
1814+
})
1815+
}
1816+
}
17591817
} else {
17601818
fix = "To fix this problem, you can add a go.work file that uses this directory."
1819+
1820+
if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use`", command.RunGoWorkArgs{
1821+
ViewID: s.view.ID(),
1822+
InitFirst: true,
1823+
Args: []string{"use", modDir},
1824+
}); err == nil {
1825+
suggestedFixes = []source.SuggestedFix{
1826+
{
1827+
Title: "Add a go.work file using this module",
1828+
Command: &cmd,
1829+
ActionKind: protocol.QuickFix,
1830+
},
1831+
}
1832+
}
1833+
1834+
if inDir {
1835+
if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use -r`", command.RunGoWorkArgs{
1836+
ViewID: s.view.ID(),
1837+
InitFirst: true,
1838+
Args: []string{"use", "-r", "."},
1839+
}); err == nil {
1840+
suggestedFixes = append(suggestedFixes, source.SuggestedFix{
1841+
Title: "Add a go.work file using all modules in your workspace",
1842+
Command: &cmd,
1843+
ActionKind: protocol.QuickFix,
1844+
})
1845+
}
1846+
}
17611847
}
17621848
} else {
17631849
fix = `To work with multiple modules simultaneously, please upgrade to Go 1.18 or
@@ -1794,16 +1880,22 @@ https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-str
17941880
if msg != "" {
17951881
// Only report diagnostics if we detect an actual exclusion.
17961882
diagnostics[fh.URI()] = &source.Diagnostic{
1797-
URI: fh.URI(),
1798-
Range: rng,
1799-
Severity: protocol.SeverityWarning,
1800-
Source: source.ListError,
1801-
Message: msg,
1883+
URI: fh.URI(),
1884+
Range: rng,
1885+
Severity: protocol.SeverityWarning,
1886+
Source: source.ListError,
1887+
Message: msg,
1888+
SuggestedFixes: suggestedFixes,
18021889
}
18031890
}
18041891
}
18051892

1806-
return diagnostics
1893+
s.mu.Lock()
1894+
defer s.mu.Unlock()
1895+
if s.orphanedFileDiagnostics == nil { // another thread may have won the race
1896+
s.orphanedFileDiagnostics = diagnostics
1897+
}
1898+
return s.orphanedFileDiagnostics, nil
18071899
}
18081900

18091901
// TODO(golang/go#53756): this function needs to consider more than just the
@@ -1848,7 +1940,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
18481940
reinit := false
18491941
wsModFiles, wsModFilesErr := s.workspaceModFiles, s.workspaceModFilesErr
18501942

1851-
if workURI := s.view.effectiveGOWORK(); workURI != "" {
1943+
if workURI, _ := s.view.GOWORK(); workURI != "" {
18521944
if change, ok := changes[workURI]; ok {
18531945
wsModFiles, wsModFilesErr = computeWorkspaceModFiles(ctx, s.view.gomod, workURI, s.view.effectiveGO111MODULE(), &unappliedChanges{
18541946
originalSnapshot: s,

gopls/internal/lsp/cache/view.go

+8-5
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,16 @@ func (w workspaceInformation) effectiveGO111MODULE() go111module {
145145
}
146146
}
147147

148-
// effectiveGOWORK returns the effective GOWORK value for this workspace, if
148+
// GOWORK returns the effective GOWORK value for this workspace, if
149149
// any, in URI form.
150-
func (w workspaceInformation) effectiveGOWORK() span.URI {
150+
//
151+
// The second result reports whether the effective GOWORK value is "" because
152+
// GOWORK=off.
153+
func (w workspaceInformation) GOWORK() (span.URI, bool) {
151154
if w.gowork == "off" || w.gowork == "" {
152-
return ""
155+
return "", w.gowork == "off"
153156
}
154-
return span.URIFromPath(w.gowork)
157+
return span.URIFromPath(w.gowork), false
155158
}
156159

157160
// GO111MODULE returns the value of GO111MODULE to use for running the go
@@ -540,7 +543,7 @@ func (v *View) relevantChange(c source.FileModification) bool {
540543
//
541544
// TODO(rfindley): Make sure the go.work files are always known
542545
// to the view.
543-
if c.URI == v.effectiveGOWORK() {
546+
if gowork, _ := v.GOWORK(); gowork == c.URI {
544547
return true
545548
}
546549

0 commit comments

Comments
 (0)