Skip to content

Commit eed1997

Browse files
findleyrgopherbot
authored andcommitted
gopls: use relative watched file patterns if supported
The relative pattern support added in version 3.17 of the LSP solves several problems related to watched file patterns. Notably, relative patterns do not suffer from windows drive letter casing problems (since they are expressed relative to a URI), and generally work more consistently across clients. Absolute glob patterns do not work well on many clients. Fixes golang/go#64763 Change-Id: I040560f236463c71dc0efaac1ea0951fb8d98fab Reviewed-on: https://go-review.googlesource.com/c/tools/+/557499 Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent fa791ac commit eed1997

File tree

9 files changed

+108
-85
lines changed

9 files changed

+108
-85
lines changed

gopls/internal/lsp/cache/session.go

+17-7
Original file line numberDiff line numberDiff line change
@@ -1025,9 +1025,9 @@ func (b brokenFile) SameContentsOnDisk() bool { return false }
10251025
func (b brokenFile) Version() int32 { return 0 }
10261026
func (b brokenFile) Content() ([]byte, error) { return nil, b.err }
10271027

1028-
// FileWatchingGlobPatterns returns a set of glob patterns patterns that the
1029-
// client is required to watch for changes, and notify the server of them, in
1030-
// order to keep the server's state up to date.
1028+
// FileWatchingGlobPatterns returns a set of glob patterns that the client is
1029+
// required to watch for changes, and notify the server of them, in order to
1030+
// keep the server's state up to date.
10311031
//
10321032
// This set includes
10331033
// 1. all go.mod and go.work files in the workspace; and
@@ -1043,25 +1043,35 @@ func (b brokenFile) Content() ([]byte, error) { return nil, b.err }
10431043
// The watch for workspace directories in (2) should keep each View up to date,
10441044
// as it should capture any newly added/modified/deleted Go files.
10451045
//
1046+
// Patterns are returned as a set of protocol.RelativePatterns, since they can
1047+
// always be later translated to glob patterns (i.e. strings) if the client
1048+
// lacks relative pattern support. By convention, any pattern returned with
1049+
// empty baseURI should be served as a glob pattern.
1050+
//
1051+
// In general, we prefer to serve relative patterns, as they work better on
1052+
// most clients that support both, and do not have issues with Windows driver
1053+
// letter casing:
1054+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#relativePattern
1055+
//
10461056
// TODO(golang/go#57979): we need to reset the memoizedFS when a view changes.
10471057
// Consider the case where we incidentally read a file, then it moved outside
10481058
// of an active module, and subsequently changed: we would still observe the
10491059
// original file state.
1050-
func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]unit {
1060+
func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[protocol.RelativePattern]unit {
10511061
s.viewMu.Lock()
10521062
defer s.viewMu.Unlock()
10531063

10541064
// Always watch files that may change the set of views.
1055-
patterns := map[string]unit{
1056-
"**/*.{mod,work}": {},
1065+
patterns := map[protocol.RelativePattern]unit{
1066+
{Pattern: "**/*.{mod,work}"}: {},
10571067
}
10581068

10591069
for _, view := range s.views {
10601070
snapshot, release, err := view.Snapshot()
10611071
if err != nil {
10621072
continue // view is shut down; continue with others
10631073
}
1064-
for k, v := range snapshot.fileWatchingGlobPatterns(ctx) {
1074+
for k, v := range snapshot.fileWatchingGlobPatterns() {
10651075
patterns[k] = v
10661076
}
10671077
release()

gopls/internal/lsp/cache/snapshot.go

+20-19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"go/types"
1717
"io"
1818
"os"
19+
"path"
1920
"path/filepath"
2021
"regexp"
2122
"runtime"
@@ -938,21 +939,25 @@ func (s *Snapshot) resetActivePackagesLocked() {
938939

939940
// See Session.FileWatchingGlobPatterns for a description of gopls' file
940941
// watching heuristic.
941-
func (s *Snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
942-
extensions := "go,mod,sum,work"
943-
for _, ext := range s.Options().TemplateExtensions {
944-
extensions += "," + ext
945-
}
946-
942+
func (s *Snapshot) fileWatchingGlobPatterns() map[protocol.RelativePattern]unit {
947943
// Always watch files that may change the view definition.
948-
patterns := make(map[string]unit)
944+
patterns := make(map[protocol.RelativePattern]unit)
949945

950946
// If GOWORK is outside the folder, ensure we are watching it.
951947
if s.view.gowork != "" && !s.view.folder.Dir.Encloses(s.view.gowork) {
952-
// TODO(rfindley): use RelativePatterns here as well (see below).
953-
patterns[filepath.ToSlash(s.view.gowork.Path())] = unit{}
948+
workPattern := protocol.RelativePattern{
949+
BaseURI: s.view.gowork.Dir(),
950+
Pattern: path.Base(string(s.view.gowork)),
951+
}
952+
patterns[workPattern] = unit{}
954953
}
955954

955+
extensions := "go,mod,sum,work"
956+
for _, ext := range s.Options().TemplateExtensions {
957+
extensions += "," + ext
958+
}
959+
watchGoFiles := fmt.Sprintf("**/*.{%s}", extensions)
960+
956961
var dirs []string
957962
if s.view.moduleMode() {
958963
// In module mode, watch directories containing active modules, and collect
@@ -964,21 +969,17 @@ func (s *Snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
964969
dir := filepath.Dir(modFile.Path())
965970
dirs = append(dirs, dir)
966971

967-
// TODO(golang/go#64763): Switch to RelativePatterns if RelativePatternSupport
968-
// is available. Relative patterns do not have issues with Windows drive
969-
// letter casing.
970-
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#relativePattern
971-
//
972-
// TODO(golang/go#64724): thoroughly test, particularly on on Windows.
972+
// TODO(golang/go#64724): thoroughly test these patterns, particularly on
973+
// on Windows.
973974
//
974975
// Note that glob patterns should use '/' on Windows:
975976
// https://code.visualstudio.com/docs/editor/glob-patterns
976-
patterns[fmt.Sprintf("%s/**/*.{%s}", filepath.ToSlash(dir), extensions)] = unit{}
977+
patterns[protocol.RelativePattern{BaseURI: modFile.Dir(), Pattern: watchGoFiles}] = unit{}
977978
}
978979
} else {
979980
// In non-module modes (GOPATH or AdHoc), we just watch the workspace root.
980981
dirs = []string{s.view.root.Path()}
981-
patterns[fmt.Sprintf("**/*.{%s}", extensions)] = unit{}
982+
patterns[protocol.RelativePattern{Pattern: watchGoFiles}] = unit{}
982983
}
983984

984985
if s.watchSubdirs() {
@@ -1007,14 +1008,14 @@ func (s *Snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
10071008
return patterns
10081009
}
10091010

1010-
func (s *Snapshot) addKnownSubdirs(patterns map[string]unit, wsDirs []string) {
1011+
func (s *Snapshot) addKnownSubdirs(patterns map[protocol.RelativePattern]unit, wsDirs []string) {
10111012
s.mu.Lock()
10121013
defer s.mu.Unlock()
10131014

10141015
s.files.getDirs().Range(func(dir string) {
10151016
for _, wsDir := range wsDirs {
10161017
if pathutil.InDir(wsDir, dir) {
1017-
patterns[filepath.ToSlash(dir)] = unit{}
1018+
patterns[protocol.RelativePattern{Pattern: filepath.ToSlash(dir)}] = unit{}
10181019
}
10191020
}
10201021
})

gopls/internal/lsp/protocol/generate/tables.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,9 @@ var goplsType = map[string]string{
133133

134134
"Or_Declaration": "[]Location",
135135
"Or_DidChangeConfigurationRegistrationOptions_section": "OrPSection_workspace_didChangeConfiguration",
136-
"Or_GlobPattern": "string",
137-
"Or_InlayHintLabelPart_tooltip": "OrPTooltipPLabel",
138-
"Or_InlayHint_tooltip": "OrPTooltip_textDocument_inlayHint",
139-
"Or_LSPAny": "interface{}",
136+
"Or_InlayHintLabelPart_tooltip": "OrPTooltipPLabel",
137+
"Or_InlayHint_tooltip": "OrPTooltip_textDocument_inlayHint",
138+
"Or_LSPAny": "interface{}",
140139

141140
"Or_ParameterInformation_documentation": "string",
142141
"Or_ParameterInformation_label": "string",
@@ -151,6 +150,7 @@ var goplsType = map[string]string{
151150
"Or_Result_textDocument_typeDefinition": "[]Location",
152151
"Or_Result_workspace_symbol": "[]SymbolInformation",
153152
"Or_TextDocumentContentChangeEvent": "TextDocumentContentChangePartial",
153+
"Or_RelativePattern_baseUri": "DocumentURI",
154154

155155
"Or_WorkspaceFoldersServerCapabilities_changeNotifications": "string",
156156
"Or_WorkspaceSymbol_location": "OrPLocation_workspace_symbol",

gopls/internal/lsp/protocol/tsjson.go

+30-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/lsp/protocol/tsprotocol.go

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/server/general.go

+17-16
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"golang.org/x/tools/gopls/internal/telemetry"
2929
"golang.org/x/tools/gopls/internal/util/bug"
3030
"golang.org/x/tools/gopls/internal/util/goversion"
31+
"golang.org/x/tools/gopls/internal/util/maps"
3132
"golang.org/x/tools/internal/event"
3233
"golang.org/x/tools/internal/jsonrpc2"
3334
)
@@ -362,7 +363,7 @@ func (s *server) updateWatchedDirectories(ctx context.Context) error {
362363
defer s.watchedGlobPatternsMu.Unlock()
363364

364365
// Nothing to do if the set of workspace directories is unchanged.
365-
if equalURISet(s.watchedGlobPatterns, patterns) {
366+
if maps.SameKeys(s.watchedGlobPatterns, patterns) {
366367
return nil
367368
}
368369

@@ -390,32 +391,32 @@ func watchedFilesCapabilityID(id int) string {
390391
return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id)
391392
}
392393

393-
func equalURISet(m1, m2 map[string]struct{}) bool {
394-
if len(m1) != len(m2) {
395-
return false
396-
}
397-
for k := range m1 {
398-
_, ok := m2[k]
399-
if !ok {
400-
return false
401-
}
402-
}
403-
return true
404-
}
405-
406394
// registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles
407395
// registrations to the client and updates s.watchedDirectories.
408396
// The caller must not subsequently mutate patterns.
409-
func (s *server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error {
397+
func (s *server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[protocol.RelativePattern]unit) error {
410398
if !s.Options().DynamicWatchedFilesSupported {
411399
return nil
412400
}
401+
402+
supportsRelativePatterns := s.Options().RelativePatternsSupported
403+
413404
s.watchedGlobPatterns = patterns
414405
watchers := make([]protocol.FileSystemWatcher, 0, len(patterns)) // must be a slice
415406
val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate
416407
for pattern := range patterns {
408+
var value any
409+
if supportsRelativePatterns && pattern.BaseURI != "" {
410+
value = pattern
411+
} else {
412+
p := pattern.Pattern
413+
if pattern.BaseURI != "" {
414+
p = path.Join(filepath.ToSlash(pattern.BaseURI.Path()), p)
415+
}
416+
value = p
417+
}
417418
watchers = append(watchers, protocol.FileSystemWatcher{
418-
GlobPattern: pattern,
419+
GlobPattern: protocol.GlobPattern{Value: value},
419420
Kind: &val,
420421
})
421422
}

gopls/internal/server/server.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ type server struct {
8989
// that the server should watch changes.
9090
// The map field may be reassigned but the map is immutable.
9191
watchedGlobPatternsMu sync.Mutex
92-
watchedGlobPatterns map[string]unit
92+
watchedGlobPatterns map[protocol.RelativePattern]unit
9393
watchRegistrationCount int
9494

9595
diagnosticsMu sync.Mutex

gopls/internal/settings/settings.go

+2
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ type ClientOptions struct {
121121
DynamicConfigurationSupported bool
122122
DynamicRegistrationSemanticTokensSupported bool
123123
DynamicWatchedFilesSupported bool
124+
RelativePatternsSupported bool
124125
PreferredContentFormat protocol.MarkupKind
125126
LineFoldingOnly bool
126127
HierarchicalDocumentSymbolSupport bool
@@ -718,6 +719,7 @@ func (o *Options) ForClientCapabilities(clientName *protocol.ClientInfo, caps pr
718719
o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
719720
o.DynamicRegistrationSemanticTokensSupported = caps.TextDocument.SemanticTokens.DynamicRegistration
720721
o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration
722+
o.RelativePatternsSupported = caps.Workspace.DidChangeWatchedFiles.RelativePatternSupport
721723

722724
// Check which types of content format are supported by this client.
723725
if hover := caps.TextDocument.Hover; hover != nil && len(hover.ContentFormat) > 0 {

gopls/internal/test/integration/fake/client.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"context"
99
"encoding/json"
1010
"fmt"
11+
"path"
12+
"path/filepath"
1113

1214
"golang.org/x/tools/gopls/internal/lsp/protocol"
1315
"golang.org/x/tools/gopls/internal/test/integration/fake/glob"
@@ -130,8 +132,15 @@ func (c *Client) RegisterCapability(ctx context.Context, params *protocol.Regist
130132
}
131133
var globs []*glob.Glob
132134
for _, watcher := range opts.Watchers {
135+
var globPattern string
136+
switch pattern := watcher.GlobPattern.Value.(type) {
137+
case protocol.Pattern:
138+
globPattern = pattern
139+
case protocol.RelativePattern:
140+
globPattern = path.Join(filepath.ToSlash(pattern.BaseURI.Path()), pattern.Pattern)
141+
}
133142
// TODO(rfindley): honor the watch kind.
134-
g, err := glob.Parse(watcher.GlobPattern)
143+
g, err := glob.Parse(globPattern)
135144
if err != nil {
136145
return fmt.Errorf("error parsing glob pattern %q: %v", watcher.GlobPattern, err)
137146
}

0 commit comments

Comments
 (0)