From 80b1e8742cb01fdfe2d41b165c52c2288265a5f0 Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Mon, 6 May 2019 16:44:43 -0400 Subject: [PATCH] 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 TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/general.go | 41 ++++++------- internal/lsp/lsp_test.go | 1 + internal/lsp/server.go | 8 ++- internal/lsp/util.go | 27 --------- internal/lsp/workspace.go | 121 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 52 deletions(-) create mode 100644 internal/lsp/workspace.go diff --git a/internal/lsp/general.go b/internal/lsp/general.go index f3f860897b6..212a0390d99 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -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" @@ -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 != "" { @@ -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{ @@ -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 } @@ -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", }}, }) } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index bfc8fc7f59e..05bae65836b 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -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, }, diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 71a3d489e1b..0a23f69037d 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -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. @@ -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 { diff --git a/internal/lsp/util.go b/internal/lsp/util.go index e1c97adbba7..48d83fcc6c3 100644 --- a/internal/lsp/util.go +++ b/internal/lsp/util.go @@ -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 { diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go new file mode 100644 index 00000000000..1b90473c6db --- /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] +}