internal/lsp: support multiple views

This uses the workspace folders to build multiple views, and then tries to pick
the right view to send each incomming request to.

Change-Id: I0cc896dbbc67eb0a88225ddeca6c518f4258bbba
Reviewed-on: https://go-review.googlesource.com/c/tools/+/170179
Run-TryBot: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index f37cba0..67bed47 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -209,6 +209,17 @@
 	delete(v.pcache.packages, pkgPath)
 }
 
+// FindFile returns the file if the given URI is already a part of the view.
+func (v *View) FindFile(ctx context.Context, uri span.URI) *File {
+	v.mu.Lock()
+	defer v.mu.Unlock()
+	f, err := v.findFile(uri)
+	if err != nil {
+		return nil
+	}
+	return f
+}
+
 // GetFile returns a File for the given URI. It will always succeed because it
 // adds the file to the managed set if needed.
 func (v *View) GetFile(ctx context.Context, uri span.URI) (source.File, error) {
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 0602bbd..a479e67 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -8,21 +8,23 @@
 	"context"
 	"sort"
 
+	"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) cacheAndDiagnose(ctx context.Context, uri span.URI, content string) error {
-	if err := s.view.SetContent(ctx, uri, []byte(content)); err != nil {
+	view := s.findView(ctx, uri)
+	if err := view.SetContent(ctx, uri, []byte(content)); err != nil {
 		return err
 	}
 	go func() {
-		ctx := s.view.BackgroundContext()
+		ctx := view.BackgroundContext()
 		if ctx.Err() != nil {
 			return
 		}
-		reports, err := source.Diagnostics(ctx, s.view, uri)
+		reports, err := source.Diagnostics(ctx, view, uri)
 		if err != nil {
 			return // handle error?
 		}
@@ -31,7 +33,7 @@
 		defer s.undeliveredMu.Unlock()
 
 		for uri, diagnostics := range reports {
-			if err := s.publishDiagnostics(ctx, uri, diagnostics); err != nil {
+			if err := s.publishDiagnostics(ctx, view, uri, diagnostics); err != nil {
 				if s.undelivered == nil {
 					s.undelivered = make(map[span.URI][]source.Diagnostic)
 				}
@@ -44,7 +46,7 @@
 		// Anytime we compute diagnostics, make sure to also send along any
 		// undelivered ones (only for remaining URIs).
 		for uri, diagnostics := range s.undelivered {
-			s.publishDiagnostics(ctx, uri, diagnostics)
+			s.publishDiagnostics(ctx, view, uri, diagnostics)
 
 			// If we fail to deliver the same diagnostics twice, just give up.
 			delete(s.undelivered, uri)
@@ -53,8 +55,8 @@
 	return nil
 }
 
-func (s *Server) publishDiagnostics(ctx context.Context, uri span.URI, diagnostics []source.Diagnostic) error {
-	protocolDiagnostics, err := toProtocolDiagnostics(ctx, s.view, diagnostics)
+func (s *Server) publishDiagnostics(ctx context.Context, view *cache.View, uri span.URI, diagnostics []source.Diagnostic) error {
+	protocolDiagnostics, err := toProtocolDiagnostics(ctx, view, diagnostics)
 	if err != nil {
 		return err
 	}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 26da588..a49060d 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -74,7 +74,7 @@
 
 	log := xlog.New(xlog.StdSink{})
 	s := &Server{
-		view:        cache.NewView(ctx, log, "lsp_test", span.FileURI(cfg.Dir), &cfg),
+		views:       []*cache.View{cache.NewView(ctx, log, "lsp_test", span.FileURI(cfg.Dir), &cfg)},
 		undelivered: make(map[span.URI][]source.Diagnostic),
 	}
 	// Do a first pass to collect special markers for completion.
@@ -121,7 +121,7 @@
 
 	t.Run("Diagnostics", func(t *testing.T) {
 		t.Helper()
-		diagnosticsCount := expectedDiagnostics.test(t, s.view)
+		diagnosticsCount := expectedDiagnostics.test(t, s.views[0])
 		if goVersion111 { // TODO(rstambler): Remove this when we no longer support Go 1.10.
 			if diagnosticsCount != expectedDiagnosticsCount {
 				t.Errorf("got %v diagnostics expected %v", diagnosticsCount, expectedDiagnosticsCount)
@@ -425,7 +425,7 @@
 			}
 			continue
 		}
-		f, m, err := newColumnMap(ctx, s.view, uri)
+		f, m, err := newColumnMap(ctx, s.findView(ctx, uri), uri)
 		if err != nil {
 			t.Error(err)
 		}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index f18debe..cbabbca 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -80,8 +80,7 @@
 
 	textDocumentSyncKind protocol.TextDocumentSyncKind
 
-	viewMu sync.Mutex
-	view   *cache.View
+	views []*cache.View
 
 	// undelivered is a cache of any diagnostics that the server
 	// failed to deliver for some reason.
@@ -113,15 +112,6 @@
 	}
 	s.signatureHelpEnabled = true
 
-	var rootURI span.URI
-	if params.RootURI != "" {
-		rootURI = span.NewURI(params.RootURI)
-	}
-	rootPath, err := rootURI.Filename()
-	if err != nil {
-		return nil, err
-	}
-
 	// TODO(rstambler): Change this default to protocol.Incremental (or add a
 	// flag). Disabled for now to simplify debugging.
 	s.textDocumentSyncKind = protocol.Full
@@ -129,19 +119,39 @@
 	//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()
-	//TODO:use workspace folders
-	s.view = cache.NewView(viewContext, s.log, path.Base(string(rootURI)), rootURI, &packages.Config{
-		Context: ctx,
-		Dir:     rootPath,
-		Env:     os.Environ(),
-		Mode:    packages.LoadImports,
-		Fset:    token.NewFileSet(),
-		Overlay: make(map[string][]byte),
-		ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
-			return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
-		},
-		Tests: true,
-	})
+	folders := params.WorkspaceFolders
+	if len(folders) == 0 {
+		if params.RootURI != "" {
+			folders = []protocol.WorkspaceFolder{{
+				URI:  params.RootURI,
+				Name: path.Base(params.RootURI),
+			}}
+		} else {
+			// no folders and no root, single file mode
+			//TODO(iancottrell): not sure how to do single file mode yet
+			//issue: golang.org/issue/31168
+			return nil, fmt.Errorf("single file mode not supported yet")
+		}
+	}
+	for _, folder := range folders {
+		uri := span.NewURI(folder.URI)
+		folderPath, err := uri.Filename()
+		if err != nil {
+			return nil, err
+		}
+		s.views = append(s.views, cache.NewView(viewContext, s.log, folder.Name, uri, &packages.Config{
+			Context: ctx,
+			Dir:     folderPath,
+			Env:     os.Environ(),
+			Mode:    packages.LoadImports,
+			Fset:    token.NewFileSet(),
+			Overlay: make(map[string][]byte),
+			ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
+				return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
+			},
+			Tests: true,
+		}))
+	}
 
 	return &protocol.InitializeResult{
 		Capabilities: protocol.ServerCapabilities{
@@ -178,17 +188,19 @@
 			Method: "workspace/didChangeConfiguration",
 		}},
 	})
-	config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
-		Items: []protocol.ConfigurationItem{{
-			ScopeURI: protocol.NewURI(s.view.Folder),
-			Section:  "gopls",
-		}},
-	})
-	if err != nil {
-		return err
-	}
-	if err := s.processConfig(config[0]); err != nil {
-		return err
+	for _, view := range s.views {
+		config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
+			Items: []protocol.ConfigurationItem{{
+				ScopeURI: protocol.NewURI(view.Folder),
+				Section:  "gopls",
+			}},
+		})
+		if err != nil {
+			return err
+		}
+		if err := s.processConfig(view, config[0]); err != nil {
+			return err
+		}
 	}
 	return nil
 }
@@ -245,7 +257,9 @@
 		return change.Text, nil
 	}
 
