internal/lsp: lowercase drive letters on Windows to fix file watching

This is a work-around for
https://github.com/microsoft/vscode/issues/104387. We now always
lowercase the drive letter on Windows.

This CL also fixes a bug introduced by CL 245327, which caused URIs
to be used instead of paths in the GlobPattern.

We really need VS Code integration tests for this
(golang/vscode-go#404).

Updates golang/go#40661

Change-Id: I21be6d929288cfe41168cea34001fc2f41ac6c8b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/247684
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index ee2adda..6661eb1 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -11,6 +11,8 @@
 	"io"
 	"os"
 	"path"
+	"path/filepath"
+	"strings"
 	"sync"
 
 	"golang.org/x/tools/internal/event"
@@ -46,6 +48,10 @@
 	if params.RootURI != "" && !params.RootURI.SpanURI().IsFile() {
 		return nil, fmt.Errorf("unsupported URI scheme: %v (gopls only supports file URIs)", params.RootURI)
 	}
+	if params.RootURI != "" {
+		s.rootURI = params.RootURI.SpanURI()
+	}
+
 	for _, folder := range params.WorkspaceFolders {
 		uri := span.URIFromURI(folder.URI)
 		if !uri.IsFile() {
@@ -144,20 +150,6 @@
 	options := s.session.Options()
 	defer func() { s.session.SetOptions(options) }()
 
-	var registrations []protocol.Registration
-	if options.ConfigurationSupported && options.DynamicConfigurationSupported {
-		registrations = append(registrations,
-			protocol.Registration{
-				ID:     "workspace/didChangeConfiguration",
-				Method: "workspace/didChangeConfiguration",
-			},
-			protocol.Registration{
-				ID:     "workspace/didChangeWorkspaceFolders",
-				Method: "workspace/didChangeWorkspaceFolders",
-			},
-		)
-	}
-
 	// TODO: this event logging may be unnecessary.
 	// The version info is included in the initialize response.
 	buf := &bytes.Buffer{}
@@ -169,9 +161,18 @@
 	}
 	s.pendingFolders = nil
 
-	if len(registrations) > 0 {
+	if options.ConfigurationSupported && options.DynamicConfigurationSupported {
 		if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
-			Registrations: registrations,
+			Registrations: []protocol.Registration{
+				{
+					ID:     "workspace/didChangeConfiguration",
+					Method: "workspace/didChangeConfiguration",
+				},
+				{
+					ID:     "workspace/didChangeWorkspaceFolders",
+					Method: "workspace/didChangeWorkspaceFolders",
+				},
+			},
 		}); err != nil {
 			return err
 		}
@@ -322,10 +323,25 @@
 	for k := range s.watchedDirectories {
 		delete(s.watchedDirectories, k)
 	}
-	var watchers []protocol.FileSystemWatcher
+	// Work-around microsoft/vscode#100870 by making sure that we are,
+	// at least, watching the user's entire workspace. This will still be
+	// applied to every folder in the workspace.
+	watchers := []protocol.FileSystemWatcher{{
+		GlobPattern: "**/*.{go,mod,sum}",
+		Kind:        float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
+	}}
 	for dir := range dirs {
+		filename := dir.Filename()
+		// If the directory is within the root URI, we're already watching it
+		// via the relative path above.
+		if isSubdirectory(s.rootURI.Filename(), filename) {
+			continue
+		}
+		// If microsoft/vscode#100870 is resolved before
+		// microsoft/vscode#104387, we will need a work-around for Windows
+		// drive letter casing.
 		watchers = append(watchers, protocol.FileSystemWatcher{
-			GlobPattern: fmt.Sprintf("%s/**/*.{go,mod,sum}", dir),
+			GlobPattern: fmt.Sprintf("%s/**/*.{go,mod,sum}", filename),
 			Kind:        float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
 		})
 	}
@@ -348,6 +364,11 @@
 	return nil
 }
 
+func isSubdirectory(root, leaf string) bool {
+	rel, err := filepath.Rel(root, leaf)
+	return err == nil && !strings.HasPrefix(rel, "..")
+}
+
 func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, o *source.Options) error {
 	if !s.session.Options().ConfigurationSupported {
 		return nil
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index cfcc72e..20568c9 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -64,6 +64,9 @@
 
 	session source.Session
 
+	// rootURI is the root of the workspace opened in the editor (if any).
+	rootURI span.URI
+
 	// changedFiles tracks files for which there has been a textDocument/didChange.
 	changedFilesMu sync.Mutex
 	changedFiles   map[span.URI]struct{}
diff --git a/internal/span/uri.go b/internal/span/uri.go
index 78e71fe..2504921 100644
--- a/internal/span/uri.go
+++ b/internal/span/uri.go
@@ -160,7 +160,7 @@
 
 // isWindowsDriveURI returns true if the file URI is of the format used by
 // Windows URIs. The url.Parse package does not specially handle Windows paths
-// (see golang/go#6027). We check if the URI path has a drive prefix (e.g. "/C:").
+// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
 func isWindowsDriveURIPath(uri string) bool {
 	if len(uri) < 4 {
 		return false