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