From 8889bfc21e268a2baf5e076ae225d2ec7da8856d Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Thu, 28 Mar 2019 08:49:42 -0400 Subject: [PATCH] internal/lsp: wire up configuration This connects up the configuration message, and uses it to allow the client to set the environment in the config passed to packages.Load Change-Id: I75e03c01c74e9b11c8b4c47b9cbdd0574cddf778 Reviewed-on: https://go-review.googlesource.com/c/tools/+/169704 Run-TryBot: Ian Cottrell TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/cache/view.go | 14 ++++-- internal/lsp/cmd/definition.go | 2 +- internal/lsp/diagnostics.go | 3 ++ internal/lsp/lsp_test.go | 2 +- internal/lsp/server.go | 89 +++++++++++++++++++++++++++++----- 5 files changed, 93 insertions(+), 17 deletions(-) diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 1868852ff5..3f3675c963 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -27,6 +27,12 @@ type View struct { // should be stopped. cancel context.CancelFunc + // Name is the user visible name of this view. + Name string + + // Folder is the root of this view. + Folder span.URI + // Config is the configuration used for the view's interaction with the // go/packages API. It is shared across all views. Config packages.Config @@ -72,13 +78,14 @@ type entry struct { ready chan struct{} // closed to broadcast ready condition } -func NewView(config *packages.Config) *View { +func NewView(name string, folder span.URI, config *packages.Config) *View { ctx, cancel := context.WithCancel(context.Background()) - - return &View{ + v := &View{ backgroundCtx: ctx, cancel: cancel, Config: *config, + Name: name, + Folder: folder, filesByURI: make(map[span.URI]*File), filesByBase: make(map[string][]*File), contentChanges: make(map[span.URI]func()), @@ -89,6 +96,7 @@ func NewView(config *packages.Config) *View { packages: make(map[string]*entry), }, } + return v } func (v *View) BackgroundContext() context.Context { diff --git a/internal/lsp/cmd/definition.go b/internal/lsp/cmd/definition.go index b7d41ceb8b..1b8de1362a 100644 --- a/internal/lsp/cmd/definition.go +++ b/internal/lsp/cmd/definition.go @@ -61,7 +61,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { if len(args) != 1 { return tool.CommandLineErrorf("definition expects 1 argument") } - view := cache.NewView(&d.query.app.Config) + view := cache.NewView("definition_test", span.FileURI(d.query.app.Config.Dir), &d.query.app.Config) from := span.Parse(args[0]) f, err := view.GetFile(ctx, from.URI()) if err != nil { diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 9aa3c7aae0..f7838e9537 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -18,6 +18,9 @@ func (s *server) cacheAndDiagnose(ctx context.Context, uri span.URI, content str return err } go func() { + //TODO: this is an ugly hack to make the diagnostics call happen after the + // configuration is collected, we need to rewrite all the concurrency + <-s.configured ctx := s.view.BackgroundContext() if ctx.Err() != nil { return diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 46f604a47e..b1e46fc4ec 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -71,7 +71,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { } s := &server{ - view: cache.NewView(&cfg), + view: cache.NewView("lsp_test", span.FileURI(cfg.Dir), &cfg), } // Do a first pass to collect special markers for completion. if err := exported.Expect(map[string]interface{}{ diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 68fdfcf78a..3296b4f825 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -13,6 +13,8 @@ import ( "go/token" "net" "os" + "path" + "strings" "sync" "golang.org/x/tools/go/packages" @@ -26,7 +28,9 @@ import ( // RunServer starts an LSP server on the supplied stream, and waits until the // stream is closed. func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) error { - s := &server{} + s := &server{ + configured: make(chan struct{}), + } conn, client := protocol.RunServer(ctx, stream, s, opts...) s.client = client return conn.Wait(ctx) @@ -41,7 +45,6 @@ func RunServerOnPort(ctx context.Context, port int, opts ...interface{}) error { // RunServerOnPort starts an LSP server on the given port and does not exit. // This function exists for debugging purposes. func RunServerOnAddress(ctx context.Context, addr string, opts ...interface{}) error { - s := &server{} ln, err := net.Listen("tcp", addr) if err != nil { return err @@ -52,11 +55,7 @@ func RunServerOnAddress(ctx context.Context, addr string, opts ...interface{}) e return err } stream := jsonrpc2.NewHeaderStream(conn, conn) - go func() { - conn, client := protocol.RunServer(ctx, stream, s, opts...) - s.client = client - conn.Wait(ctx) - }() + go RunServer(ctx, stream, opts...) } } @@ -71,8 +70,9 @@ type server struct { textDocumentSyncKind protocol.TextDocumentSyncKind - viewMu sync.Mutex - view *cache.View + view *cache.View + + configured chan struct{} } func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) { @@ -103,9 +103,11 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara // flag). Disabled for now to simplify debugging. s.textDocumentSyncKind = protocol.Full - s.view = cache.NewView(&packages.Config{ + //TODO:use workspace folders + s.view = cache.NewView(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), @@ -143,8 +145,32 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara }, nil } -func (s *server) Initialized(context.Context, *protocol.InitializedParams) error { - return nil // ignore +func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { + go func() { + // we hae to do this in a go routine to unblock the jsonrpc processor + // but we also have to block all calls to packages.Load until this is done + // TODO: we need to rewrite all the concurrency handling hin the server + defer func() { close(s.configured) }() + s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ + Registrations: []protocol.Registration{{ + ID: "workspace/didChangeConfiguration", + 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 { + s.Error(err) + } + if err := s.processConfig(config[0]); err != nil { + s.Error(err) + } + }() + return nil } func (s *server) Shutdown(context.Context) error { @@ -532,6 +558,45 @@ func (s *server) FoldingRanges(context.Context, *protocol.FoldingRangeParams) ([ return nil, notImplemented("FoldingRanges") } +func (s *server) Error(err error) { + s.client.LogMessage(context.Background(), &protocol.LogMessageParams{ + Type: protocol.Error, + Message: fmt.Sprint(err), + }) +} + +func (s *server) processConfig(config interface{}) error { + //TODO: we should probably store and process more of the config + c, ok := config.(map[string]interface{}) + if !ok { + return fmt.Errorf("Invalid config gopls type %T", config) + } + env := c["env"] + if env == nil { + return nil + } + menv, ok := env.(map[string]interface{}) + if !ok { + 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) + } + return nil +} + +func applyEnv(env []string, k string, v interface{}) []string { + prefix := k + "=" + value := prefix + fmt.Sprint(v) + for i, s := range env { + if strings.HasPrefix(s, prefix) { + env[i] = value + return env + } + } + return append(env, value) +} + func notImplemented(method string) *jsonrpc2.Error { return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method) }