internal/lsp: support dynamic workspace folder changes

This also required us to change the way we map files to a view, as it may change
over time.

Fixes golang/go#31635

Change-Id: Ic82467a1185717081487389f4c25ad69df1af290
Reviewed-on: https://go-review.googlesource.com/c/tools/+/175477
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index f3f8608..212a039 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -7,14 +7,10 @@
 import (
 	"context"
 	"fmt"
-	"go/ast"
-	"go/parser"
-	"go/token"
 	"os"
 	"path"
 	"strings"
 
-	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp/cache"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -39,9 +35,6 @@
 
 	s.setClientCapabilities(params.Capabilities)
 
-	// 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()
 	folders := params.WorkspaceFolders
 	if len(folders) == 0 {
 		if params.RootURI != "" {
@@ -56,24 +49,11 @@
 			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 {
+		if err := s.addView(ctx, folder.Name, span.NewURI(folder.URI)); 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{
@@ -96,6 +76,20 @@
 				OpenClose: true,
 			},
 			TypeDefinitionProvider: true,
+			Workspace: &struct {
+				WorkspaceFolders *struct {
+					Supported           bool   "json:\"supported,omitempty\""
+					ChangeNotifications string "json:\"changeNotifications,omitempty\""
+				} "json:\"workspaceFolders,omitempty\""
+			}{
+				WorkspaceFolders: &struct {
+					Supported           bool   "json:\"supported,omitempty\""
+					ChangeNotifications string "json:\"changeNotifications,omitempty\""
+				}{
+					Supported:           true,
+					ChangeNotifications: "workspace/didChangeWorkspaceFolders",
+				},
+			},
 		},
 	}, nil
 }
@@ -124,6 +118,9 @@
 				Registrations: []protocol.Registration{{
 					ID:     "workspace/didChangeConfiguration",
 					Method: "workspace/didChangeConfiguration",
+				}, {
+					ID:     "workspace/didChangeWorkspaceFolders",
+					Method: "workspace/didChangeWorkspaceFolders",
 				}},
 			})
 		}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index bfc8fc7..05bae65 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -43,6 +43,7 @@
 	r := &runner{
 		server: &Server{
 			views:       []*cache.View{cache.NewView(ctx, log, "lsp_test", span.FileURI(data.Config.Dir), &data.Config)},
+			viewMap:     make(map[span.URI]*cache.View),
 			undelivered: make(map[span.URI][]source.Diagnostic),
 			log:         log,
 		},
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 71a3d48..0a23f69 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -82,7 +82,9 @@
 
 	textDocumentSyncKind protocol.TextDocumentSyncKind
 
-	views []*cache.View
+	viewMu  sync.Mutex
+	views   []*cache.View
+	viewMap map[span.URI]*cache.View
 
 	// undelivered is a cache of any diagnostics that the server
 	// failed to deliver for some reason.
@@ -110,8 +112,8 @@
 
 // Workspace
 
-func (s *Server) DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error {
-	return notImplemented("DidChangeWorkspaceFolders")
+func (s *Server) DidChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error {
+	return s.changeFolders(ctx, params.Event)
 }
 
 func (s *Server) DidChangeConfiguration(context.Context, *protocol.DidChangeConfigurationParams) error {
diff --git a/internal/lsp/util.go b/internal/lsp/util.go
index e1c97ad..48d83fc 100644
--- a/internal/lsp/util.go
+++ b/internal/lsp/util.go
@@ -7,39 +7,12 @@
 import (
 	"context"
 	"fmt"
-	"strings"
 
-	"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"
 )
 
-// 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) *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]
-}
-
 func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
 	f, err := v.GetFile(ctx, uri)
 	if err != nil {
diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go
new file mode 100644
index 0000000..1b90473
--- /dev/null
+++ b/internal/lsp/workspace.go
@@ -0,0 +1,121 @@
+// 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 lsp
+
+import (
+	"context"
+	"fmt"
+	"go/ast"
+	"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/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
+		}
+	}
+
+	for _, folder := range event.Added {
+		if err := s.addView(ctx, folder.Name, span.NewURI(folder.URI)); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+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{
+		Context: viewContext,
+		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,
+	}))
+	// we always need to drop the view map
+	s.viewMap = make(map[span.URI]*cache.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]*cache.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]
+			//TODO: shutdown the view in here
+			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) *cache.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) *cache.View {
+	// we need to find the best view for this file
+	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]
+}