internal/lsp: add structured layers to the cache

This is primarily to separate the levels because they have different cache
lifetimes and sharability.
This will allow us to share results between views and even between servers.

Change-Id: I280ca19d17a6ea8a15e48637d4445e2b6cf04769
Reviewed-on: https://go-review.googlesource.com/c/tools/+/177518
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/cmd/gopls/main.go b/cmd/gopls/main.go
index 7b708ab..26d32d0 100644
--- a/cmd/gopls/main.go
+++ b/cmd/gopls/main.go
@@ -17,5 +17,5 @@
 )
 
 func main() {
-	tool.Main(context.Background(), &cmd.Application{}, os.Args[1:])
+	tool.Main(context.Background(), cmd.New(nil), os.Args[1:])
 }
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
new file mode 100644
index 0000000..71db06b
--- /dev/null
+++ b/internal/lsp/cache/cache.go
@@ -0,0 +1,24 @@
+// 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 (
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/lsp/xlog"
+)
+
+func New() source.Cache {
+	return &cache{}
+}
+
+type cache struct {
+}
+
+func (c *cache) NewSession(log xlog.Logger) source.Session {
+	return &session{
+		cache: c,
+		log:   log,
+	}
+}
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 90eb5a5..b2d1252 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -138,7 +138,7 @@
 		if f, _ := v.getFile(span.FileURI(filename)); f != nil {
 			gof, ok := f.(*goFile)
 			if !ok {
-				v.Logger().Errorf(ctx, "not a go file: %v", f.URI())
+				v.Session().Logger().Errorf(ctx, "not a go file: %v", f.URI())
 				continue
 			}
 			gof.meta = m
@@ -270,23 +270,23 @@
 	for _, file := range pkg.GetSyntax() {
 		// TODO: If a file is in multiple packages, which package do we store?
 		if !file.Pos().IsValid() {
-			v.Logger().Errorf(ctx, "invalid position for file %v", file.Name)
+			v.Session().Logger().Errorf(ctx, "invalid position for file %v", file.Name)
 			continue
 		}
 		tok := v.config.Fset.File(file.Pos())
 		if tok == nil {
-			v.Logger().Errorf(ctx, "no token.File for %v", file.Name)
+			v.Session().Logger().Errorf(ctx, "no token.File for %v", file.Name)
 			continue
 		}
 		fURI := span.FileURI(tok.Name())
 		f, err := v.getFile(fURI)
 		if err != nil {
-			v.Logger().Errorf(ctx, "no file: %v", err)
+			v.Session().Logger().Errorf(ctx, "no file: %v", err)
 			continue
 		}
 		gof, ok := f.(*goFile)
 		if !ok {
-			v.Logger().Errorf(ctx, "not a go file: %v", f.URI())
+			v.Session().Logger().Errorf(ctx, "not a go file: %v", f.URI())
 			continue
 		}
 		gof.token = tok
diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go
index 3711e96..9aecfa4 100644
--- a/internal/lsp/cache/file.go
+++ b/internal/lsp/cache/file.go
@@ -147,7 +147,7 @@
 	// We don't know the content yet, so read it.
 	content, err := ioutil.ReadFile(f.filename())
 	if err != nil {
-		f.view.Logger().Errorf(ctx, "unable to read file %s: %v", f.filename(), err)
+		f.view.Session().Logger().Errorf(ctx, "unable to read file %s: %v", f.filename(), err)
 		return
 	}
 	f.content = content
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index b8264b5..c9876ba 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -152,7 +152,7 @@
 		switch n := n.(type) {
 		case *ast.BadStmt:
 			if err := v.parseDeferOrGoStmt(n, parent, tok, src); err != nil {
-				v.log.Debugf(ctx, "unable to parse defer or go from *ast.BadStmt: %v", err)
+				v.Session().Logger().Debugf(ctx, "unable to parse defer or go from *ast.BadStmt: %v", err)
 			}
 			return false
 		default:
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
new file mode 100644
index 0000000..7335b61
--- /dev/null
+++ b/internal/lsp/cache/session.go
@@ -0,0 +1,153 @@
+// 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"
+	"strings"
+	"sync"
+
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/lsp/xlog"
+	"golang.org/x/tools/internal/span"
+)
+
+type session struct {
+	cache *cache
+	// the logger to use to communicate back with the client
+	log xlog.Logger
+
+	viewMu  sync.Mutex
+	views   []*view
+	viewMap map[span.URI]source.View
+}
+
+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
+}
+
+func (s *session) Cache() source.Cache {
+	return s.cache
+}
+
+func (s *session) NewView(name string, folder span.URI, config *packages.Config) source.View {
+	s.viewMu.Lock()
+	defer s.viewMu.Unlock()
+	ctx := context.Background()
+	backgroundCtx, cancel := context.WithCancel(ctx)
+	v := &view{
+		session:        s,
+		baseCtx:        ctx,
+		backgroundCtx:  backgroundCtx,
+		builtinPkg:     builtinPkg(*config),
+		cancel:         cancel,
+		config:         *config,
+		name:           name,
+		folder:         folder,
+		filesByURI:     make(map[span.URI]viewFile),
+		filesByBase:    make(map[string][]viewFile),
+		contentChanges: make(map[span.URI]func()),
+		mcache: &metadataCache{
+			packages: make(map[string]*metadata),
+		},
+		pcache: &packageCache{
+			packages: make(map[string]*entry),
+		},
+	}
+	s.views = append(s.views, v)
+	// we always need to drop the view map
+	s.viewMap = make(map[span.URI]source.View)
+	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 fmt.Errorf("view %s for %v not found", view.Name(), view.Folder())
+}
+
+func (s *session) Logger() xlog.Logger {
+	return s.log
+}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 15c054e..cf4227c 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -16,11 +16,12 @@
 
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/lsp/source"
-	"golang.org/x/tools/internal/lsp/xlog"
 	"golang.org/x/tools/internal/span"
 )
 
 type view struct {
+	session *session
+
 	// mu protects all mutable state of the view.
 	mu sync.Mutex
 
@@ -36,9 +37,6 @@
 	// should be stopped.
 	cancel context.CancelFunc
 
-	// the logger to use to communicate back with the client
-	log xlog.Logger
-
 	// Name is the user visible name of this view.
 	name string
 
@@ -94,28 +92,8 @@
 	ready chan struct{} // closed to broadcast ready condition
 }
 
