| // Copyright 2019 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 cache |
| |
| import ( |
| "context" |
| "fmt" |
| "strconv" |
| "strings" |
| "sync" |
| "sync/atomic" |
| |
| "golang.org/x/tools/gopls/internal/govulncheck" |
| "golang.org/x/tools/gopls/internal/lsp/source" |
| "golang.org/x/tools/gopls/internal/span" |
| "golang.org/x/tools/internal/bug" |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/internal/gocommand" |
| "golang.org/x/tools/internal/imports" |
| "golang.org/x/tools/internal/persistent" |
| "golang.org/x/tools/internal/xcontext" |
| ) |
| |
| type Session struct { |
| // Unique identifier for this session. |
| id string |
| |
| // Immutable attributes shared across views. |
| cache *Cache // shared cache |
| gocmdRunner *gocommand.Runner // limits go command concurrency |
| |
| optionsMu sync.Mutex |
| options *source.Options |
| |
| viewMu sync.Mutex |
| views []*View |
| viewMap map[span.URI]*View // map of URI->best view |
| |
| *overlayFS |
| } |
| |
| // ID returns the unique identifier for this session on this server. |
| func (s *Session) ID() string { return s.id } |
| func (s *Session) String() string { return s.id } |
| |
| // Options returns a copy of the SessionOptions for this session. |
| func (s *Session) Options() *source.Options { |
| s.optionsMu.Lock() |
| defer s.optionsMu.Unlock() |
| return s.options |
| } |
| |
| // SetOptions sets the options of this session to new values. |
| func (s *Session) SetOptions(options *source.Options) { |
| s.optionsMu.Lock() |
| defer s.optionsMu.Unlock() |
| s.options = options |
| } |
| |
| // Shutdown the session and all views it has created. |
| func (s *Session) Shutdown(ctx context.Context) { |
| var views []*View |
| s.viewMu.Lock() |
| views = append(views, s.views...) |
| s.views = nil |
| s.viewMap = nil |
| s.viewMu.Unlock() |
| for _, view := range views { |
| view.shutdown() |
| } |
| event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s)) |
| } |
| |
| // Cache returns the cache that created this session, for debugging only. |
| func (s *Session) Cache() *Cache { |
| return s.cache |
| } |
| |
| // NewView creates a new View, returning it and its first snapshot. If a |
| // non-empty tempWorkspace directory is provided, the View will record a copy |
| // of its gopls workspace module in that directory, so that client tooling |
| // can execute in the same main module. On success it also returns a release |
| // function that must be called when the Snapshot is no longer needed. |
| func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (*View, source.Snapshot, func(), error) { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| for _, view := range s.views { |
| if span.SameExistingFile(view.folder, folder) { |
| return nil, nil, nil, source.ErrViewExists |
| } |
| } |
| view, snapshot, release, err := s.createView(ctx, name, folder, options, 0) |
| if err != nil { |
| return nil, nil, nil, err |
| } |
| s.views = append(s.views, view) |
| // we always need to drop the view map |
| s.viewMap = make(map[span.URI]*View) |
| return view, snapshot, release, nil |
| } |
| |
| // TODO(rfindley): clarify that createView can never be cancelled (with the |
| // possible exception of server shutdown). |
| func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) { |
| index := atomic.AddInt64(&viewIndex, 1) |
| |
| // Get immutable workspace information. |
| info, err := s.getWorkspaceInformation(ctx, folder, options) |
| if err != nil { |
| return nil, nil, func() {}, err |
| } |
| |
| wsModFiles, wsModFilesErr := computeWorkspaceModFiles(ctx, info.gomod, info.effectiveGOWORK(), info.effectiveGO111MODULE(), s) |
| |
| // We want a true background context and not a detached context here |
| // the spans need to be unrelated and no tag values should pollute it. |
| baseCtx := event.Detach(xcontext.Detach(ctx)) |
| backgroundCtx, cancel := context.WithCancel(baseCtx) |
| |
| v := &View{ |
| id: strconv.FormatInt(index, 10), |
| gocmdRunner: s.gocmdRunner, |
| initialWorkspaceLoad: make(chan struct{}), |
| initializationSema: make(chan struct{}, 1), |
| options: options, |
| baseCtx: baseCtx, |
| name: name, |
| folder: folder, |
| moduleUpgrades: map[span.URI]map[string]string{}, |
| vulns: map[span.URI]*govulncheck.Result{}, |
| fs: s.overlayFS, |
| workspaceInformation: info, |
| } |
| v.importsState = &importsState{ |
| ctx: backgroundCtx, |
| processEnv: &imports.ProcessEnv{ |
| GocmdRunner: s.gocmdRunner, |
| SkipPathInScan: func(dir string) bool { |
| prefix := strings.TrimSuffix(string(v.folder), "/") + "/" |
| uri := strings.TrimSuffix(string(span.URIFromPath(dir)), "/") |
| if !strings.HasPrefix(uri+"/", prefix) { |
| return false |
| } |
| filterer := source.NewFilterer(options.DirectoryFilters) |
| rel := strings.TrimPrefix(uri, prefix) |
| disallow := filterer.Disallow(rel) |
| return disallow |
| }, |
| }, |
| } |
| v.snapshot = &snapshot{ |
| sequenceID: seqID, |
| globalID: nextSnapshotID(), |
| view: v, |
| backgroundCtx: backgroundCtx, |
| cancel: cancel, |
| store: s.cache.store, |
| packages: persistent.NewMap(packageIDLessInterface), |
| meta: new(metadataGraph), |
| files: newFilesMap(), |
| parseCache: new(parseCache), |
| activePackages: persistent.NewMap(packageIDLessInterface), |
| symbolizeHandles: persistent.NewMap(uriLessInterface), |
| analyses: persistent.NewMap(analysisKeyLessInterface), |
| workspacePackages: make(map[PackageID]PackagePath), |
| unloadableFiles: make(map[span.URI]struct{}), |
| parseModHandles: persistent.NewMap(uriLessInterface), |
| parseWorkHandles: persistent.NewMap(uriLessInterface), |
| modTidyHandles: persistent.NewMap(uriLessInterface), |
| modVulnHandles: persistent.NewMap(uriLessInterface), |
| modWhyHandles: persistent.NewMap(uriLessInterface), |
| knownSubdirs: newKnownDirsSet(), |
| workspaceModFiles: wsModFiles, |
| workspaceModFilesErr: wsModFilesErr, |
| } |
| // Save one reference in the view. |
| v.releaseSnapshot = v.snapshot.Acquire() |
| |
| // Record the environment of the newly created view in the log. |
| event.Log(ctx, viewEnv(v)) |
| |
| // Initialize the view without blocking. |
| initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) |
| v.initCancelFirstAttempt = initCancel |
| snapshot := v.snapshot |
| |
| // Pass a second reference to the background goroutine. |
| bgRelease := snapshot.Acquire() |
| go func() { |
| defer bgRelease() |
| snapshot.initialize(initCtx, true) |
| }() |
| |
| // Return a third reference to the caller. |
| return v, snapshot, snapshot.Acquire(), nil |
| } |
| |
| // View returns a view with a matching name, if the session has one. |
| func (s *Session) View(name string) *View { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| for _, view := range s.views { |
| if view.Name() == name { |
| return view |
| } |
| } |
| return nil |
| } |
| |
| // ViewOf returns a view corresponding to the given URI. |
| // If the file is not already associated with a view, pick one using some heuristics. |
| func (s *Session) ViewOf(uri span.URI) (*View, error) { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| return s.viewOfLocked(uri) |
| } |
| |
| // Precondition: caller holds s.viewMu lock. |
| func (s *Session) viewOfLocked(uri span.URI) (*View, error) { |
| // Check if we already know this file. |
| if v, found := s.viewMap[uri]; found { |
| return v, nil |
| } |
| // Pick the best view for this file and memoize the result. |
| if len(s.views) == 0 { |
| return nil, fmt.Errorf("no views in session") |
| } |
| s.viewMap[uri] = bestViewForURI(uri, s.views) |
| return s.viewMap[uri], nil |
| } |
| |
| func (s *Session) Views() []*View { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| result := make([]*View, len(s.views)) |
| copy(result, s.views) |
| return result |
| } |
| |
| // bestViewForURI returns the most closely matching view for the given URI |
| // out of the given set of views. |
| func bestViewForURI(uri span.URI, views []*View) *View { |
| // we need to find the best view for this file |
| var longest *View |
| for _, view := range views { |
| if longest != nil && len(longest.Folder()) > len(view.Folder()) { |
| continue |
| } |
| // TODO(rfindley): this should consider the workspace layout (i.e. |
| // go.work). |
| if view.contains(uri) { |
| longest = view |
| } |
| } |
| if longest != nil { |
| return longest |
| } |
| // Try our best to return a view that knows the file. |
| for _, view := range views { |
| if view.knownFile(uri) { |
| return view |
| } |
| } |
| // TODO: are there any more heuristics we can use? |
| return views[0] |
| } |
| |
| // RemoveView removes the view v from the session |
| func (s *Session) RemoveView(view *View) { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| i := s.dropView(view) |
| if i == -1 { // error reported elsewhere |
| return |
| } |
| // delete this view... we don't care about order but we do want to make |
| // sure we can garbage collect the view |
| s.views = removeElement(s.views, i) |
| } |
| |
| // updateView recreates the view with the given options. |
| // |
| // If the resulting error is non-nil, the view may or may not have already been |
| // dropped from the session. |
| func (s *Session) updateView(ctx context.Context, view *View, options *source.Options) (*View, error) { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| |
| return s.updateViewLocked(ctx, view, options) |
| } |
| |
| func (s *Session) updateViewLocked(ctx context.Context, view *View, options *source.Options) (*View, error) { |
| // Preserve the snapshot ID if we are recreating the view. |
| view.snapshotMu.Lock() |
| if view.snapshot == nil { |
| view.snapshotMu.Unlock() |
| panic("updateView called after View was already shut down") |
| } |
| seqID := view.snapshot.sequenceID // Preserve sequence IDs when updating a view in place. |
| view.snapshotMu.Unlock() |
| |
| i := s.dropView(view) |
| if i == -1 { |
| return nil, fmt.Errorf("view %q not found", view.id) |
| } |
| |
| v, _, release, err := s.createView(ctx, view.name, view.folder, options, seqID) |
| release() |
| |
| if err != nil { |
| // we have dropped the old view, but could not create the new one |
| // this should not happen and is very bad, but we still need to clean |
| // up the view array if it happens |
| s.views = removeElement(s.views, i) |
| return nil, err |
| } |
| // substitute the new view into the array where the old view was |
| s.views[i] = v |
| return v, nil |
| } |
| |
| // removeElement removes the ith element from the slice replacing it with the last element. |
| // TODO(adonovan): generics, someday. |
| func removeElement(slice []*View, index int) []*View { |
| last := len(slice) - 1 |
| slice[index] = slice[last] |
| slice[last] = nil // aid GC |
| return slice[:last] |
| } |
| |
| // dropView removes v from the set of views for the receiver s and calls |
| // v.shutdown, returning the index of v in s.views (if found), or -1 if v was |
| // not found. s.viewMu must be held while calling this function. |
| func (s *Session) dropView(v *View) int { |
| // we always need to drop the view map |
| s.viewMap = make(map[span.URI]*View) |
| for i := range s.views { |
| if v == s.views[i] { |
| // we found the view, drop it and return the index it was found at |
| s.views[i] = nil |
| v.shutdown() |
| return i |
| } |
| } |
| // TODO(rfindley): it looks wrong that we don't shutdown v in this codepath. |
| // We should never get here. |
| bug.Reportf("tried to drop nonexistent view %q", v.id) |
| return -1 |
| } |
| |
| func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModification) error { |
| _, release, err := s.DidModifyFiles(ctx, changes) |
| release() |
| return err |
| } |
| |
| // TODO(rfindley): fileChange seems redundant with source.FileModification. |
| // De-dupe into a common representation for changes. |
| type fileChange struct { |
| content []byte |
| exists bool |
| fileHandle source.FileHandle |
| |
| // isUnchanged indicates whether the file action is one that does not |
| // change the actual contents of the file. Opens and closes should not |
| // be treated like other changes, since the file content doesn't change. |
| isUnchanged bool |
| } |
| |
| // DidModifyFiles reports a file modification to the session. It returns |
| // the new snapshots after the modifications have been applied, paired with |
| // the affected file URIs for those snapshots. |
| // On success, it returns a release function that |
| // must be called when the snapshots are no longer needed. |
| // |
| // TODO(rfindley): what happens if this function fails? It must leave us in a |
| // broken state, which we should surface to the user, probably as a request to |
| // restart gopls. |
| func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, func(), error) { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| |
| // Update overlays. |
| // |
| // TODO(rfindley): I think we do this while holding viewMu to prevent views |
| // from seeing the updated file content before they have processed |
| // invalidations, which could lead to a partial view of the changes (i.e. |
| // spurious diagnostics). However, any such view would immediately be |
| // invalidated here, so it is possible that we could update overlays before |
| // acquiring viewMu. |
| if err := s.updateOverlays(ctx, changes); err != nil { |
| return nil, nil, err |
| } |
| |
| // Re-create views whose definition may have changed. |
| // |
| // checkViews controls whether to re-evaluate view definitions when |
| // collecting views below. Any addition or deletion of a go.mod or go.work |
| // file may have affected the definition of the view. |
| checkViews := false |
| |
| for _, c := range changes { |
| if isGoMod(c.URI) || isGoWork(c.URI) { |
| // Change, InvalidateMetadata, and UnknownFileAction actions do not cause |
| // us to re-evaluate views. |
| redoViews := (c.Action != source.Change && |
| c.Action != source.InvalidateMetadata && |
| c.Action != source.UnknownFileAction) |
| |
| if redoViews { |
| checkViews = true |
| break |
| } |
| } |
| } |
| |
| if checkViews { |
| for _, view := range s.views { |
| // TODO(rfindley): can we avoid running the go command (go env) |
| // synchronously to change processing? Can we assume that the env did not |
| // change, and derive go.work using a combination of the configured |
| // GOWORK value and filesystem? |
| info, err := s.getWorkspaceInformation(ctx, view.folder, view.Options()) |
| if err != nil { |
| // Catastrophic failure, equivalent to a failure of session |
| // initialization and therefore should almost never happen. One |
| // scenario where this failure mode could occur is if some file |
| // permissions have changed preventing us from reading go.mod |
| // files. |
| // |
| // TODO(rfindley): consider surfacing this error more loudly. We |
| // could report a bug, but it's not really a bug. |
| event.Error(ctx, "fetching workspace information", err) |
| } |
| |
| if info != view.workspaceInformation { |
| _, err := s.updateViewLocked(ctx, view, view.Options()) |
| if err != nil { |
| // More catastrophic failure. The view may or may not still exist. |
| // The best we can do is log and move on. |
| event.Error(ctx, "recreating view", err) |
| } |
| } |
| } |
| } |
| |
| // Collect information about views affected by these changes. |
| views := make(map[*View]map[span.URI]*fileChange) |
| affectedViews := map[span.URI][]*View{} |
| // forceReloadMetadata records whether any change is the magic |
| // source.InvalidateMetadata action. |
| forceReloadMetadata := false |
| for _, c := range changes { |
| if c.Action == source.InvalidateMetadata { |
| forceReloadMetadata = true |
| } |
| // Build the list of affected views. |
| var changedViews []*View |
| for _, view := range s.views { |
| // Don't propagate changes that are outside of the view's scope |
| // or knowledge. |
| if !view.relevantChange(c) { |
| continue |
| } |
| changedViews = append(changedViews, view) |
| } |
| // If the change is not relevant to any view, but the change is |
| // happening in the editor, assign it the most closely matching view. |
| if len(changedViews) == 0 { |
| if c.OnDisk { |
| continue |
| } |
| bestView, err := s.viewOfLocked(c.URI) |
| if err != nil { |
| return nil, nil, err |
| } |
| changedViews = append(changedViews, bestView) |
| } |
| affectedViews[c.URI] = changedViews |
| |
| isUnchanged := c.Action == source.Open || c.Action == source.Close |
| |
| // Apply the changes to all affected views. |
| for _, view := range changedViews { |
| // Make sure that the file is added to the view's seenFiles set. |
| view.markKnown(c.URI) |
| if _, ok := views[view]; !ok { |
| views[view] = make(map[span.URI]*fileChange) |
| } |
| fh, err := s.ReadFile(ctx, c.URI) |
| if err != nil { |
| return nil, nil, err |
| } |
| content, err := fh.Content() |
| if err != nil { |
| // Ignore the error: the file may be deleted. |
| content = nil |
| } |
| views[view][c.URI] = &fileChange{ |
| content: content, |
| exists: err == nil, |
| fileHandle: fh, |
| isUnchanged: isUnchanged, |
| } |
| } |
| } |
| |
| var releases []func() |
| viewToSnapshot := map[*View]*snapshot{} |
| for view, changed := range views { |
| snapshot, release := view.invalidateContent(ctx, changed, forceReloadMetadata) |
| releases = append(releases, release) |
| viewToSnapshot[view] = snapshot |
| } |
| |
| // The release function is called when the |
| // returned URIs no longer need to be valid. |
| release := func() { |
| for _, release := range releases { |
| release() |
| } |
| } |
| |
| // We only want to diagnose each changed file once, in the view to which |
| // it "most" belongs. We do this by picking the best view for each URI, |
| // and then aggregating the set of snapshots and their URIs (to avoid |
| // diagnosing the same snapshot multiple times). |
| snapshotURIs := map[source.Snapshot][]span.URI{} |
| for _, mod := range changes { |
| viewSlice, ok := affectedViews[mod.URI] |
| if !ok || len(viewSlice) == 0 { |
| continue |
| } |
| view := bestViewForURI(mod.URI, viewSlice) |
| snapshot, ok := viewToSnapshot[view] |
| if !ok { |
| panic(fmt.Sprintf("no snapshot for view %s", view.Folder())) |
| } |
| snapshotURIs[snapshot] = append(snapshotURIs[snapshot], mod.URI) |
| } |
| |
| return snapshotURIs, release, nil |
| } |
| |
| // ExpandModificationsToDirectories returns the set of changes with the |
| // directory changes removed and expanded to include all of the files in |
| // the directory. |
| func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification { |
| var snapshots []*snapshot |
| s.viewMu.Lock() |
| for _, v := range s.views { |
| snapshot, release, err := v.getSnapshot() |
| if err != nil { |
| continue // view is shut down; continue with others |
| } |
| defer release() |
| snapshots = append(snapshots, snapshot) |
| } |
| s.viewMu.Unlock() |
| |
| knownDirs := knownDirectories(ctx, snapshots) |
| defer knownDirs.Destroy() |
| |
| var result []source.FileModification |
| for _, c := range changes { |
| if !knownDirs.Contains(c.URI) { |
| result = append(result, c) |
| continue |
| } |
| affectedFiles := knownFilesInDir(ctx, snapshots, c.URI) |
| var fileChanges []source.FileModification |
| for uri := range affectedFiles { |
| fileChanges = append(fileChanges, source.FileModification{ |
| URI: uri, |
| Action: c.Action, |
| LanguageID: "", |
| OnDisk: c.OnDisk, |
| // changes to directories cannot include text or versions |
| }) |
| } |
| result = append(result, fileChanges...) |
| } |
| return result |
| } |
| |
| // knownDirectories returns all of the directories known to the given |
| // snapshots, including workspace directories and their subdirectories. |
| // It is responsibility of the caller to destroy the returned set. |
| func knownDirectories(ctx context.Context, snapshots []*snapshot) knownDirsSet { |
| result := newKnownDirsSet() |
| for _, snapshot := range snapshots { |
| dirs := snapshot.dirs(ctx) |
| for _, dir := range dirs { |
| result.Insert(dir) |
| } |
| knownSubdirs := snapshot.getKnownSubdirs(dirs) |
| result.SetAll(knownSubdirs) |
| knownSubdirs.Destroy() |
| } |
| return result |
| } |
| |
| // knownFilesInDir returns the files known to the snapshots in the session. |
| // It does not respect symlinks. |
| func knownFilesInDir(ctx context.Context, snapshots []*snapshot, dir span.URI) map[span.URI]struct{} { |
| files := map[span.URI]struct{}{} |
| |
| for _, snapshot := range snapshots { |
| for _, uri := range snapshot.knownFilesInDir(ctx, dir) { |
| files[uri] = struct{}{} |
| } |
| } |
| return files |
| } |
| |
| // Precondition: caller holds s.viewMu lock. |
| // TODO(rfindley): move this to fs_overlay.go. |
| func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileModification) error { |
| fs.mu.Lock() |
| defer fs.mu.Unlock() |
| |
| for _, c := range changes { |
| // Don't update overlays for metadata invalidations. |
| if c.Action == source.InvalidateMetadata { |
| continue |
| } |
| |
| o, ok := fs.overlays[c.URI] |
| |
| // If the file is not opened in an overlay and the change is on disk, |
| // there's no need to update an overlay. If there is an overlay, we |
| // may need to update the overlay's saved value. |
| if !ok && c.OnDisk { |
| continue |
| } |
| |
| // Determine the file kind on open, otherwise, assume it has been cached. |
| var kind source.FileKind |
| switch c.Action { |
| case source.Open: |
| kind = source.FileKindForLang(c.LanguageID) |
| default: |
| if !ok { |
| return fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI) |
| } |
| kind = o.kind |
| } |
| |
| // Closing a file just deletes its overlay. |
| if c.Action == source.Close { |
| delete(fs.overlays, c.URI) |
| continue |
| } |
| |
| // If the file is on disk, check if its content is the same as in the |
| // overlay. Saves and on-disk file changes don't come with the file's |
| // content. |
| text := c.Text |
| if text == nil && (c.Action == source.Save || c.OnDisk) { |
| if !ok { |
| return fmt.Errorf("no known content for overlay for %s", c.Action) |
| } |
| text = o.content |
| } |
| // On-disk changes don't come with versions. |
| version := c.Version |
| if c.OnDisk || c.Action == source.Save { |
| version = o.version |
| } |
| hash := source.HashOf(text) |
| var sameContentOnDisk bool |
| switch c.Action { |
| case source.Delete: |
| // Do nothing. sameContentOnDisk should be false. |
| case source.Save: |
| // Make sure the version and content (if present) is the same. |
| if false && o.version != version { // Client no longer sends the version |
| return fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version) |
| } |
| if c.Text != nil && o.hash != hash { |
| return fmt.Errorf("updateOverlays: overlay %s changed on save", c.URI) |
| } |
| sameContentOnDisk = true |
| default: |
| fh, err := fs.delegate.ReadFile(ctx, c.URI) |
| if err != nil { |
| return err |
| } |
| _, readErr := fh.Content() |
| sameContentOnDisk = (readErr == nil && fh.FileIdentity().Hash == hash) |
| } |
| o = &Overlay{ |
| uri: c.URI, |
| version: version, |
| content: text, |
| kind: kind, |
| hash: hash, |
| saved: sameContentOnDisk, |
| } |
| |
| // NOTE: previous versions of this code checked here that the overlay had a |
| // view and file kind (but we don't know why). |
| |
| fs.overlays[c.URI] = o |
| } |
| |
| return nil |
| } |
| |
| // FileWatchingGlobPatterns returns glob patterns to watch every directory |
| // known by the view. For views within a module, this is the module root, |
| // any directory in the module root, and any replace targets. |
| func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { |
| s.viewMu.Lock() |
| defer s.viewMu.Unlock() |
| patterns := map[string]struct{}{} |
| for _, view := range s.views { |
| snapshot, release, err := view.getSnapshot() |
| if err != nil { |
| continue // view is shut down; continue with others |
| } |
| for k, v := range snapshot.fileWatchingGlobPatterns(ctx) { |
| patterns[k] = v |
| } |
| release() |
| } |
| return patterns |
| } |