Skip to content

Commit

Permalink
internal/lsp: support dynamic workspace folder changes
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
ianthehat committed May 10, 2019
1 parent 4ca280b commit 80b1e87
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 52 deletions.
41 changes: 19 additions & 22 deletions internal/lsp/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@ package lsp
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"
Expand All @@ -39,9 +35,6 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara

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 != "" {
Expand All @@ -56,24 +49,11 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
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{
Expand All @@ -96,6 +76,20 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
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
}
Expand Down Expand Up @@ -124,6 +118,9 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa
Registrations: []protocol.Registration{{
ID: "workspace/didChangeConfiguration",
Method: "workspace/didChangeConfiguration",
}, {
ID: "workspace/didChangeWorkspaceFolders",
Method: "workspace/didChangeWorkspaceFolders",
}},
})
}
Expand Down
1 change: 1 addition & 0 deletions internal/lsp/lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
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,
},
Expand Down
8 changes: 5 additions & 3 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ type Server struct {

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.
Expand Down Expand Up @@ -110,8 +112,8 @@ func (s *Server) Exit(ctx context.Context) error {

// 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 {
Expand Down
27 changes: 0 additions & 27 deletions internal/lsp/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,12 @@ package lsp
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 {
Expand Down
121 changes: 121 additions & 0 deletions internal/lsp/workspace.go
Original file line number Diff line number Diff line change
@@ -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]
}

0 comments on commit 80b1e87

Please sign in to comment.