-func NewView(ctx context.Context, log xlog.Logger, name string, folder span.URI, config *packages.Config) source.View {
-	backgroundCtx, cancel := context.WithCancel(ctx)
-	v := &view{
-		baseCtx:        ctx,
-		backgroundCtx:  backgroundCtx,
-		builtinPkg:     builtinPkg(*config),
-		cancel:         cancel,
-		log:            log,
-		config:         *config,
-		name:           name,
-		folder:         folder,
-		filesByURI:     make(map[span.URI]viewFile),
-		filesByBase:    make(map[string][]viewFile),
-		contentChanges: make(map[span.URI]func()),
-		mcache: &metadataCache{
-			packages: make(map[string]*metadata),
-		},
-		pcache: &packageCache{
-			packages: make(map[string]*entry),
-		},
-	}
-	return v
+func (v *view) Session() source.Session {
+	return v.session
 }
 
 // Name returns the user visible name of this view.
@@ -138,7 +116,11 @@
 	v.config.Env = env
 }
 
-func (v *view) Shutdown(context.Context) {
+func (v *view) Shutdown(ctx context.Context) {
+	v.session.removeView(ctx, v)
+}
+
+func (v *view) shutdown(context.Context) {
 	v.mu.Lock()
 	defer v.mu.Unlock()
 	if v.cancel != nil {
@@ -393,7 +375,3 @@
 		v.filesByBase[basename] = append(v.filesByBase[basename], f)
 	}
 }
-
-func (v *view) Logger() xlog.Logger {
-	return v.log
-}
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 8be04fd..00ccd02 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -24,7 +24,9 @@
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp"
+	"golang.org/x/tools/internal/lsp/cache"
 	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/tool"
 )
@@ -45,6 +47,9 @@
 	// An initial, common go/packages configuration
 	Config packages.Config
 
+	// The base cache to use for sessions from this application.
+	Cache source.Cache
+
 	// Support for remote lsp server
 	Remote string `flag:"remote" help:"*EXPERIMENTAL* - forward all commands to a remote lsp"`
 
