internal/lsp: process configuration options more thoroughly
Change-Id: Ic3e2f948f857c697564fa6ab02008444dd9392c7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/194458
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 1e7ac99..69f973f 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -7,7 +7,6 @@
import (
"bytes"
"context"
- "fmt"
"os"
"path"
@@ -17,7 +16,6 @@
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
- "golang.org/x/tools/internal/telemetry/tag"
errors "golang.org/x/xerrors"
)
@@ -35,30 +33,9 @@
options := s.session.Options()
defer func() { s.session.SetOptions(options) }()
- // TODO: Remove the option once we are certain there are no issues here.
- options.TextDocumentSyncKind = protocol.Incremental
- if opts, ok := params.InitializationOptions.(map[string]interface{}); ok {
- if opt, ok := opts["noIncrementalSync"].(bool); ok && opt {
- options.TextDocumentSyncKind = protocol.Full
- }
-
- // Check if user has enabled watching for file changes.
- setBool(&options.WatchFileChanges, opts, "watchFileChanges")
- }
-
- // Default to using synopsis as a default for hover information.
- options.HoverKind = source.SynopsisDocumentation
-
- options.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{
- source.Go: {
- protocol.SourceOrganizeImports: true,
- protocol.QuickFix: true,
- },
- source.Mod: {},
- source.Sum: {},
- }
-
- s.setClientCapabilities(&options, params.Capabilities)
+ //TODO: handle the options results
+ source.SetOptions(&options, params.InitializationOptions)
+ options.ForClientCapabilities(params.Capabilities)
s.pendingFolders = params.WorkspaceFolders
if len(s.pendingFolders) == 0 {
@@ -140,27 +117,6 @@
}, nil
}
-func (s *Server) setClientCapabilities(o *source.SessionOptions, caps protocol.ClientCapabilities) {
- // Check if the client supports snippets in completion items.
- o.InsertTextFormat = protocol.PlainTextTextFormat
- if caps.TextDocument.Completion.CompletionItem != nil &&
- caps.TextDocument.Completion.CompletionItem.SnippetSupport {
- o.InsertTextFormat = protocol.SnippetTextFormat
- }
- // Check if the client supports configuration messages.
- o.ConfigurationSupported = caps.Workspace.Configuration
- o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
- o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration
-
- // Check which types of content format are supported by this client.
- o.PreferredContentFormat = protocol.PlainText
- if len(caps.TextDocument.Hover.ContentFormat) > 0 {
- o.PreferredContentFormat = caps.TextDocument.Hover.ContentFormat[0]
- }
- // Check if the client supports only line folding.
- o.LineFoldingOnly = caps.TextDocument.FoldingRange.LineFoldingOnly
-}
-
func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error {
s.stateMu.Lock()
s.state = serverInitialized
@@ -216,8 +172,8 @@
return nil
}
-func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, options *source.SessionOptions, vo *source.ViewOptions) error {
- if !options.ConfigurationSupported {
+func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, vo *source.ViewOptions) error {
+ if !s.session.Options().ConfigurationSupported {
return nil
}
v := protocol.ParamConfig{
@@ -237,92 +193,12 @@
return err
}
for _, config := range configs {
- if err := s.processConfig(ctx, options, vo, config); err != nil {
- return err
- }
+ //TODO: handle the options results
+ source.SetOptions(vo, config)
}
return nil
}
-func (s *Server) processConfig(ctx context.Context, options *source.SessionOptions, vo *source.ViewOptions, config interface{}) error {
- // TODO: We should probably store and process more of the config.
- 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{})
- if !ok {
- return errors.Errorf("invalid config gopls.env type %T", env)
- }
- for k, v := range menv {
- vo.Env = append(vo.Env, fmt.Sprintf("%s=%s", k, v))
- }
- }
-
- // Get the build flags for the go/packages config.
- if buildFlags := c["buildFlags"]; buildFlags != nil {
- iflags, ok := buildFlags.([]interface{})
- if !ok {
- return errors.Errorf("invalid config gopls.buildFlags type %T", buildFlags)
- }
- flags := make([]string, 0, len(iflags))
- for _, flag := range iflags {
- flags = append(flags, fmt.Sprintf("%s", flag))
- }
- vo.BuildFlags = flags
- }
-
- // Set the hover kind.
- if hoverKind, ok := c["hoverKind"].(string); ok {
- switch hoverKind {
- case "NoDocumentation":
- options.HoverKind = source.NoDocumentation
- case "SingleLine":
- options.HoverKind = source.SingleLine
- case "SynopsisDocumentation":
- options.HoverKind = source.SynopsisDocumentation
- case "FullDocumentation":
- options.HoverKind = source.FullDocumentation
- case "Structured":
- options.HoverKind = source.Structured
- default:
- log.Error(ctx, "unsupported hover kind", nil, tag.Of("HoverKind", hoverKind))
- // The default value is already be set to synopsis.
- }
- }
-
- // Check if the user has explicitly disabled any analyses.
- if disabledAnalyses, ok := c["experimentalDisabledAnalyses"].([]interface{}); ok {
- options.DisabledAnalyses = make(map[string]struct{})
- for _, a := range disabledAnalyses {
- if a, ok := a.(string); ok {
- options.DisabledAnalyses[a] = struct{}{}
- }
- }
- }
-
- // Set completion options. For now, we allow disabling of completion documentation,
- // deep completion, and fuzzy matching.
- setBool(&options.Completion.Documentation, c, "wantCompletionDocumentation")
- setNotBool(&options.Completion.Deep, c, "disableDeepCompletion")
- setNotBool(&options.Completion.FuzzyMatching, c, "disableFuzzyMatching")
-
- // Unimported package completion is still experimental, so not enabled by default.
- setBool(&options.Completion.Unimported, c, "wantUnimportedCompletions")
-
- // If the user wants placeholders for autocompletion results.
- setBool(&options.Completion.Placeholders, c, "usePlaceholders")
-
- return nil
-}
-
func (s *Server) shutdown(ctx context.Context) error {
s.stateMu.Lock()
defer s.stateMu.Unlock()
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index a2b58d4..062f020 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -5,16 +5,20 @@
package source
import (
+ "fmt"
"os"
"golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/telemetry/tag"
+ errors "golang.org/x/xerrors"
)
var (
DefaultSessionOptions = SessionOptions{
- TextDocumentSyncKind: protocol.Incremental,
- HoverKind: SynopsisDocumentation,
- InsertTextFormat: protocol.PlainTextTextFormat,
+ TextDocumentSyncKind: protocol.Incremental,
+ HoverKind: SynopsisDocumentation,
+ InsertTextFormat: protocol.PlainTextTextFormat,
+ PreferredContentFormat: protocol.PlainText,
SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{
Go: {
protocol.SourceOrganizeImports: true,
@@ -50,6 +54,7 @@
SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool
+ // TODO: Remove the option once we are certain there are no issues here.
TextDocumentSyncKind protocol.TextDocumentSyncKind
Completion CompletionOptions
@@ -76,6 +81,10 @@
type HoverKind int
+type Options interface {
+ set(name string, value interface{}) OptionResult
+}
+
const (
SingleLine = HoverKind(iota)
NoDocumentation
@@ -89,3 +98,173 @@
// This should only be used by clients that support this behavior.
Structured
)
+
+type OptionResults []OptionResult
+
+type OptionResult struct {
+ Name string
+ Value interface{}
+ State OptionState
+ Error error
+}
+
+type OptionState int
+
+const (
+ OptionHandled = OptionState(iota)
+ OptionDeprecated
+ OptionUnexpected
+)
+
+func SetOptions(options Options, opts interface{}) OptionResults {
+ var results OptionResults
+ switch opts := opts.(type) {
+ case nil:
+ case map[string]interface{}:
+ for name, value := range opts {
+ results = append(results, options.set(name, value))
+ }
+ default:
+ results = append(results, OptionResult{
+ Value: opts,
+ Error: errors.Errorf("Invalid options type %T", opts),
+ })
+ }
+ return results
+}
+
+func (o *SessionOptions) ForClientCapabilities(caps protocol.ClientCapabilities) {
+ // Check if the client supports snippets in completion items.
+ if caps.TextDocument.Completion.CompletionItem != nil &&
+ caps.TextDocument.Completion.CompletionItem.SnippetSupport {
+ o.InsertTextFormat = protocol.SnippetTextFormat
+ }
+ // Check if the client supports configuration messages.
+ o.ConfigurationSupported = caps.Workspace.Configuration
+ o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
+ o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration
+
+ // Check which types of content format are supported by this client.
+ if len(caps.TextDocument.Hover.ContentFormat) > 0 {
+ o.PreferredContentFormat = caps.TextDocument.Hover.ContentFormat[0]
+ }
+ // Check if the client supports only line folding.
+ o.LineFoldingOnly = caps.TextDocument.FoldingRange.LineFoldingOnly
+}
+
+func (o *SessionOptions) set(name string, value interface{}) OptionResult {
+ result := OptionResult{Name: name, Value: value}
+ switch name {
+ case "noIncrementalSync":
+ if v, ok := result.asBool(); ok && v {
+ o.TextDocumentSyncKind = protocol.Full
+ }
+ case "watchFileChanges":
+ result.setBool(&o.WatchFileChanges)
+ case "wantCompletionDocumentation":
+ result.setBool(&o.Completion.Documentation)
+ case "usePlaceholders":
+ result.setBool(&o.Completion.Placeholders)
+ case "disableDeepCompletion":
+ result.setNotBool(&o.Completion.Deep)
+ case "disableFuzzyMatching":
+ result.setNotBool(&o.Completion.FuzzyMatching)
+ case "wantUnimportedCompletions":
+ result.setBool(&o.Completion.Unimported)
+
+ case "hoverKind":
+ hoverKind, ok := value.(string)
+ if !ok {
+ result.errorf("Invalid type %T for string option %q", value, name)
+ break
+ }
+ switch hoverKind {
+ case "NoDocumentation":
+ o.HoverKind = NoDocumentation
+ case "SingleLine":
+ o.HoverKind = SingleLine
+ case "SynopsisDocumentation":
+ o.HoverKind = SynopsisDocumentation
+ case "FullDocumentation":
+ o.HoverKind = FullDocumentation
+ case "Structured":
+ o.HoverKind = Structured
+ default:
+ result.errorf("Unsupported hover kind", tag.Of("HoverKind", hoverKind))
+ }
+
+ case "experimentalDisabledAnalyses":
+ disabledAnalyses, ok := value.([]interface{})
+ if !ok {
+ result.errorf("Invalid type %T for []string option %q", value, name)
+ break
+ }
+ o.DisabledAnalyses = make(map[string]struct{})
+ for _, a := range disabledAnalyses {
+ o.DisabledAnalyses[fmt.Sprint(a)] = struct{}{}
+ }
+
+ case "wantSuggestedFixes":
+ result.State = OptionDeprecated
+
+ default:
+ return o.DefaultViewOptions.set(name, value)
+ }
+ return result
+}
+
+func (o *ViewOptions) set(name string, value interface{}) OptionResult {
+ result := OptionResult{Name: name, Value: value}
+ switch name {
+ case "env":
+ menv, ok := value.(map[string]interface{})
+ if !ok {
+ result.errorf("invalid config gopls.env type %T", value)
+ break
+ }
+ for k, v := range menv {
+ o.Env = append(o.Env, fmt.Sprintf("%s=%s", k, v))
+ }
+
+ case "buildFlags":
+ iflags, ok := value.([]interface{})
+ if !ok {
+ result.errorf("invalid config gopls.buildFlags type %T", value)
+ break
+ }
+ flags := make([]string, 0, len(iflags))
+ for _, flag := range iflags {
+ flags = append(flags, fmt.Sprintf("%s", flag))
+ }
+ o.BuildFlags = flags
+
+ default:
+ result.State = OptionUnexpected
+ }
+ return result
+}
+
+func (r *OptionResult) errorf(msg string, values ...interface{}) {
+ r.Error = errors.Errorf(msg, values...)
+}
+
+func (r *OptionResult) asBool() (bool, bool) {
+ b, ok := r.Value.(bool)
+ if !ok {
+ r.errorf("Invalid type %T for bool option %q", r.Value, r.Name)
+ return false, false
+ }
+ return b, true
+}
+
+func (r *OptionResult) setBool(b *bool) {
+ if v, ok := r.asBool(); ok {
+ *b = v
+ }
+}
+
+func (r *OptionResult) setNotBool(b *bool) {
+ if v, ok := r.asBool(); ok {
+ *b = !v
+ }
+}
diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go
index bfbf065..27bdc16 100644
--- a/internal/lsp/workspace.go
+++ b/internal/lsp/workspace.go
@@ -38,11 +38,8 @@
return errors.Errorf("addView called before server initialized")
}
- options := s.session.Options()
- viewOptions := options.DefaultViewOptions
- //TODO: take this out, we only allow new session options here
- defer func() { s.session.SetOptions(options) }()
- s.fetchConfig(ctx, name, uri, &options, &viewOptions)
+ viewOptions := s.session.Options().DefaultViewOptions
+ s.fetchConfig(ctx, name, uri, &viewOptions)
s.session.NewView(ctx, name, uri, viewOptions)
return nil
}