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 <iancottrell@google.com>
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
Muir Manders 2019-08-19 09:53:51 -07:00 committed by Rebecca Stambler
parent 11cc3c157e
commit 1f0dd0289f
5 changed files with 118 additions and 15 deletions

View File

@ -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
}

View File

@ -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{{
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
}

View File

@ -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) {

View File

@ -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)

View File

@ -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
}