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>
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 573ebc8..2174df4 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -80,9 +80,20 @@
}
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()
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))
@@ -141,10 +152,6 @@
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})
return v, loadErr
}
@@ -223,20 +230,51 @@
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 {
- // 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]
+ // we found the view, drop it and return the index it was found at
+ s.views[i] = nil
v.shutdown(ctx)
- return nil
+ 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.
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 1488a36..5bd8d10 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -12,6 +12,7 @@
"go/token"
"os"
"os/exec"
+ "reflect"
"strings"
"sync"
"time"
@@ -103,8 +104,26 @@
return v.options
}
-func (v *view) SetOptions(options source.Options) {
- v.options = 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
+ 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
diff --git a/internal/lsp/completion_test.go b/internal/lsp/completion_test.go
index 7c20ce1..0ee2e06 100644
--- a/internal/lsp/completion_test.go
+++ b/internal/lsp/completion_test.go
@@ -125,8 +125,12 @@
modified := original
modified.InsertTextFormat = protocol.SnippetTextFormat
modified.Completion = options
- view.SetOptions(modified)
- defer view.SetOptions(original)
+ view, err := view.SetOptions(r.ctx, modified)
+ if err != nil {
+ t.Error(err)
+ return nil
+ }
+ defer view.SetOptions(r.ctx, original)
list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 225f943..8e2615b 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -104,7 +104,11 @@
// Test all folding ranges.
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{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(uri),
@@ -118,7 +122,11 @@
// Test folding ranges with 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{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(uri),
@@ -129,7 +137,7 @@
return
}
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) {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index dcbd1b8..1f3f18d 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -237,9 +237,10 @@
Options() Options
// SetOptions sets the options of this view to new values.
- // Warning: Do not use this, unless in a test.
- // This function does not correctly invalidate the view when needed.
- SetOptions(Options)
+ // Calling this may cause the view to be invalidated and a replacement view
+ // added to the session. If so the new view will be returned, otherwise the
+ // original one will be.
+ SetOptions(context.Context, Options) (View, error)
// CheckPackageHandles returns the CheckPackageHandles for the packages
// that this file belongs to.