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]
+}