go/internal/lsp/cache/session.go
Ian Cottrell c41a8f58b5 internal/lsp: make View.SetOptions save and useful
It attempts to detect changes that would invalidate the view and replace itself
with a new view when that happens

Change-Id: I0f1a8cd3bd6ddcef115fedc6c57ae0398b16d3b6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/206147
Run-TryBot: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-11-12 16:15:35 +00:00

490 lines
12 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
"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/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, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
v, err := s.createView(ctx, name, folder, options)
if err != nil {
return nil, err
}
s.views = append(s.views, v)
// we always need to drop the view map
s.viewMap = make(map[span.URI]source.View)
return v, nil
}
func (s *session) createView(ctx context.Context, name string, folder span.URI, options source.Options) (*view, error) {
index := atomic.AddInt64(&viewIndex, 1)
// 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),
snapshot: &snapshot{
packages: make(map[packageKey]*checkPackageHandle),
ids: make(map[span.URI][]packageID),
metadata: make(map[packageID]*metadata),
files: make(map[span.URI]source.FileHandle),
importedBy: make(map[packageID][]packageID),
actions: make(map[actionKey]*actionHandle),
},
ignoredURIs: make(map[span.URI]struct{}),
builtin: &builtinPkg{},
}
v.snapshot.view = v
if v.session.cache.options != nil {
v.session.cache.options(&v.options)
}
// Preemptively build the builtin package,
// so we immediately add builtin.go to the list of ignored files.
v.buildBuiltinPackage(ctx)
// Preemptively load everything in this directory.
// TODO(matloob): Determine if this can be done in parallel with something else.
// Perhaps different calls to NewView can be run in parallel?
// TODO(matloob): By default when a new file is opened, its data is invalidated
// and it's loaded again. Determine if the redundant reload can be avoided.
v.snapshotMu.Lock()
defer v.snapshotMu.Unlock() // The code after the snapshot is used isn't expensive.
m, err := v.snapshot.load(ctx, source.DirectoryURI(folder))
var loadErr error
if err != nil && err != errNoPackagesFound {
loadErr = fmt.Errorf("Error loading packages: %v", err)
}
// Prepare CheckPackageHandles for every package that's been loaded.
// (*snapshot).CheckPackageHandle makes the assumption that every package that's
// been loaded has an existing checkPackageHandle.
for _, m := range m {
_, err := v.snapshot.checkPackageHandle(ctx, m.id, source.ParseFull)
if err != nil {
return nil, err
}
}
debug.AddView(debugView{v})
return v, loadErr
}
// 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) viewsOf(uri span.URI) []*view {
s.viewMu.Lock()
defer s.viewMu.Unlock()
var views []*view
for _, view := range s.views {
if strings.HasPrefix(string(uri), string(view.Folder())) {
views = append(views, view)
}
}
return views
}
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()
i, err := s.dropView(ctx, view)
if err != nil {
return err
}
// 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]
return nil
}
func (s *session) updateView(ctx context.Context, view *view, options source.Options) (*view, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
i, err := s.dropView(ctx, view)
if err != nil {
return nil, err
}
v, err := s.createView(ctx, view.name, view.folder, options)
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[i] = s.views[len(s.views)-1]
s.views[len(s.views)-1] = nil
s.views = s.views[:len(s.views)-1]
}
// substitute the new view into the array where the old view was
s.views[i] = v
return v, nil
}
func (s *session) dropView(ctx context.Context, view *view) (int, error) {
// 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 {
// we found the view, drop it and return the index it was found at
s.views[i] = nil
v.shutdown(ctx)
return i, nil
}
}
return -1, 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, kind source.FileKind, text []byte) error {
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 nil
}
// Make sure that the file gets added to the session's file watch map.
view := s.bestView(uri)
if _, err := view.GetFile(ctx, uri); err != nil {
return err
}
// 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, kind, text)
return nil
}
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, kind source.FileKind) source.FileHandle {
if overlay := s.readOverlay(uri); overlay != nil {
return overlay
}
// Fall back to the cache-level file system.
return s.cache.GetFile(uri, kind)
}
func (s *session) SetOverlay(uri span.URI, kind source.FileKind, data []byte) bool {
s.overlayMu.Lock()
defer func() {
s.overlayMu.Unlock()
s.filesWatchMap.Notify(uri, protocol.Changed)
}()
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,
kind: kind,
data: data,
hash: hashContents(data),
unchanged: o == nil,
}
return firstChange
}
func (s *session) clearOverlay(uri span.URI) {
s.overlayMu.Lock()
defer s.overlayMu.Unlock()
delete(s.overlays, uri)
}
// 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, kind source.FileKind, data []byte) {
s.overlayMu.Lock()
defer func() {
s.overlayMu.Unlock()
s.filesWatchMap.Notify(uri, protocol.Created)
}()
s.overlays[uri] = &overlay{
session: s,
uri: uri,
kind: kind,
data: data,
hash: hashContents(data),
unchanged: true,
}
// If the file is on disk, check if its content is the same as the overlay.
if _, hash, err := s.cache.GetFile(uri, kind).Read(ctx); err == nil {
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, uri span.URI, changeType protocol.FileChangeType) bool {
return s.filesWatchMap.Notify(uri, changeType)
}
func (o *overlay) FileSystem() source.FileSystem {
return o.session
}
func (o *overlay) Identity() source.FileIdentity {
return source.FileIdentity{
URI: o.uri,
Version: o.hash,
Kind: o.kind,
}
}
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,
}
}