From 1f0dd0289ffc9930aee5bd74be59b58fbb7e857c Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Mon, 19 Aug 2019 09:53:51 -0700 Subject: [PATCH] internal/lsp: start handling watched file change events Now we register for and handle didChangeWatchedFiles "change" events. We don't handle "create" or "delete" yet. When a file changes on disk, there are two basic cases. If the editor has the file open, we want to ignore the change since we need to respect the file contents in the editor. If the file isn't open in the editor then we need to re-type check (and re-diagnose) any packages it belongs to. We will need special handling of go.mod changes, but start with just *.go files for now. I'm putting the new behavior behind an initialization flag while it is under development. Updates golang/go#31553 Change-Id: I81a767ebe12f5f82657752dcdfb069c5820cbaa0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/190857 Reviewed-by: Ian Cottrell Run-TryBot: Ian Cottrell TryBot-Result: Gobot Gobot --- internal/lsp/cache/session.go | 4 +++ internal/lsp/general.go | 57 ++++++++++++++++++++++++++++------- internal/lsp/server.go | 6 ++-- internal/lsp/source/view.go | 13 ++++++-- internal/lsp/watched_files.go | 53 ++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 internal/lsp/watched_files.go diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 1c133f3076..b092631ea2 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -327,6 +327,10 @@ func (s *session) buildOverlay() map[string][]byte { return overlays } +func (s *session) DidChangeOutOfBand(uri span.URI) { + s.filesWatchMap.Notify(uri) +} + func (o *overlay) FileSystem() source.FileSystem { return o.session } diff --git a/internal/lsp/general.go b/internal/lsp/general.go index 0da3020bd5..b946abe5d4 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -38,6 +38,9 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara if opt, ok := opts["noIncrementalSync"].(bool); ok && opt { s.textDocumentSyncKind = protocol.Full } + + // Check if user has enabled watching for file changes. + s.watchFileChanges, _ = opts["watchFileChanges"].(bool) } // Default to using synopsis as a default for hover information. @@ -126,6 +129,7 @@ func (s *Server) setClientCapabilities(caps protocol.ClientCapabilities) { // Check if the client supports configuration messages. s.configurationSupported = caps.Workspace.Configuration s.dynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration + s.dynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration // Check which types of content format are supported by this client. s.preferredContentFormat = protocol.PlainText @@ -139,18 +143,40 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa s.state = serverInitialized s.stateMu.Unlock() - if s.configurationSupported { - if s.dynamicConfigurationSupported { - s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ - Registrations: []protocol.Registration{{ - ID: "workspace/didChangeConfiguration", - Method: "workspace/didChangeConfiguration", - }, { - ID: "workspace/didChangeWorkspaceFolders", - Method: "workspace/didChangeWorkspaceFolders", + var registrations []protocol.Registration + if s.configurationSupported && s.dynamicConfigurationSupported { + registrations = append(registrations, + protocol.Registration{ + ID: "workspace/didChangeConfiguration", + Method: "workspace/didChangeConfiguration", + }, + protocol.Registration{ + ID: "workspace/didChangeWorkspaceFolders", + Method: "workspace/didChangeWorkspaceFolders", + }, + ) + } + + if s.watchFileChanges && s.dynamicWatchedFilesSupported { + registrations = append(registrations, protocol.Registration{ + ID: "workspace/didChangeWatchedFiles", + Method: "workspace/didChangeWatchedFiles", + RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{ + Watchers: []protocol.FileSystemWatcher{{ + GlobPattern: "**/*.go", + Kind: float64(protocol.WatchChange), }}, - }) - } + }, + }) + } + + if len(registrations) > 0 { + s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ + Registrations: registrations, + }) + } + + if s.configurationSupported { for _, view := range s.session.Views() { if err := s.fetchConfig(ctx, view); err != nil { return err @@ -190,10 +216,12 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int if config == nil { return nil // ignore error if you don't have a config } + c, ok := config.(map[string]interface{}) if !ok { return errors.Errorf("invalid config gopls type %T", config) } + // Get the environment for the go/packages config. if env := c["env"]; env != nil { menv, ok := env.(map[string]interface{}) @@ -206,6 +234,7 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int } view.SetEnv(env) } + // Get the build flags for the go/packages config. if buildFlags := c["buildFlags"]; buildFlags != nil { iflags, ok := buildFlags.([]interface{}) @@ -218,6 +247,7 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int } view.SetBuildFlags(flags) } + // Check if the user wants documentation in completion items. if wantCompletionDocumentation, ok := c["wantCompletionDocumentation"].(bool); ok { s.wantCompletionDocumentation = wantCompletionDocumentation @@ -244,10 +274,12 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int // The default value is already be set to synopsis. } } + // Check if the user wants to see suggested fixes from go/analysis. if wantSuggestedFixes, ok := c["wantSuggestedFixes"].(bool); ok { s.wantSuggestedFixes = wantSuggestedFixes } + // Check if the user has explicitly disabled any analyses. if disabledAnalyses, ok := c["experimentalDisabledAnalyses"].([]interface{}); ok { s.disabledAnalyses = make(map[string]struct{}) @@ -257,14 +289,17 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int } } } + // Check if deep completions are enabled. if useDeepCompletions, ok := c["useDeepCompletions"].(bool); ok { s.useDeepCompletions = useDeepCompletions } + // Check if want unimported package completions. if wantUnimportedCompletions, ok := c["wantUnimportedCompletions"].(bool); ok { s.wantUnimportedCompletions = wantUnimportedCompletions } + return nil } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 9c5aea9563..f542a8e03f 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -81,11 +81,13 @@ type Server struct { usePlaceholders bool hoverKind hoverKind useDeepCompletions bool + watchFileChanges bool wantCompletionDocumentation bool wantUnimportedCompletions bool insertTextFormat protocol.InsertTextFormat configurationSupported bool dynamicConfigurationSupported bool + dynamicWatchedFilesSupported bool preferredContentFormat protocol.MarkupKind disabledAnalyses map[string]struct{} wantSuggestedFixes bool @@ -130,8 +132,8 @@ func (s *Server) DidChangeConfiguration(context.Context, *protocol.DidChangeConf return notImplemented("DidChangeConfiguration") } -func (s *Server) DidChangeWatchedFiles(context.Context, *protocol.DidChangeWatchedFilesParams) error { - return notImplemented("DidChangeWatchedFiles") +func (s *Server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { + return s.didChangeWatchedFiles(ctx, params) } func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 4a0c073e50..b3a9288e26 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -181,11 +181,15 @@ type Session interface { // DidClose is invoked each time an open file is closed in the editor. DidClose(uri span.URI) - // IsOpen can be called to check if the editor has a file currently open. + // IsOpen returns whether the editor currently has a file open. IsOpen(uri span.URI) bool // Called to set the effective contents of a file from this session. SetOverlay(uri span.URI, data []byte) (wasFirstChange bool) + + // DidChangeOutOfBand is called when a file under the root folder + // changes. The file is not necessarily open in the editor. + DidChangeOutOfBand(uri span.URI) } // View represents a single workspace. @@ -204,9 +208,14 @@ type View interface { // BuiltinPackage returns the ast for the special "builtin" package. BuiltinPackage() *ast.Package - // GetFile returns the file object for a given uri. + // GetFile returns the file object for a given URI, initializing it + // if it is not already part of the view. GetFile(ctx context.Context, uri span.URI) (File, error) + // FindFile returns the file object for a given URI if it is + // already part of the view. + FindFile(ctx context.Context, uri span.URI) File + // Called to set the effective contents of a file from this view. SetContent(ctx context.Context, uri span.URI, content []byte) (wasFirstChange bool, err error) diff --git a/internal/lsp/watched_files.go b/internal/lsp/watched_files.go new file mode 100644 index 0000000000..37c82e43b8 --- /dev/null +++ b/internal/lsp/watched_files.go @@ -0,0 +1,53 @@ +// 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" + + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/telemetry/log" + "golang.org/x/tools/internal/telemetry/tag" +) + +func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { + if !s.watchFileChanges { + return nil + } + + for _, change := range params.Changes { + uri := span.NewURI(change.URI) + + switch change.Type { + case protocol.Changed: + view := s.session.ViewOf(uri) + + // If we have never seen this file before, there is nothing to do. + if view.FindFile(ctx, uri) == nil { + break + } + + log.Print(ctx, "watched file changed", tag.Of("uri", uri)) + + // If client has this file open, don't do anything. The client's contents + // must remain the source of truth. + if s.session.IsOpen(uri) { + break + } + + s.session.DidChangeOutOfBand(uri) + + // Refresh diagnostics to reflect updated file contents. + s.Diagnostics(ctx, view, uri) + case protocol.Created: + log.Print(ctx, "watched file created", tag.Of("uri", uri)) + case protocol.Deleted: + log.Print(ctx, "watched file deleted", tag.Of("uri", uri)) + } + } + + return nil +}