@@ -52,6 +57,17 @@
 	Verbose bool `flag:"v" help:"Verbose output"`
 }
 
+// Returns a new Application ready to run.
+func New(config *packages.Config) *Application {
+	app := &Application{
+		Cache: cache.New(),
+	}
+	if config != nil {
+		app.Config = *config
+	}
+	return app
+}
+
 // Name implements tool.Application returning the binary name.
 func (app *Application) Name() string { return "gopls" }
 
@@ -135,7 +151,7 @@
 	switch app.Remote {
 	case "":
 		connection := newConnection(app)
-		connection.Server = lsp.NewClientServer(connection.Client)
+		connection.Server = lsp.NewClientServer(app.Cache, connection.Client)
 		return connection, connection.initialize(ctx)
 	case "internal":
 		internalMu.Lock()
@@ -150,7 +166,7 @@
 		var jc *jsonrpc2.Conn
 		jc, connection.Server, _ = protocol.NewClient(jsonrpc2.NewHeaderStream(cr, cw), connection.Client)
 		go jc.Run(ctx)
-		go lsp.NewServer(jsonrpc2.NewHeaderStream(sr, sw)).Run(ctx)
+		go lsp.NewServer(app.Cache, jsonrpc2.NewHeaderStream(sr, sw)).Run(ctx)
 		if err := connection.initialize(ctx); err != nil {
 			return nil, err
 		}
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index 316b46e..5b8b4c5 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -37,9 +37,7 @@
 	r := &runner{
 		exporter: exporter,
 		data:     data,
-		app: &cmd.Application{
-			Config: *data.Exported.Config,
-		},
+		app:      cmd.New(data.Exported.Config),
 	}
 	tests.Run(t, r, data)
 }
diff --git a/internal/lsp/cmd/definition_test.go b/internal/lsp/cmd/definition_test.go
index 0f298c4..033c418 100644
--- a/internal/lsp/cmd/definition_test.go
+++ b/internal/lsp/cmd/definition_test.go
@@ -54,7 +54,7 @@
 		fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)} {
 		args := append(baseArgs, query)
 		got := captureStdOut(t, func() {
-			tool.Main(context.Background(), &cmd.Application{}, args)
+			tool.Main(context.Background(), cmd.New(nil), args)
 		})
 		if !expect.MatchString(got) {
 			t.Errorf("test with %v\nexpected:\n%s\ngot:\n%s", args, expect, got)
diff --git a/internal/lsp/cmd/format_test.go b/internal/lsp/cmd/format_test.go
index 5674fa4..981ebdb 100644
--- a/internal/lsp/cmd/format_test.go
+++ b/internal/lsp/cmd/format_test.go
@@ -41,7 +41,7 @@
 				//TODO: our error handling differs, for now just skip unformattable files
 				continue
 			}
-			app := &cmd.Application{}
+			app := cmd.New(nil)
 			app.Config = r.data.Config
 			got := captureStdOut(t, func() {
 				tool.Main(context.Background(), app, append([]string{"-remote=internal", "format"}, args...))
diff --git a/internal/lsp/cmd/serve.go b/internal/lsp/cmd/serve.go
index 2cc1de2..ddac356 100644
--- a/internal/lsp/cmd/serve.go
+++ b/internal/lsp/cmd/serve.go
@@ -79,13 +79,13 @@
 		go srv.Conn.Run(ctx)
 	}
 	if s.Address != "" {
-		return lsp.RunServerOnAddress(ctx, s.Address, run)
+		return lsp.RunServerOnAddress(ctx, s.app.Cache, s.Address, run)
 	}
 	if s.Port != 0 {
-		return lsp.RunServerOnPort(ctx, s.Port, run)
+		return lsp.RunServerOnPort(ctx, s.app.Cache, s.Port, run)
 	}
 	stream := jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout)
-	srv := lsp.NewServer(stream)
+	srv := lsp.NewServer(s.app.Cache, stream)
 	srv.Conn.Logger = logger(s.Trace, out)
 	return srv.Conn.Run(ctx)
 }
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 7518e9a..2b7e1cc 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -16,7 +16,7 @@
 
 func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	_, m, err := getSourceFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index b9d82f4..7183462 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -17,7 +17,7 @@
 
 func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
