mirror of
https://github.com/golang/go
synced 2024-11-19 01:44:40 -07:00
f1f4a3381f
This cl is the first in a set that change the configuration behaviour. This one should have no behaviour differences, but makes a lot of preparatory changes. The same options are set to the same values in the same places. The options are now stored on the Session instead of the Server The View supports options, but does not have any yet. Change-Id: Ie966cceca6878861686a1766d63bb8a78021259b Reviewed-on: https://go-review.googlesource.com/c/tools/+/193726 Run-TryBot: Ian Cottrell <iancottrell@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
418 lines
10 KiB
Go
418 lines
10 KiB
Go
// 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"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
|
||
"golang.org/x/tools/internal/lsp/debug"
|
||
"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.SessionOptions
|
||
|
||
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.SessionOptions {
|
||
return s.options
|
||
}
|
||
|
||
func (s *session) SetOptions(options source.SessionOptions) {
|
||
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) source.View {
|
||
index := atomic.AddInt64(&viewIndex, 1)
|
||
s.viewMu.Lock()
|
||
defer s.viewMu.Unlock()
|
||
// We want a true background context and not a detatched 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),
|
||
baseCtx: baseCtx,
|
||
backgroundCtx: backgroundCtx,
|
||
cancel: cancel,
|
||
name: name,
|
||
env: os.Environ(),
|
||
folder: folder,
|
||
filesByURI: make(map[span.URI]viewFile),
|
||
filesByBase: make(map[string][]viewFile),
|
||
mcache: &metadataCache{
|
||
packages: make(map[packageID]*metadata),
|
||
ids: make(map[packagePath]packageID),
|
||
},
|
||
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(uri span.URI) {
|
||
s.filesWatchMap.Notify(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,
|
||
}
|
||
}
|