Skip to content

Commit

Permalink
internal/lsp: start handling watched file change events
Browse files Browse the repository at this point in the history
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
  • Loading branch information
muirdm committed Aug 19, 2019
1 parent 6889da9 commit 2b6b6f2
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 15 deletions.
4 changes: 4 additions & 0 deletions internal/lsp/cache/session.go
Expand Up @@ -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
}
Expand Down
57 changes: 46 additions & 11 deletions internal/lsp/general.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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{})
Expand All @@ -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{})
Expand All @@ -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
Expand All @@ -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{})
Expand All @@ -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
}

Expand Down
10 changes: 8 additions & 2 deletions internal/lsp/server.go
Expand Up @@ -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
Expand Down Expand Up @@ -130,8 +132,12 @@ 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 {
if s.watchFileChanges {
return s.didChangeWatchedFiles(ctx, params)
} else {
return notImplemented("DidChangeWatchedFiles")
}
}

func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
Expand Down
13 changes: 11 additions & 2 deletions internal/lsp/source/view.go
Expand Up @@ -184,11 +184,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.
Expand All @@ -207,9 +211,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)

Expand Down
49 changes: 49 additions & 0 deletions internal/lsp/watched_files.go
@@ -0,0 +1,49 @@
// 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 {
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
}

0 comments on commit 2b6b6f2

Please sign in to comment.