-	file, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	file, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
 	}
@@ -308,14 +322,15 @@
 }
 
 func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
-	if err := s.view.SetContent(ctx, span.NewURI(params.TextDocument.URI), nil); err != nil {
-		return err
-	}
-	return nil
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	return view.SetContent(ctx, uri, nil)
 }
 
 func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
-	f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -342,7 +357,9 @@
 }
 
 func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
-	f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -354,7 +371,7 @@
 	if err != nil {
 		return nil, err
 	}
-	ident, err := source.Identifier(ctx, s.view, f, identRange.Start)
+	ident, err := source.Identifier(ctx, view, f, identRange.Start)
 	if err != nil {
 		return nil, err
 	}
@@ -381,7 +398,9 @@
 }
 
 func (s *Server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
-	f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -401,7 +420,9 @@
 }
 
 func (s *Server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
-	f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -413,7 +434,7 @@
 	if err != nil {
 		return nil, err
 	}
-	ident, err := source.Identifier(ctx, s.view, f, rng.Start)
+	ident, err := source.Identifier(ctx, view, f, rng.Start)
 	if err != nil {
 		return nil, err
 	}
@@ -421,7 +442,7 @@
 	if err != nil {
 		return nil, err
 	}
-	_, decM, err := newColumnMap(ctx, s.view, decSpan.URI())
+	_, decM, err := newColumnMap(ctx, view, decSpan.URI())
 	if err != nil {
 		return nil, err
 	}
@@ -433,7 +454,9 @@
 }
 
 func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
