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>
This commit is contained in:
parent
4ca280b5bd
commit
80b1e8742c
|
@ -7,14 +7,10 @@ package lsp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
|
||||||
"go/parser"
|
|
||||||
"go/token"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/tools/go/packages"
|
|
||||||
"golang.org/x/tools/internal/jsonrpc2"
|
"golang.org/x/tools/internal/jsonrpc2"
|
||||||
"golang.org/x/tools/internal/lsp/cache"
|
"golang.org/x/tools/internal/lsp/cache"
|
||||||
"golang.org/x/tools/internal/lsp/protocol"
|
"golang.org/x/tools/internal/lsp/protocol"
|
||||||
|
@ -39,9 +35,6 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
|
||||||
|
|
||||||
s.setClientCapabilities(params.Capabilities)
|
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
|
folders := params.WorkspaceFolders
|
||||||
if len(folders) == 0 {
|
if len(folders) == 0 {
|
||||||
if params.RootURI != "" {
|
if params.RootURI != "" {
|
||||||
|
@ -56,24 +49,11 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
|
||||||
return nil, fmt.Errorf("single file mode not supported yet")
|
return nil, fmt.Errorf("single file mode not supported yet")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
uri := span.NewURI(folder.URI)
|
if err := s.addView(ctx, folder.Name, span.NewURI(folder.URI)); err != nil {
|
||||||
folderPath, err := uri.Filename()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
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{
|
return &protocol.InitializeResult{
|
||||||
|
@ -96,6 +76,20 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
|
||||||
OpenClose: true,
|
OpenClose: true,
|
||||||
},
|
},
|
||||||
TypeDefinitionProvider: 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
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -124,6 +118,9 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa
|
||||||
Registrations: []protocol.Registration{{
|
Registrations: []protocol.Registration{{
|
||||||
ID: "workspace/didChangeConfiguration",
|
ID: "workspace/didChangeConfiguration",
|
||||||
Method: "workspace/didChangeConfiguration",
|
Method: "workspace/didChangeConfiguration",
|
||||||
|
}, {
|
||||||
|
ID: "workspace/didChangeWorkspaceFolders",
|
||||||
|
Method: "workspace/didChangeWorkspaceFolders",
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
|
||||||
r := &runner{
|
r := &runner{
|
||||||
server: &Server{
|
server: &Server{
|
||||||
views: []*cache.View{cache.NewView(ctx, log, "lsp_test", span.FileURI(data.Config.Dir), &data.Config)},
|
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),
|
undelivered: make(map[span.URI][]source.Diagnostic),
|
||||||
log: log,
|
log: log,
|
||||||
},
|
},
|
||||||
|
|
|
@ -82,7 +82,9 @@ type Server struct {
|
||||||
|
|
||||||
textDocumentSyncKind protocol.TextDocumentSyncKind
|
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
|
// undelivered is a cache of any diagnostics that the server
|
||||||
// failed to deliver for some reason.
|
// failed to deliver for some reason.
|
||||||
|
@ -110,8 +112,8 @@ func (s *Server) Exit(ctx context.Context) error {
|
||||||
|
|
||||||
// Workspace
|
// Workspace
|
||||||
|
|
||||||
func (s *Server) DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error {
|
func (s *Server) DidChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error {
|
||||||
return notImplemented("DidChangeWorkspaceFolders")
|
return s.changeFolders(ctx, params.Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) DidChangeConfiguration(context.Context, *protocol.DidChangeConfigurationParams) error {
|
func (s *Server) DidChangeConfiguration(context.Context, *protocol.DidChangeConfigurationParams) error {
|
||||||
|
|
|
@ -7,39 +7,12 @@ package lsp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/tools/internal/lsp/cache"
|
|
||||||
"golang.org/x/tools/internal/lsp/protocol"
|
"golang.org/x/tools/internal/lsp/protocol"
|
||||||
"golang.org/x/tools/internal/lsp/source"
|
"golang.org/x/tools/internal/lsp/source"
|
||||||
"golang.org/x/tools/internal/span"
|
"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) {
|
func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
|
||||||
f, err := v.GetFile(ctx, uri)
|
f, err := v.GetFile(ctx, uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
Loading…
Reference in New Issue