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
 }