gopls/internal/lsp: cleanups to view logic

CL 541124 exposed a number of subleties in the view
logic that this CL cleans up. Specifically:

- addFolders was called in one place with potentially
  invalid folder URIs, violating its recently added
  precondition. The precondition has been removed
  and addFolder now handles the error.

- DidChangeWorkspaceFolders used the Folder.Name as the
  key for removing the view, which is incorrect as the
  name is merely decorative; the URI is the key.

- The ViewByName and RemoveView loops have been fused;
  each is used only once.

Change-Id: I1df39d5739464530b4e0ec826a7c08bb73b5b314
Reviewed-on: https://go-review.googlesource.com/c/tools/+/545555
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 4383e87..9b632ca 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -213,16 +213,24 @@
 	return v, snapshot, snapshot.Acquire(), nil
 }
 
-// ViewByName returns a view with a matching name, if the session has one.
-func (s *Session) ViewByName(name string) *View {
+// RemoveView removes from the session the view rooted at the specified directory.
+// It reports whether a view of that directory was removed.
+func (s *Session) RemoveView(dir protocol.DocumentURI) bool {
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
 	for _, view := range s.views {
-		if view.Name() == name {
-			return view
+		if view.folder.Dir == dir {
+			i := s.dropView(view)
+			if i == -1 {
+				return false // can't happen
+			}
+			// delete this view... we don't care about order but we do want to make
+			// sure we can garbage collect the view
+			s.views = removeElement(s.views, i)
+			return true
 		}
 	}
-	return nil
+	return false
 }
 
 // View returns the view with a matching id, if present.
@@ -295,20 +303,6 @@
 	return views[0]
 }
 
-// RemoveView removes the view v from the session
-func (s *Session) RemoveView(view *View) {
-	s.viewMu.Lock()
-	defer s.viewMu.Unlock()
-
-	i := s.dropView(view)
-	if i == -1 { // error reported elsewhere
-		return
-	}
-	// delete this view... we don't care about order but we do want to make
-	// sure we can garbage collect the view
-	s.views = removeElement(s.views, i)
-}
-
 // updateViewLocked recreates the view with the given options.
 //
 // If the resulting error is non-nil, the view may or may not have already been
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index 4029af0..8d52fc8 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -44,7 +44,7 @@
 // Folders must not be mutated, as they may be shared across multiple views.
 type Folder struct {
 	Dir     protocol.DocumentURI
-	Name    string
+	Name    string // decorative name for UI; not necessarily unique
 	Options *settings.Options
 }
 
diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go
index 663edbd..cf154d6 100644
--- a/gopls/internal/lsp/general.go
+++ b/gopls/internal/lsp/general.go
@@ -289,11 +289,9 @@
 // directories) to the session. It does not return an error, though it
 // may report an error to the client over LSP if one or more folders
 // had problems.
-//
-// addFolders must be called only with valid file URIs.
 func (s *server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) {
 	originalViews := len(s.session.Views())
-	viewErrors := make(map[protocol.DocumentURI]error)
+	viewErrors := make(map[protocol.URI]error)
 
 	var ndiagnose sync.WaitGroup // number of unfinished diagnose calls
 	if s.Options().VerboseWorkDoneProgress {
@@ -310,7 +308,7 @@
 	for _, folder := range folders {
 		uri, err := protocol.ParseDocumentURI(folder.URI)
 		if err != nil {
-			bug.Reportf("addFolders: invalid folder URI: %v", err)
+			viewErrors[folder.URI] = fmt.Errorf("invalid folder URI: %v", err)
 			continue
 		}
 		work := s.progress.Start(ctx, "Setting up workspace", "Loading packages...", nil, nil)
@@ -319,7 +317,7 @@
 			if err == cache.ErrViewExists {
 				continue
 			}
-			viewErrors[uri] = err
+			viewErrors[folder.URI] = err
 			work.End(ctx, fmt.Sprintf("Error loading packages: %s", err))
 			continue
 		}
diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go
index e2a2da8..88d1e50 100644
--- a/gopls/internal/lsp/workspace.go
+++ b/gopls/internal/lsp/workspace.go
@@ -15,34 +15,34 @@
 )
 
 func (s *server) DidChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error {
-	event := params.Event
-	for _, folder := range event.Removed {
-		view := s.session.ViewByName(folder.Name)
-		if view != nil {
-			s.session.RemoveView(view)
-		} else {
-			return fmt.Errorf("view %s for %v not found", folder.Name, folder.URI)
+	for _, folder := range params.Event.Removed {
+		dir, err := protocol.ParseDocumentURI(folder.URI)
+		if err != nil {
+			return fmt.Errorf("invalid folder %q: %v", folder.URI, err)
+		}
+		if !s.session.RemoveView(dir) {
+			return fmt.Errorf("view %q for %v not found", folder.Name, folder.URI)
 		}
 	}
-	s.addFolders(ctx, event.Added)
+	s.addFolders(ctx, params.Event.Added)
 	return nil
 }
 
 // addView returns a Snapshot and a release function that must be
 // called when it is no longer needed.
-func (s *server) addView(ctx context.Context, name string, uri protocol.DocumentURI) (*cache.Snapshot, func(), error) {
+func (s *server) addView(ctx context.Context, name string, dir protocol.DocumentURI) (*cache.Snapshot, func(), error) {
 	s.stateMu.Lock()
 	state := s.state
 	s.stateMu.Unlock()
 	if state < serverInitialized {
 		return nil, nil, fmt.Errorf("addView called before server initialized")
 	}
-	options, err := s.fetchFolderOptions(ctx, uri)
+	options, err := s.fetchFolderOptions(ctx, dir)
 	if err != nil {
 		return nil, nil, err
 	}
 	folder := &cache.Folder{
-		Dir:     uri,
+		Dir:     dir,
 		Name:    name,
 		Options: options,
 	}