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>
This commit is contained in:
Ian Cottrell 2019-11-08 13:25:29 -05:00
parent a3f652f180
commit c41a8f58b5
5 changed files with 92 additions and 22 deletions

View File

@ -80,9 +80,20 @@ func (s *session) Cache() source.Cache {
} }
func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) (source.View, error) { func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) (source.View, error) {
index := atomic.AddInt64(&viewIndex, 1)
s.viewMu.Lock() s.viewMu.Lock()
defer s.viewMu.Unlock() 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 // 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. // the spans need to be unrelated and no tag values should pollute it.
baseCtx := trace.Detach(xcontext.Detach(ctx)) baseCtx := trace.Detach(xcontext.Detach(ctx))
@ -141,10 +152,6 @@ func (s *session) NewView(ctx context.Context, name string, folder span.URI, opt
return nil, err 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)
debug.AddView(debugView{v}) debug.AddView(debugView{v})
return v, loadErr return v, loadErr
} }
@ -223,20 +230,51 @@ func (s *session) bestView(uri span.URI) source.View {
func (s *session) removeView(ctx context.Context, view *view) error { func (s *session) removeView(ctx context.Context, view *view) error {
s.viewMu.Lock() s.viewMu.Lock()
defer s.viewMu.Unlock() defer s.viewMu.Unlock()
// we always need to drop the view map i, err := s.dropView(ctx, view)
s.viewMap = make(map[span.URI]source.View) if err != nil {
for i, v := range s.views { return err
if view == v { }
// delete this view... we don't care about order but we do want to make // delete this view... we don't care about order but we do want to make
// sure we can garbage collect the view // sure we can garbage collect the view
s.views[i] = s.views[len(s.views)-1] s.views[i] = s.views[len(s.views)-1]
s.views[len(s.views)-1] = nil s.views[len(s.views)-1] = nil
s.views = s.views[:len(s.views)-1] s.views = s.views[:len(s.views)-1]
v.shutdown(ctx)
return nil 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 errors.Errorf("view %s for %v not found", view.Name(), view.Folder()) return -1, errors.Errorf("view %s for %v not found", view.Name(), view.Folder())
} }
// TODO: Propagate the language ID through to the view. // TODO: Propagate the language ID through to the view.

View File

@ -12,6 +12,7 @@ import (
"go/token" "go/token"
"os" "os"
"os/exec" "os/exec"
"reflect"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -103,8 +104,26 @@ func (v *view) Options() source.Options {
return v.options return v.options
} }
func (v *view) SetOptions(options source.Options) { func minorOptionsChange(a, b source.Options) bool {
// Check if any of the settings that modify our understanding of files have been changed
if !reflect.DeepEqual(a.Env, b.Env) {
return false
}
if !reflect.DeepEqual(a.BuildFlags, b.BuildFlags) {
return false
}
// the rest of the options are benign
return true
}
func (v *view) SetOptions(ctx context.Context, options source.Options) (source.View, error) {
// no need to rebuild the view if the options were not materially changed
if minorOptionsChange(v.options, options) {
v.options = options v.options = options
return v, nil
}
newView, err := v.session.updateView(ctx, v, options)
return newView, err
} }
// Config returns the configuration used for the view's interaction with the // Config returns the configuration used for the view's interaction with the

View File

@ -125,8 +125,12 @@ func (r *runner) callCompletion(t *testing.T, src span.Span, options source.Comp
modified := original modified := original
modified.InsertTextFormat = protocol.SnippetTextFormat modified.InsertTextFormat = protocol.SnippetTextFormat
modified.Completion = options modified.Completion = options
view.SetOptions(modified) view, err := view.SetOptions(r.ctx, modified)
defer view.SetOptions(original) if err != nil {
t.Error(err)
return nil
}
defer view.SetOptions(r.ctx, original)
list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{ list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{

View File

@ -104,7 +104,11 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) {
// Test all folding ranges. // Test all folding ranges.
modified.LineFoldingOnly = false modified.LineFoldingOnly = false
view.SetOptions(modified) view, err := view.SetOptions(r.ctx, modified)
if err != nil {
t.Error(err)
return
}
ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{
TextDocument: protocol.TextDocumentIdentifier{ TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(uri), URI: protocol.NewURI(uri),
@ -118,7 +122,11 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) {
// Test folding ranges with lineFoldingOnly = true. // Test folding ranges with lineFoldingOnly = true.
modified.LineFoldingOnly = true modified.LineFoldingOnly = true
view.SetOptions(modified) view, err = view.SetOptions(r.ctx, modified)
if err != nil {
t.Error(err)
return
}
ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{
TextDocument: protocol.TextDocumentIdentifier{ TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(uri), URI: protocol.NewURI(uri),
@ -129,7 +137,7 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) {
return return
} }
r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges) r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges)
view.SetOptions(original) view.SetOptions(r.ctx, original)
} }
func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) { func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) {

View File

@ -237,9 +237,10 @@ type View interface {
Options() Options Options() Options
// SetOptions sets the options of this view to new values. // SetOptions sets the options of this view to new values.
// Warning: Do not use this, unless in a test. // Calling this may cause the view to be invalidated and a replacement view
// This function does not correctly invalidate the view when needed. // added to the session. If so the new view will be returned, otherwise the
SetOptions(Options) // original one will be.
SetOptions(context.Context, Options) (View, error)
// CheckPackageHandles returns the CheckPackageHandles for the packages // CheckPackageHandles returns the CheckPackageHandles for the packages
// that this file belongs to. // that this file belongs to.