internal/lsp: support multiple views
This uses the workspace folders to build multiple views, and then tries to pick
the right view to send each incomming request to.
Change-Id: I0cc896dbbc67eb0a88225ddeca6c518f4258bbba
Reviewed-on: https://go-review.googlesource.com/c/tools/+/170179
Run-TryBot: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index f37cba0..67bed47 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -209,6 +209,17 @@
delete(v.pcache.packages, pkgPath)
}
+// FindFile returns the file if the given URI is already a part of the view.
+func (v *View) FindFile(ctx context.Context, uri span.URI) *File {
+ v.mu.Lock()
+ defer v.mu.Unlock()
+ f, err := v.findFile(uri)
+ if err != nil {
+ return nil
+ }
+ return f
+}
+
// GetFile returns a File for the given URI. It will always succeed because it
// adds the file to the managed set if needed.
func (v *View) GetFile(ctx context.Context, uri span.URI) (source.File, error) {
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 0602bbd..a479e67 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -8,21 +8,23 @@
"context"
"sort"
+ "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"
)
func (s *Server) cacheAndDiagnose(ctx context.Context, uri span.URI, content string) error {
- if err := s.view.SetContent(ctx, uri, []byte(content)); err != nil {
+ view := s.findView(ctx, uri)
+ if err := view.SetContent(ctx, uri, []byte(content)); err != nil {
return err
}
go func() {
- ctx := s.view.BackgroundContext()
+ ctx := view.BackgroundContext()
if ctx.Err() != nil {
return
}
- reports, err := source.Diagnostics(ctx, s.view, uri)
+ reports, err := source.Diagnostics(ctx, view, uri)
if err != nil {
return // handle error?
}
@@ -31,7 +33,7 @@
defer s.undeliveredMu.Unlock()
for uri, diagnostics := range reports {
- if err := s.publishDiagnostics(ctx, uri, diagnostics); err != nil {
+ if err := s.publishDiagnostics(ctx, view, uri, diagnostics); err != nil {
if s.undelivered == nil {
s.undelivered = make(map[span.URI][]source.Diagnostic)
}
@@ -44,7 +46,7 @@
// Anytime we compute diagnostics, make sure to also send along any
// undelivered ones (only for remaining URIs).
for uri, diagnostics := range s.undelivered {
- s.publishDiagnostics(ctx, uri, diagnostics)
+ s.publishDiagnostics(ctx, view, uri, diagnostics)
// If we fail to deliver the same diagnostics twice, just give up.
delete(s.undelivered, uri)
@@ -53,8 +55,8 @@
return nil
}
-func (s *Server) publishDiagnostics(ctx context.Context, uri span.URI, diagnostics []source.Diagnostic) error {
- protocolDiagnostics, err := toProtocolDiagnostics(ctx, s.view, diagnostics)
+func (s *Server) publishDiagnostics(ctx context.Context, view *cache.View, uri span.URI, diagnostics []source.Diagnostic) error {
+ protocolDiagnostics, err := toProtocolDiagnostics(ctx, view, diagnostics)
if err != nil {
return err
}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 26da588..a49060d 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -74,7 +74,7 @@
log := xlog.New(xlog.StdSink{})
s := &Server{
- view: cache.NewView(ctx, log, "lsp_test", span.FileURI(cfg.Dir), &cfg),
+ views: []*cache.View{cache.NewView(ctx, log, "lsp_test", span.FileURI(cfg.Dir), &cfg)},
undelivered: make(map[span.URI][]source.Diagnostic),
}
// Do a first pass to collect special markers for completion.
@@ -121,7 +121,7 @@
t.Run("Diagnostics", func(t *testing.T) {
t.Helper()
- diagnosticsCount := expectedDiagnostics.test(t, s.view)
+ diagnosticsCount := expectedDiagnostics.test(t, s.views[0])
if goVersion111 { // TODO(rstambler): Remove this when we no longer support Go 1.10.
if diagnosticsCount != expectedDiagnosticsCount {
t.Errorf("got %v diagnostics expected %v", diagnosticsCount, expectedDiagnosticsCount)
@@ -425,7 +425,7 @@
}
continue
}
- f, m, err := newColumnMap(ctx, s.view, uri)
+ f, m, err := newColumnMap(ctx, s.findView(ctx, uri), uri)
if err != nil {
t.Error(err)
}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index f18debe..cbabbca 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -80,8 +80,7 @@
textDocumentSyncKind protocol.TextDocumentSyncKind
- viewMu sync.Mutex
- view *cache.View
+ views []*cache.View
// undelivered is a cache of any diagnostics that the server
// failed to deliver for some reason.
@@ -113,15 +112,6 @@
}
s.signatureHelpEnabled = true
- var rootURI span.URI
- if params.RootURI != "" {
- rootURI = span.NewURI(params.RootURI)
- }
- rootPath, err := rootURI.Filename()
- if err != nil {
- return nil, err
- }
-
// TODO(rstambler): Change this default to protocol.Incremental (or add a
// flag). Disabled for now to simplify debugging.
s.textDocumentSyncKind = protocol.Full
@@ -129,19 +119,39 @@
//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()
- //TODO:use workspace folders
- s.view = cache.NewView(viewContext, s.log, path.Base(string(rootURI)), rootURI, &packages.Config{
- Context: ctx,
- Dir: rootPath,
- 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,
- })
+ folders := params.WorkspaceFolders
+ if len(folders) == 0 {
+ if params.RootURI != "" {
+ folders = []protocol.WorkspaceFolder{{
+ URI: params.RootURI,
+ Name: path.Base(params.RootURI),
+ }}
+ } else {
+ // no folders and no root, single file mode
+ //TODO(iancottrell): not sure how to do single file mode yet
+ //issue: golang.org/issue/31168
+ 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 {
+ 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{
Capabilities: protocol.ServerCapabilities{
@@ -178,17 +188,19 @@
Method: "workspace/didChangeConfiguration",
}},
})
- config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
- Items: []protocol.ConfigurationItem{{
- ScopeURI: protocol.NewURI(s.view.Folder),
- Section: "gopls",
- }},
- })
- if err != nil {
- return err
- }
- if err := s.processConfig(config[0]); err != nil {
- return err
+ for _, view := range s.views {
+ config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
+ Items: []protocol.ConfigurationItem{{
+ ScopeURI: protocol.NewURI(view.Folder),
+ Section: "gopls",
+ }},
+ })
+ if err != nil {
+ return err
+ }
+ if err := s.processConfig(view, config[0]); err != nil {
+ return err
+ }
}
return nil
}
@@ -245,7 +257,9 @@
return change.Text, nil
}
- file, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ file, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
}
@@ -308,14 +322,15 @@
}
func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
- if err := s.view.SetContent(ctx, span.NewURI(params.TextDocument.URI), nil); err != nil {
- return err
- }
- return nil
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ return view.SetContent(ctx, uri, nil)
}
func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
- f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -342,7 +357,9 @@
}
func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
- f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -354,7 +371,7 @@
if err != nil {
return nil, err
}
- ident, err := source.Identifier(ctx, s.view, f, identRange.Start)
+ ident, err := source.Identifier(ctx, view, f, identRange.Start)
if err != nil {
return nil, err
}
@@ -381,7 +398,9 @@
}
func (s *Server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
- f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -401,7 +420,9 @@
}
func (s *Server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
- f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -413,7 +434,7 @@
if err != nil {
return nil, err
}
- ident, err := source.Identifier(ctx, s.view, f, rng.Start)
+ ident, err := source.Identifier(ctx, view, f, rng.Start)
if err != nil {
return nil, err
}
@@ -421,7 +442,7 @@
if err != nil {
return nil, err
}
- _, decM, err := newColumnMap(ctx, s.view, decSpan.URI())
+ _, decM, err := newColumnMap(ctx, view, decSpan.URI())
if err != nil {
return nil, err
}
@@ -433,7 +454,9 @@
}
func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
- f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -445,7 +468,7 @@
if err != nil {
return nil, err
}
- ident, err := source.Identifier(ctx, s.view, f, rng.Start)
+ ident, err := source.Identifier(ctx, view, f, rng.Start)
if err != nil {
return nil, err
}
@@ -453,7 +476,7 @@
if err != nil {
return nil, err
}
- _, identM, err := newColumnMap(ctx, s.view, identSpan.URI())
+ _, identM, err := newColumnMap(ctx, view, identSpan.URI())
if err != nil {
return nil, err
}
@@ -473,7 +496,9 @@
}
func (s *Server) DocumentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
- f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -493,7 +518,9 @@
}
func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
- f, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -502,7 +529,9 @@
}
func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
- _, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ _, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -510,7 +539,7 @@
if err != nil {
return nil, err
}
- edits, err := organizeImports(ctx, s.view, spn)
+ edits, err := organizeImports(ctx, view, spn)
if err != nil {
return nil, err
}
@@ -552,12 +581,16 @@
}
func (s *Server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
- spn := span.New(span.URI(params.TextDocument.URI), span.Point{}, span.Point{})
- return formatRange(ctx, s.view, spn)
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ spn := span.New(uri, span.Point{}, span.Point{})
+ return formatRange(ctx, view, spn)
}
func (s *Server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
- _, m, err := newColumnMap(ctx, s.view, span.NewURI(params.TextDocument.URI))
+ uri := span.NewURI(params.TextDocument.URI)
+ view := s.findView(ctx, uri)
+ _, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
@@ -565,7 +598,7 @@
if err != nil {
return nil, err
}
- return formatRange(ctx, s.view, spn)
+ return formatRange(ctx, view, spn)
}
func (s *Server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {
@@ -580,7 +613,7 @@
return nil, notImplemented("FoldingRanges")
}
-func (s *Server) processConfig(config interface{}) error {
+func (s *Server) processConfig(view *cache.View, config interface{}) error {
//TODO: we should probably store and process more of the config
c, ok := config.(map[string]interface{})
if !ok {
@@ -595,7 +628,7 @@
return fmt.Errorf("Invalid config gopls.env type %T", env)
}
for k, v := range menv {
- s.view.Config.Env = applyEnv(s.view.Config.Env, k, v)
+ view.Config.Env = applyEnv(view.Config.Env, k, v)
}
return nil
}
@@ -615,3 +648,26 @@
func notImplemented(method string) *jsonrpc2.Error {
return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method)
}
+
+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]
+}