@@ -32,19 +32,19 @@
 	}
 	items, prefix, err := source.Completion(ctx, f, rng.Start)
 	if err != nil {
-		s.log.Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
+		s.session.Logger().Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
 	}
 	// We might need to adjust the position to account for the prefix.
 	pos := params.Position
 	if prefix.Pos().IsValid() {
 		spn, err := span.NewRange(view.FileSet(), prefix.Pos(), 0).Span()
 		if err != nil {
-			s.log.Infof(ctx, "failed to get span for prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
+			s.session.Logger().Infof(ctx, "failed to get span for prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
 		}
 		if prefixPos, err := m.Position(spn.Start()); err == nil {
 			pos = prefixPos
 		} else {
-			s.log.Infof(ctx, "failed to convert prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
+			s.session.Logger().Infof(ctx, "failed to convert prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
 		}
 	}
 	return &protocol.CompletionList{
diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go
index b0f996c..1453aa8 100644
--- a/internal/lsp/definition.go
+++ b/internal/lsp/definition.go
@@ -14,7 +14,7 @@
 
 func (s *Server) definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
@@ -48,7 +48,7 @@
 
 func (s *Server) typeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 1d0ac83..851d9bd 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -14,12 +14,12 @@
 
 func (s *Server) Diagnostics(ctx context.Context, view source.View, uri span.URI) {
 	if ctx.Err() != nil {
-		s.log.Errorf(ctx, "canceling diagnostics for %s: %v", uri, ctx.Err())
+		s.session.Logger().Errorf(ctx, "canceling diagnostics for %s: %v", uri, ctx.Err())
 		return
 	}
 	reports, err := source.Diagnostics(ctx, view, uri)
 	if err != nil {
-		s.log.Errorf(ctx, "failed to compute diagnostics for %s: %v", uri, err)
+		s.session.Logger().Errorf(ctx, "failed to compute diagnostics for %s: %v", uri, err)
 		return
 	}
 
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index 0c672e8..f7fa065 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -15,7 +15,7 @@
 
 func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	spn := span.New(uri, span.Point{}, span.Point{})
 	return formatRange(ctx, view, spn)
 }
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index b23d7d5..78d14f1 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -125,7 +125,7 @@
 				}},
 			})
 		}
-		for _, view := range s.views {
+		for _, view := range s.session.Views() {
 			config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
 				Items: []protocol.ConfigurationItem{{
 					ScopeURI: protocol.NewURI(view.Folder()),
@@ -142,7 +142,7 @@
 	}
 	buf := &bytes.Buffer{}
 	PrintVersionInfo(buf, true, false)
-	s.log.Infof(ctx, "%s", buf)
+	s.session.Logger().Infof(ctx, "%s", buf)
 	return nil
 }
 
@@ -195,13 +195,7 @@
 		return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
 	}
 	// drop all the active views
-	s.viewMu.Lock()
-	defer s.viewMu.Unlock()
-	for _, v := range s.views {
-		v.Shutdown(ctx)
-	}
-	s.views = nil
-	s.viewMap = nil
+	s.session.Shutdown(ctx)
 	s.isInitialized = false
 	return nil
 }
diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go
index 22bb169..2473d52 100644
--- a/internal/lsp/highlight.go
+++ b/internal/lsp/highlight.go
@@ -14,7 +14,7 @@
 
 func (s *Server) documentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
index f053b9d..bdc295b 100644
--- a/internal/lsp/hover.go
+++ b/internal/lsp/hover.go
@@ -15,7 +15,7 @@
 
 func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index ecf6e3e..d437fb1 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -14,7 +14,7 @@
 
 func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 04eaa81..c550ada 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -33,19 +33,20 @@
 	data   *tests.Data
 }
 
