@@ -31,6 +31,7 @@ import (
31
31
"golang.org/x/tools/go/packages"
32
32
"golang.org/x/tools/go/types/objectpath"
33
33
"golang.org/x/tools/gopls/internal/bug"
34
+ "golang.org/x/tools/gopls/internal/lsp/command"
34
35
"golang.org/x/tools/gopls/internal/lsp/filecache"
35
36
"golang.org/x/tools/gopls/internal/lsp/protocol"
36
37
"golang.org/x/tools/gopls/internal/lsp/source"
@@ -191,6 +192,16 @@ type snapshot struct {
191
192
// detect ignored files.
192
193
ignoreFilterOnce sync.Once
193
194
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
194
205
}
195
206
196
207
var globalSnapshotID uint64
@@ -293,7 +304,8 @@ func (s *snapshot) ModFiles() []span.URI {
293
304
}
294
305
295
306
func (s * snapshot ) WorkFile () span.URI {
296
- return s .view .effectiveGOWORK ()
307
+ gowork , _ := s .view .GOWORK ()
308
+ return gowork
297
309
}
298
310
299
311
func (s * snapshot ) Templates () map [span.URI ]source.FileHandle {
@@ -544,7 +556,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat
544
556
// the main (workspace) module. Otherwise, we should use the module for
545
557
// the passed-in working dir.
546
558
if mode == source .LoadWorkspace {
547
- if s .view .effectiveGOWORK () == "" && s .view .gomod != "" {
559
+ if gowork , _ := s .view .GOWORK (); gowork == "" && s .view .gomod != "" {
548
560
modURI = s .view .gomod
549
561
}
550
562
} else {
@@ -929,7 +941,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
929
941
}
930
942
931
943
// If GOWORK is outside the folder, ensure we are watching it.
932
- gowork := s .view .effectiveGOWORK ()
944
+ gowork , _ := s .view .GOWORK ()
933
945
if gowork != "" && ! source .InDir (s .view .folder .Filename (), gowork .Filename ()) {
934
946
patterns [gowork .Filename ()] = struct {}{}
935
947
}
@@ -1351,19 +1363,6 @@ func (s *snapshot) IsOpen(uri span.URI) bool {
1351
1363
return open
1352
1364
}
1353
1365
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
-
1367
1366
func isFileOpen (fh source.FileHandle ) bool {
1368
1367
_ , open := fh .(* Overlay )
1369
1368
return open
@@ -1588,7 +1587,7 @@ func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error {
1588
1587
// that exist only in overlays. As a workaround, we search all of the files
1589
1588
// available in the snapshot and reload their metadata individually using a
1590
1589
// file= query if the metadata is unavailable.
1591
- open := s .openFiles ()
1590
+ open := s .overlays ()
1592
1591
var files []* Overlay
1593
1592
for _ , o := range open {
1594
1593
uri := o .URI ()
@@ -1686,10 +1685,26 @@ func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error {
1686
1685
// TODO(rfindley): reconcile the definition of "orphaned" here with
1687
1686
// reloadOrphanedFiles. The latter does not include files with
1688
1687
// 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.
1690
1698
s .mu .Lock ()
1691
- meta := s .meta
1699
+ existing := s .orphanedFileDiagnostics
1692
1700
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
+ }
1693
1708
1694
1709
var files []* Overlay
1695
1710
@@ -1699,20 +1714,30 @@ searchOverlays:
1699
1714
if s .IsBuiltin (uri ) || s .view .FileKind (o ) != source .Go {
1700
1715
continue
1701
1716
}
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 {
1704
1723
continue searchOverlays
1705
1724
}
1706
1725
}
1707
1726
files = append (files , o )
1708
1727
}
1709
1728
if len (files ) == 0 {
1710
- return nil
1729
+ return nil , nil
1711
1730
}
1712
1731
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 {
1716
1741
if meta .Module != nil && meta .Module .GoMod != "" {
1717
1742
gomod := span .URIFromPath (meta .Module .GoMod )
1718
1743
loadedModFiles [gomod ] = struct {}{}
@@ -1740,24 +1765,85 @@ searchOverlays:
1740
1765
continue
1741
1766
}
1742
1767
1768
+ var (
1769
+ msg string // if non-empty, report a diagnostic with this message
1770
+ suggestedFixes []source.SuggestedFix // associated fixes, if any
1771
+ )
1772
+
1743
1773
// If we have a relevant go.mod file, check whether the file is orphaned
1744
1774
// due to its go.mod file being inactive. We could also offer a
1745
1775
// prescriptive diagnostic in the case that there is no go.mod file, but it
1746
1776
// is harder to be precise in that case, and less important.
1747
- var msg string
1748
1777
if goMod , err := nearestModFile (ctx , fh .URI (), s ); err == nil && goMod != "" {
1749
1778
if _ , ok := loadedModFiles [goMod ]; ! ok {
1750
1779
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 {
1752
1787
modDir = rel
1753
1788
}
1754
1789
1755
1790
var fix string
1756
1791
if s .view .goversion >= 18 {
1757
1792
if s .view .gowork != "" {
1758
1793
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
+ }
1759
1817
} else {
1760
1818
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
+ }
1761
1847
}
1762
1848
} else {
1763
1849
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
1794
1880
if msg != "" {
1795
1881
// Only report diagnostics if we detect an actual exclusion.
1796
1882
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 ,
1802
1889
}
1803
1890
}
1804
1891
}
1805
1892
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
1807
1899
}
1808
1900
1809
1901
// 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
1848
1940
reinit := false
1849
1941
wsModFiles , wsModFilesErr := s .workspaceModFiles , s .workspaceModFilesErr
1850
1942
1851
- if workURI := s .view .effectiveGOWORK (); workURI != "" {
1943
+ if workURI , _ := s .view .GOWORK (); workURI != "" {
1852
1944
if change , ok := changes [workURI ]; ok {
1853
1945
wsModFiles , wsModFilesErr = computeWorkspaceModFiles (ctx , s .view .gomod , workURI , s .view .effectiveGO111MODULE (), & unappliedChanges {
1854
1946
originalSnapshot : s ,
0 commit comments