|  | // 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" | 
|  | "path/filepath" | 
|  | "sort" | 
|  | "strconv" | 
|  | "strings" | 
|  | "sync" | 
|  | "sync/atomic" | 
|  |  | 
|  | "golang.org/x/tools/internal/lsp/debug" | 
|  | "golang.org/x/tools/internal/lsp/protocol" | 
|  | "golang.org/x/tools/internal/lsp/source" | 
|  | "golang.org/x/tools/internal/lsp/telemetry" | 
|  | "golang.org/x/tools/internal/span" | 
|  | "golang.org/x/tools/internal/telemetry/log" | 
|  | "golang.org/x/tools/internal/telemetry/trace" | 
|  | "golang.org/x/tools/internal/xcontext" | 
|  | errors "golang.org/x/xerrors" | 
|  | ) | 
|  |  | 
|  | type session struct { | 
|  | cache *cache | 
|  | id    string | 
|  |  | 
|  | options source.Options | 
|  |  | 
|  | viewMu  sync.Mutex | 
|  | views   []*view | 
|  | viewMap map[span.URI]source.View | 
|  |  | 
|  | overlayMu sync.Mutex | 
|  | overlays  map[span.URI]*overlay | 
|  |  | 
|  | openFiles     sync.Map | 
|  | filesWatchMap *WatchMap | 
|  | } | 
|  |  | 
|  | type overlay struct { | 
|  | session *session | 
|  | uri     span.URI | 
|  | data    []byte | 
|  | hash    string | 
|  | kind    source.FileKind | 
|  |  | 
|  | // sameContentOnDisk is true if a file has been saved on disk, | 
|  | // and therefore does not need to be part of the overlay sent to go/packages. | 
|  | sameContentOnDisk bool | 
|  |  | 
|  | // unchanged is true if a file has not yet been edited. | 
|  | unchanged bool | 
|  | } | 
|  |  | 
|  | func (s *session) Options() source.Options { | 
|  | return s.options | 
|  | } | 
|  |  | 
|  | func (s *session) SetOptions(options source.Options) { | 
|  | s.options = options | 
|  | } | 
|  |  | 
|  | func (s *session) Shutdown(ctx context.Context) { | 
|  | s.viewMu.Lock() | 
|  | defer s.viewMu.Unlock() | 
|  | for _, view := range s.views { | 
|  | view.shutdown(ctx) | 
|  | } | 
|  | s.views = nil | 
|  | s.viewMap = nil | 
|  | debug.DropSession(debugSession{s}) | 
|  | } | 
|  |  | 
|  | func (s *session) Cache() source.Cache { | 
|  | return s.cache | 
|  | } | 
|  |  | 
|  | func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) source.View { | 
|  | index := atomic.AddInt64(&viewIndex, 1) | 
|  | s.viewMu.Lock() | 
|  | defer s.viewMu.Unlock() | 
|  | // 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 := trace.Detach(xcontext.Detach(ctx)) | 
|  | backgroundCtx, cancel := context.WithCancel(baseCtx) | 
|  | v := &view{ | 
|  | session:       s, | 
|  | id:            strconv.FormatInt(index, 10), | 
|  | options:       options, | 
|  | baseCtx:       baseCtx, | 
|  | backgroundCtx: backgroundCtx, | 
|  | cancel:        cancel, | 
|  | name:          name, | 
|  | folder:        folder, | 
|  | filesByURI:    make(map[span.URI]viewFile), | 
|  | filesByBase:   make(map[string][]viewFile), | 
|  | mcache: &metadataCache{ | 
|  | packages: make(map[packageID]*metadata), | 
|  | }, | 
|  | ignoredURIs: make(map[span.URI]struct{}), | 
|  | } | 
|  | // Preemptively build the builtin package, | 
|  | // so we immediately add builtin.go to the list of ignored files. | 
|  | v.buildBuiltinPkg(ctx) | 
|  |  | 
|  | s.views = append(s.views, v) | 
|  | // we always need to drop the view map | 
|  | s.viewMap = make(map[span.URI]source.View) | 
|  | debug.AddView(debugView{v}) | 
|  | return v | 
|  | } | 
|  |  | 
|  | // View returns the view by name. | 
|  | func (s *session) View(name string) source.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) source.View { | 
|  | s.viewMu.Lock() | 
|  | defer s.viewMu.Unlock() | 
|  |  | 
|  | // Check if we already know this file. | 
|  | if v, found := s.viewMap[uri]; found { | 
|  | return v | 
|  | } | 
|  | // Pick the best view for this file and memoize the result. | 
|  | v := s.bestView(uri) | 
|  | s.viewMap[uri] = v | 
|  | return v | 
|  | } | 
|  |  | 
|  | func (s *session) Views() []source.View { | 
|  | s.viewMu.Lock() | 
|  | defer s.viewMu.Unlock() | 
|  | result := make([]source.View, len(s.views)) | 
|  | for i, v := range s.views { | 
|  | result[i] = v | 
|  | } | 
|  | return result | 
|  | } | 
|  |  | 
|  | // bestView finds the best view to associate a given URI with. | 
|  | // viewMu must be held when calling this method. | 
|  | func (s *session) bestView(uri span.URI) source.View { | 
|  | // we need to find the best view for this file | 
|  | var longest source.View | 
|  | for _, view := range s.views { | 
|  | if longest != nil && len(longest.Folder()) > len(view.Folder()) { | 
|  | continue | 
|  | } | 
|  | if strings.HasPrefix(string(uri), string(view.Folder())) { | 
|  | longest = view | 
|  | } | 
|  | } | 
|  | if longest != nil { | 
|  | return longest | 
|  | } | 
|  | // TODO: are there any more heuristics we can use? | 
|  | return s.views[0] | 
|  | } | 
|  |  | 
|  | func (s *session) removeView(ctx context.Context, view *view) error { | 
|  | s.viewMu.Lock() | 
|  | defer s.viewMu.Unlock() | 
|  | // we always need to drop the view map | 
|  | s.viewMap = make(map[span.URI]source.View) | 
|  | for i, v := range s.views { | 
|  | if view == v { | 
|  | // delete this view... we don't care about order but we do want to make | 
|  | // sure we can garbage collect the view | 
|  | s.views[i] = s.views[len(s.views)-1] | 
|  | s.views[len(s.views)-1] = nil | 
|  | s.views = s.views[:len(s.views)-1] | 
|  | v.shutdown(ctx) | 
|  | return nil | 
|  | } | 
|  | } | 
|  | return errors.Errorf("view %s for %v not found", view.Name(), view.Folder()) | 
|  | } | 
|  |  | 
|  | // TODO: Propagate the language ID through to the view. | 
|  | func (s *session) DidOpen(ctx context.Context, uri span.URI, _ source.FileKind, text []byte) { | 
|  | ctx = telemetry.File.With(ctx, uri) | 
|  |  | 
|  | // Files with _ prefixes are ignored. | 
|  | if strings.HasPrefix(filepath.Base(uri.Filename()), "_") { | 
|  | for _, view := range s.views { | 
|  | view.ignoredURIsMu.Lock() | 
|  | view.ignoredURIs[uri] = struct{}{} | 
|  | view.ignoredURIsMu.Unlock() | 
|  | } | 
|  | return | 
|  | } | 
|  |  | 
|  | // Mark the file as open. | 
|  | s.openFiles.Store(uri, true) | 
|  |  | 
|  | // Read the file on disk and compare it to the text provided. | 
|  | // If it is the same as on disk, we can avoid sending it as an overlay to go/packages. | 
|  | s.openOverlay(ctx, uri, text) | 
|  |  | 
|  | // Mark the file as just opened so that we know to re-run packages.Load on it. | 
|  | // We do this because we may not be aware of all of the packages the file belongs to. | 
|  | // A file may be in multiple views. | 
|  | for _, view := range s.views { | 
|  | if strings.HasPrefix(string(uri), string(view.Folder())) { | 
|  | f, err := view.GetFile(ctx, uri) | 
|  | if err != nil { | 
|  | log.Error(ctx, "error getting file", nil, telemetry.File) | 
|  | return | 
|  | } | 
|  | gof, ok := f.(*goFile) | 
|  | if !ok { | 
|  | log.Error(ctx, "not a Go file", nil, telemetry.File) | 
|  | return | 
|  | } | 
|  | // Mark file as open. | 
|  | gof.mu.Lock() | 
|  | gof.justOpened = true | 
|  | gof.mu.Unlock() | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (s *session) DidSave(uri span.URI) { | 
|  | s.overlayMu.Lock() | 
|  | defer s.overlayMu.Unlock() | 
|  |  | 
|  | if overlay, ok := s.overlays[uri]; ok { | 
|  | overlay.sameContentOnDisk = true | 
|  | } | 
|  | } | 
|  |  | 
|  | func (s *session) DidClose(uri span.URI) { | 
|  | s.openFiles.Delete(uri) | 
|  | } | 
|  |  | 
|  | func (s *session) IsOpen(uri span.URI) bool { | 
|  | _, open := s.openFiles.Load(uri) | 
|  | return open | 
|  | } | 
|  |  | 
|  | func (s *session) GetFile(uri span.URI) source.FileHandle { | 
|  | if overlay := s.readOverlay(uri); overlay != nil { | 
|  | return overlay | 
|  | } | 
|  | // Fall back to the cache-level file system. | 
|  | return s.Cache().GetFile(uri) | 
|  | } | 
|  |  | 
|  | func (s *session) SetOverlay(uri span.URI, data []byte) bool { | 
|  | s.overlayMu.Lock() | 
|  | defer func() { | 
|  | s.overlayMu.Unlock() | 
|  | s.filesWatchMap.Notify(uri) | 
|  | }() | 
|  |  | 
|  | if data == nil { | 
|  | delete(s.overlays, uri) | 
|  | return false | 
|  | } | 
|  |  | 
|  | o := s.overlays[uri] | 
|  | firstChange := o != nil && o.unchanged | 
|  |  | 
|  | s.overlays[uri] = &overlay{ | 
|  | session:   s, | 
|  | uri:       uri, | 
|  | data:      data, | 
|  | hash:      hashContents(data), | 
|  | unchanged: o == nil, | 
|  | } | 
|  | return firstChange | 
|  | } | 
|  |  | 
|  | // openOverlay adds the file content to the overlay. | 
|  | // It also checks if the provided content is equivalent to the file's content on disk. | 
|  | func (s *session) openOverlay(ctx context.Context, uri span.URI, data []byte) { | 
|  | s.overlayMu.Lock() | 
|  | defer func() { | 
|  | s.overlayMu.Unlock() | 
|  | s.filesWatchMap.Notify(uri) | 
|  | }() | 
|  | s.overlays[uri] = &overlay{ | 
|  | session:   s, | 
|  | uri:       uri, | 
|  | data:      data, | 
|  | hash:      hashContents(data), | 
|  | unchanged: true, | 
|  | } | 
|  | _, hash, err := s.cache.GetFile(uri).Read(ctx) | 
|  | if err != nil { | 
|  | log.Error(ctx, "failed to read", err, telemetry.File) | 
|  | return | 
|  | } | 
|  | if hash == s.overlays[uri].hash { | 
|  | s.overlays[uri].sameContentOnDisk = true | 
|  | } | 
|  | } | 
|  |  | 
|  | func (s *session) readOverlay(uri span.URI) *overlay { | 
|  | s.overlayMu.Lock() | 
|  | defer s.overlayMu.Unlock() | 
|  |  | 
|  | // We might have the content saved in an overlay. | 
|  | if overlay, ok := s.overlays[uri]; ok { | 
|  | return overlay | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (s *session) buildOverlay() map[string][]byte { | 
|  | s.overlayMu.Lock() | 
|  | defer s.overlayMu.Unlock() | 
|  |  | 
|  | overlays := make(map[string][]byte) | 
|  | for uri, overlay := range s.overlays { | 
|  | if overlay.sameContentOnDisk { | 
|  | continue | 
|  | } | 
|  | overlays[uri.Filename()] = overlay.data | 
|  | } | 
|  | return overlays | 
|  | } | 
|  |  | 
|  | func (s *session) DidChangeOutOfBand(ctx context.Context, f source.GoFile, changeType protocol.FileChangeType) { | 
|  | if changeType == protocol.Deleted { | 
|  | // After a deletion we must invalidate the package's metadata to | 
|  | // force a go/packages invocation to refresh the package's file | 
|  | // list. | 
|  | f.(*goFile).invalidateMeta(ctx) | 
|  | } | 
|  |  | 
|  | s.filesWatchMap.Notify(f.URI()) | 
|  | } | 
|  |  | 
|  | func (o *overlay) FileSystem() source.FileSystem { | 
|  | return o.session | 
|  | } | 
|  |  | 
|  | func (o *overlay) Identity() source.FileIdentity { | 
|  | return source.FileIdentity{ | 
|  | URI:     o.uri, | 
|  | Version: o.hash, | 
|  | } | 
|  | } | 
|  |  | 
|  | func (o *overlay) Kind() source.FileKind { | 
|  | // TODO: Determine the file kind using textDocument.languageId. | 
|  | return source.Go | 
|  | } | 
|  |  | 
|  | func (o *overlay) Read(ctx context.Context) ([]byte, string, error) { | 
|  | return o.data, o.hash, nil | 
|  | } | 
|  |  | 
|  | type debugSession struct{ *session } | 
|  |  | 
|  | func (s debugSession) ID() string         { return s.id } | 
|  | func (s debugSession) Cache() debug.Cache { return debugCache{s.cache} } | 
|  | func (s debugSession) Files() []*debug.File { | 
|  | var files []*debug.File | 
|  | seen := make(map[span.URI]*debug.File) | 
|  | s.openFiles.Range(func(key interface{}, value interface{}) bool { | 
|  | uri, ok := key.(span.URI) | 
|  | if ok { | 
|  | f := &debug.File{Session: s, URI: uri} | 
|  | seen[uri] = f | 
|  | files = append(files, f) | 
|  | } | 
|  | return true | 
|  | }) | 
|  | s.overlayMu.Lock() | 
|  | defer s.overlayMu.Unlock() | 
|  | for _, overlay := range s.overlays { | 
|  | f, ok := seen[overlay.uri] | 
|  | if !ok { | 
|  | f = &debug.File{Session: s, URI: overlay.uri} | 
|  | seen[overlay.uri] = f | 
|  | files = append(files, f) | 
|  | } | 
|  | f.Data = string(overlay.data) | 
|  | f.Error = nil | 
|  | f.Hash = overlay.hash | 
|  | } | 
|  | sort.Slice(files, func(i int, j int) bool { | 
|  | return files[i].URI < files[j].URI | 
|  | }) | 
|  | return files | 
|  | } | 
|  |  | 
|  | func (s debugSession) File(hash string) *debug.File { | 
|  | s.overlayMu.Lock() | 
|  | defer s.overlayMu.Unlock() | 
|  | for _, overlay := range s.overlays { | 
|  | if overlay.hash == hash { | 
|  | return &debug.File{ | 
|  | Session: s, | 
|  | URI:     overlay.uri, | 
|  | Data:    string(overlay.data), | 
|  | Error:   nil, | 
|  | Hash:    overlay.hash, | 
|  | } | 
|  | } | 
|  | } | 
|  | return &debug.File{ | 
|  | Session: s, | 
|  | Hash:    hash, | 
|  | } | 
|  | } |