-func testLSP(t *testing.T, exporter packagestest.Exporter) {
-	ctx := context.Background()
+const viewName = "lsp_test"
 
+func testLSP(t *testing.T, exporter packagestest.Exporter) {
 	data := tests.Load(t, exporter, "testdata")
 	defer data.Exported.Cleanup()
 
 	log := xlog.New(xlog.StdSink{})
+	cache := cache.New()
+	session := cache.NewSession(log)
+	session.NewView(viewName, span.FileURI(data.Config.Dir), &data.Config)
 	r := &runner{
 		server: &Server{
-			views:       []source.View{cache.NewView(ctx, log, "lsp_test", span.FileURI(data.Config.Dir), &data.Config)},
-			viewMap:     make(map[span.URI]source.View),
+			session:     session,
 			undelivered: make(map[span.URI][]source.Diagnostic),
-			log:         log,
 		},
 		data: data,
 	}
@@ -53,7 +54,7 @@
 }
 
 func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) {
-	v := r.server.views[0]
+	v := r.server.session.View(viewName)
 	for uri, want := range data {
 		results, err := source.Diagnostics(context.Background(), v, uri)
 		if err != nil {
@@ -293,7 +294,7 @@
 			}
 			continue
 		}
-		_, m, err := getSourceFile(ctx, r.server.findView(ctx, uri), uri)
+		_, m, err := getSourceFile(ctx, r.server.session.ViewOf(uri), uri)
 		if err != nil {
 			t.Error(err)
 		}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 7424ba3..bfedab4 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -18,30 +18,32 @@
 )
 
 // NewClientServer