-	f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -445,7 +468,7 @@
 	if err != nil {
 		return nil, err
 	}
-	ident, err := source.Identifier(ctx, s.view, f, rng.Start)
+	ident, err := source.Identifier(ctx, view, f, rng.Start)
 	if err != nil {
 		return nil, err
 	}
@@ -453,7 +476,7 @@
 	if err != nil {
 		return nil, err
 	}
-	_, identM, err := newColumnMap(ctx, s.view, identSpan.URI())
+	_, identM, err := newColumnMap(ctx, view, identSpan.URI())
 	if err != nil {
 		return nil, err
 	}
@@ -473,7 +496,9 @@
 }
 
 func (s *Server) DocumentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
-	f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -493,7 +518,9 @@
 }
 
 func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
-	f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -502,7 +529,9 @@
 }
 
 func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
-	_, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	_, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -510,7 +539,7 @@
 	if err != nil {
 		return nil, err
 	}
-	edits, err := organizeImports(ctx, s.view, spn)
+	edits, err := organizeImports(ctx, view, spn)
 	if err != nil {
 		return nil, err
 	}
@@ -552,12 +581,16 @@
 }
 
 func (s *Server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
-	spn := span.New(span.URI(params.TextDocument.URI), span.Point{}, span.Point{})
-	return formatRange(ctx, s.view, spn)
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	spn := span.New(uri, span.Point{}, span.Point{})
+	return formatRange(ctx, view, spn)
 }
 
 func (s *Server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
-	_, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	_, m, err := newColumnMap(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -565,7 +598,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return formatRange(ctx, s.view, spn)
+	return formatRange(ctx, view, spn)
 }
 
 func (s *Server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {
@@ -580,7 +613,7 @@
 	return nil, notImplemented("FoldingRanges")
 }
 
-func (s *Server) processConfig(config interface{}) error {
+func (s *Server) processConfig(view *cache.View, config interface{}) error {
 	//TODO: we should probably store and process more of the config
 	c, ok := config.(map[string]interface{})
 	if !ok {
@@ -595,7 +628,7 @@
 		return fmt.Errorf("Invalid config gopls.env type %T", env)
 	}
 	for k, v := range menv {
-		s.view.Config.Env = applyEnv(s.view.Config.Env, k, v)
+		view.Config.Env = applyEnv(view.Config.Env, k, v)
 	}
 	return nil
 }
@@ -615,3 +648,26 @@
 func notImplemented(method string) *jsonrpc2.Error {
 	return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method)
 }
+
+func (s *Server) findView(ctx context.Context, uri span.URI) *cache.View {
+	// first see if a view already has this file
+	for _, view := range s.views {
+		if view.FindFile(ctx, uri) != nil {
+			return view
+		}
+	}
+	var longest *cache.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]
+}