-func NewClientServer(client protocol.Client) *Server {
+func NewClientServer(cache source.Cache, client protocol.Client) *Server {
 	return &Server{
-		client: client,
-		log:    xlog.New(protocol.NewLogger(client)),
+		client:  client,
+		session: cache.NewSession(xlog.New(protocol.NewLogger(client))),
 	}
 }
 
 // NewServer starts an LSP server on the supplied stream, and waits until the
 // stream is closed.
-func NewServer(stream jsonrpc2.Stream) *Server {
+func NewServer(cache source.Cache, stream jsonrpc2.Stream) *Server {
 	s := &Server{}
-	s.Conn, s.client, s.log = protocol.NewServer(stream, s)
+	var log xlog.Logger
+	s.Conn, s.client, log = protocol.NewServer(stream, s)
+	s.session = cache.NewSession(log)
 	return s
 }
 
 // RunServerOnPort starts an LSP server on the given port and does not exit.
 // This function exists for debugging purposes.
-func RunServerOnPort(ctx context.Context, port int, h func(s *Server)) error {
-	return RunServerOnAddress(ctx, fmt.Sprintf(":%v", port), h)
+func RunServerOnPort(ctx context.Context, cache source.Cache, port int, h func(s *Server)) error {
+	return RunServerOnAddress(ctx, cache, fmt.Sprintf(":%v", port), h)
 }
 
 // RunServerOnPort starts an LSP server on the given port and does not exit.
 // This function exists for debugging purposes.
-func RunServerOnAddress(ctx context.Context, addr string, h func(s *Server)) error {
+func RunServerOnAddress(ctx context.Context, cache source.Cache, addr string, h func(s *Server)) error {
 	ln, err := net.Listen("tcp", addr)
 	if err != nil {
 		return err
@@ -52,7 +54,7 @@
 			return err
 		}
 		stream := jsonrpc2.NewHeaderStream(conn, conn)
-		s := NewServer(stream)
+		s := NewServer(cache, stream)
 		h(s)
 		go s.Run(ctx)
 	}
@@ -65,7 +67,6 @@
 type Server struct {
 	Conn   *jsonrpc2.Conn
 	client protocol.Client
-	log    xlog.Logger
 
 	initializedMu sync.Mutex
 	isInitialized bool // set once the server has received "initialize" request
@@ -81,9 +82,7 @@
 
 	textDocumentSyncKind protocol.TextDocumentSyncKind
 
-	viewMu  sync.Mutex
-	views   []source.View
-	viewMap map[span.URI]source.View
+	session source.Session
 
 	// undelivered is a cache of any diagnostics that the server
 	// failed to deliver for some reason.
diff --git a/internal/lsp/signature_help.go b/internal/lsp/signature_help.go
index b28f860..20fe7b4 100644
--- a/internal/lsp/signature_help.go
+++ b/internal/lsp/signature_help.go
@@ -14,7 +14,7 @@
 
 func (s *Server) signatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
@@ -29,7 +29,7 @@
 	}
 	info, err := source.SignatureHelp(ctx, f, rng.Start)
 	if err != nil {
-		s.log.Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
+		s.session.Logger().Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
 	}
 	return toProtocolSignatureHelp(info), nil
 }
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 24b24ce..ad7f15b 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -163,7 +163,7 @@
 		cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4}
 		b := &bytes.Buffer{}
 		if err := cfg.Fprint(b, v.FileSet(), p.Type); err != nil {
-			v.Logger().Errorf(ctx, "unable to print type %v", p.Type)
+			v.Session().Logger().Errorf(ctx, "unable to print type %v", p.Type)
 			continue
 		}
 		typ := replacer.Replace(b.String())
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 23b33b6..667f07d 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -157,29 +157,29 @@
 	// Don't set a range if it's anything other than a type error.
 	f, err := v.GetFile(ctx, spn.URI())
 	if err != nil {
-		v.Logger().Errorf(ctx, "Could find file for diagnostic: %v", spn.URI())
+		v.Session().Logger().Errorf(ctx, "Could find file for diagnostic: %v", spn.URI())
 		return spn
 	}
 	diagFile, ok := f.(GoFile)
 	if !ok {
-		v.Logger().Errorf(ctx, "Not a go file: %v", spn.URI())
+		v.Session().Logger().Errorf(ctx, "Not a go file: %v", spn.URI())
 		return spn
 	}
 	tok := diagFile.GetToken(ctx)
 	if tok == nil {
-		v.Logger().Errorf(ctx, "Could not find tokens for diagnostic: %v", spn.URI())
+		v.Session().Logger().Errorf(ctx, "Could not find tokens for diagnostic: %v", spn.URI())
 		return spn
 	}
 	content := diagFile.GetContent(ctx)
 	if content == nil {
-		v.Logger().Errorf(ctx, "Could not find content for diagnostic: %v", spn.URI())
+		v.Session().Logger().Errorf(ctx, "Could not find content for diagnostic: %v", spn.URI())
 		return spn
 	}
 	c := span.NewTokenConverter(diagFile.GetFileSet(ctx), tok)
 	s, err := spn.WithOffset(c)
 	//we just don't bother producing an error if this failed
 	if err != nil {
-		v.Logger().Errorf(ctx, "invalid span for diagnostic: %v: %v", spn.URI(), err)
+		v.Session().Logger().Errorf(ctx, "invalid span for diagnostic: %v: %v", spn.URI(), err)
 		return spn
 	}
 	start := s.Start()
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index a0e8308..263ba08 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -32,14 +32,14 @@
 }
 
 func testSource(t *testing.T, exporter packagestest.Exporter) {
-	ctx := context.Background()
-
 	data := tests.Load(t, exporter, "../testdata")
 	defer data.Exported.Cleanup()
 
 	log := xlog.New(xlog.StdSink{})
+	cache := cache.New()
+	session := cache.NewSession(log)
 	r := &runner{
-		view: cache.NewView(ctx, log, "source_test", span.FileURI(data.Config.Dir), &data.Config),
+		view: session.NewView("source_test", span.FileURI(data.Config.Dir), &data.Config),
 		data: data,
 	}
 	tests.Run(t, r, data)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index fbbf91f..57a8707 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -18,13 +18,47 @@
 	"golang.org/x/tools/internal/span"
 )
 
-// View abstracts the underlying architecture of the package using the source
-// package. The view provides access to files and their contents, so the source
+// Cache abstracts the core logic of dealing with the environment from the
+// higher level logic that processes the information to produce results.
+// The cache provides access to files and their contents, so the source
 // package does not directly access the file system.
+// A single cache is intended to be process wide, and is the primary point of
+// sharing between all consumers.
+// A cache may have many active sessions at any given time.
+type Cache interface {
+	// NewSession creates a new Session manager and returns it.
+	NewSession(log xlog.Logger) Session
+}
+
+// Session represents a single connection from a client.
+// This is the level at which things like open files are maintained on behalf
+// of the client.
+// A session may have many active views at any given time.
+type Session interface {
+	// NewView creates a new View and returns it.
+	NewView(name string, folder span.URI, config *packages.Config) View
+
+	// Cache returns the cache that created this session.
+	Cache() Cache
+
+	// Returns the logger in use for this session.
+	Logger() xlog.Logger
+
+	View(name string) View
+	ViewOf(uri span.URI) View
+	Views() []View
+
+	Shutdown(ctx context.Context)
+}
+
+// View represents a single workspace.
+// This is the level at which we maintain configuration like working directory
+// and build tags.
 type View interface {
+	// Session returns the session that created this view.
+	Session() Session
 	Name() string
 	Folder() span.URI
-	Logger() xlog.Logger
 	FileSet() *token.FileSet
 	BuiltinPackage() *ast.Package
 	GetFile(ctx context.Context, uri span.URI) (File, error)
diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go
index 2ee526a..1b1eb22 100644
--- a/internal/lsp/symbols.go
+++ b/internal/lsp/symbols.go
@@ -14,7 +14,7 @@
 
 func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 5402f23..d8c219e 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -42,7 +42,7 @@
 }
 
 func (s *Server) cacheAndDiagnose(ctx context.Context, uri span.URI, content []byte) error {
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	if err := view.SetContent(ctx, uri, content); err != nil {
 		return err
 	}
@@ -64,7 +64,7 @@
 	}
 
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	file, m, err := getSourceFile(ctx, view, uri)
 	if err != nil {
 		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
@@ -97,6 +97,6 @@
 
 func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
 	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
+	view := s.session.ViewOf(uri)
 	return view.SetContent(ctx, uri, nil)
 }
diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go
index 938450a..687e2c8 100644
--- a/internal/lsp/workspace.go
+++ b/internal/lsp/workspace.go
@@ -11,20 +11,19 @@
 	"go/parser"
 	"go/token"
 	"os"
-	"strings"
 
 	"golang.org/x/tools/go/packages"
-	"golang.org/x/tools/internal/lsp/cache"
 	"golang.org/x/tools/internal/lsp/protocol"
-	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 )
 
 func (s *Server) changeFolders(ctx context.Context, event protocol.WorkspaceFoldersChangeEvent) error {
-	s.log.Infof(ctx, "change folders")
 	for _, folder := range event.Removed {
-		if err := s.removeView(ctx, folder.Name, span.NewURI(folder.URI)); err != nil {
-			return err
+		view := s.session.View(folder.Name)
+		if view != nil {
+			view.Shutdown(ctx)
+		} else {
+			return fmt.Errorf("view %s for %v not found", folder.Name, folder.URI)
 		}
 	}
 
@@ -37,17 +36,14 @@
 }
 
 func (s *Server) addView(ctx context.Context, name string, uri span.URI) error {
-	s.viewMu.Lock()
-	defer s.viewMu.Unlock()
 	// We need a "detached" context so it does not get timeout cancelled.
 	// TODO(iancottrell): Do we need to copy any values across?
 	viewContext := context.Background()
-	s.log.Infof(viewContext, "add view %v as %v", name, uri)
 	folderPath, err := uri.Filename()
 	if err != nil {
 		return err
 	}
-	s.views = append(s.views, cache.NewView(viewContext, s.log, name, uri, &packages.Config{
+	s.session.NewView(name, uri, &packages.Config{
 		Context: viewContext,
 		Dir:     folderPath,
 		Env:     os.Environ(),
@@ -58,65 +54,7 @@
 			return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
 		},
 		Tests: true,
-	}))
-	// we always need to drop the view map
-	s.viewMap = make(map[span.URI]source.View)
+	})
+
 	return nil
 }
-
-func (s *Server) removeView(ctx context.Context, name string, uri span.URI) error {
-	s.viewMu.Lock()
-	defer s.viewMu.Unlock()
-	// we always need to drop the view map
-	s.viewMap = make(map[span.URI]source.View)
-	s.log.Infof(ctx, "drop view %v as %v", name, uri)
-	for i, view := range s.views {
-		if view.Name() == name {
-			// 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]
-			view.Shutdown(ctx)
-			return nil
-		}
-	}
-	return fmt.Errorf("view %s for %v not found", name, uri)
-}
-
-// findView returns the view corresponding to the given URI.
-// If the file is not already associated with a view, pick one using some heuristics.
-func (s *Server) findView(ctx context.Context, 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(ctx, uri)
-	s.viewMap[uri] = v
-	return v
-}
-
-// bestView finds the best view to associate a given URI with.
-// viewMu must be held when calling this method.
-func (s *Server) bestView(ctx context.Context, 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]
-}