internal/lsp: merge session and view options into one

This fixes the issue of config options not being applied.
Also, handle config errors and deprecation by showing a message to the
user.

Change-Id: I850d5303a7a1e301c97324060a87929710ee6700
Reviewed-on: https://go-review.googlesource.com/c/tools/+/194682
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 44dae99..b20712e 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -76,7 +76,7 @@
 	s := &session{
 		cache:         c,
 		id:            strconv.FormatInt(index, 10),
-		options:       source.DefaultSessionOptions,
+		options:       source.DefaultOptions,
 		overlays:      make(map[span.URI]*overlay),
 		filesWatchMap: NewWatchMap(),
 	}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 6052976..ac004c6 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -28,7 +28,7 @@
 	cache *cache
 	id    string
 
-	options source.SessionOptions
+	options source.Options
 
 	viewMu  sync.Mutex
 	views   []*view
@@ -56,11 +56,11 @@
 	unchanged bool
 }
 
-func (s *session) Options() source.SessionOptions {
+func (s *session) Options() source.Options {
 	return s.options
 }
 
-func (s *session) SetOptions(options source.SessionOptions) {
+func (s *session) SetOptions(options source.Options) {
 	s.options = options
 }
 
@@ -79,7 +79,7 @@
 	return s.cache
 }
 
-func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.ViewOptions) source.View {
+func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) source.View {
 	index := atomic.AddInt64(&viewIndex, 1)
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 3a2f93f..5168ebd 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -30,7 +30,7 @@
 	session *session
 	id      string
 
-	options source.ViewOptions
+	options source.Options
 
 	// mu protects all mutable state of the view.
 	mu sync.Mutex
@@ -113,7 +113,7 @@
 	return v.folder
 }
 
-func (v *view) Options() source.ViewOptions {
+func (v *view) Options() source.Options {
 	return v.options
 }
 
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 3052840..ce04ca1 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -298,8 +298,7 @@
 			env[l[0]] = l[1]
 		}
 		results[i] = map[string]interface{}{
-			"env":           env,
-			"noDocsOnHover": true,
+			"env": env,
 		}
 	}
 	return results, nil
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index 7b33063..df2659f 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -51,7 +51,7 @@
 	}, nil
 }
 
-func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range, options source.SessionOptions) []protocol.CompletionItem {
+func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range, options source.Options) []protocol.CompletionItem {
 	var (
 		items                  = make([]protocol.CompletionItem, 0, len(candidates))
 		numDeepCompletionsSeen int
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 69f973f..69f262a 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -7,6 +7,7 @@
 import (
 	"bytes"
 	"context"
+	"fmt"
 	"os"
 	"path"
 
@@ -33,7 +34,7 @@
 	options := s.session.Options()
 	defer func() { s.session.SetOptions(options) }()
 
-	//TODO: handle the options results
+	// TODO: Handle results here.
 	source.SetOptions(&options, params.InitializationOptions)
 	options.ForClientCapabilities(params.Capabilities)
 
@@ -172,7 +173,7 @@
 	return nil
 }
 
-func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, vo *source.ViewOptions) error {
+func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, o *source.Options) error {
 	if !s.session.Options().ConfigurationSupported {
 		return nil
 	}
@@ -193,8 +194,31 @@
 		return err
 	}
 	for _, config := range configs {
-		//TODO: handle the options results
-		source.SetOptions(vo, config)
+		results := source.SetOptions(o, config)
+		for _, result := range results {
+			if result.Error != nil {
+				s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+					Type:    protocol.Error,
+					Message: result.Error.Error(),
+				})
+			}
+			switch result.State {
+			case source.OptionUnexpected:
+				s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+					Type:    protocol.Error,
+					Message: fmt.Sprintf("unexpected config %s", result.Name),
+				})
+			case source.OptionDeprecated:
+				msg := fmt.Sprintf("config %s is deprecated", result.Name)
+				if result.Replacement != "" {
+					msg = fmt.Sprintf("%s, use %s instead", msg, result.Replacement)
+				}
+				s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+					Type:    protocol.Warning,
+					Message: msg,
+				})
+			}
+		}
 	}
 	return nil
 }
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 1b4b684..ce03f67 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -61,9 +61,8 @@
 	}
 	options.HoverKind = source.SynopsisDocumentation
 	session.SetOptions(options)
-	vo := options.DefaultViewOptions
-	vo.Env = data.Config.Env
-	session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), vo)
+	options.Env = data.Config.Env
+	session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options)
 	for filename, content := range data.Config.Overlay {
 		session.SetOverlay(span.FileURI(filename), content)
 	}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 120e170..2501a45 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -14,7 +14,8 @@
 )
 
 var (
-	DefaultSessionOptions = SessionOptions{
+	DefaultOptions = Options{
+		Env:                    os.Environ(),
 		TextDocumentSyncKind:   protocol.Incremental,
 		HoverKind:              SynopsisDocumentation,
 		InsertTextFormat:       protocol.PlainTextTextFormat,
@@ -32,15 +33,17 @@
 			Deep:          false,
 			FuzzyMatching: false,
 		},
-		DefaultViewOptions: ViewOptions{
-			Env: os.Environ(),
-		},
 	}
 )
 
-type SessionOptions struct {
-	Env              []string
-	BuildFlags       []string
+type Options struct {
+
+	// Env is the current set of environment overrides on this view.
+	Env []string
+
+	// BuildFlags is used to adjust the build flags applied to the view.
+	BuildFlags []string
+
 	HoverKind        HoverKind
 	DisabledAnalyses map[string]struct{}
 
@@ -58,16 +61,6 @@
 	TextDocumentSyncKind protocol.TextDocumentSyncKind
 
 	Completion CompletionOptions
-
-	DefaultViewOptions ViewOptions
-}
-
-type ViewOptions struct {
-	// Env is the current set of environment overrides on this view.
-	Env []string
-
-	// BuildFlags is used to adjust the build flags applied to the view.
-	BuildFlags []string
 }
 
 type CompletionOptions struct {
@@ -81,10 +74,6 @@
 
 type HoverKind int
 
-type Options interface {
-	set(name string, value interface{}) OptionResult
-}
-
 const (
 	SingleLine = HoverKind(iota)
 	NoDocumentation
@@ -104,8 +93,10 @@
 type OptionResult struct {
 	Name  string
 	Value interface{}
-	State OptionState
 	Error error
+
+	State       OptionState
+	Replacement string
 }
 
 type OptionState int
@@ -116,7 +107,7 @@
 	OptionUnexpected
 )
 
-func SetOptions(options Options, opts interface{}) OptionResults {
+func SetOptions(options *Options, opts interface{}) OptionResults {
 	var results OptionResults
 	switch opts := opts.(type) {
 	case nil:
@@ -133,7 +124,7 @@
 	return results
 }
 
-func (o *SessionOptions) ForClientCapabilities(caps protocol.ClientCapabilities) {
+func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) {
 	// Check if the client supports snippets in completion items.
 	if caps.TextDocument.Completion.CompletionItem != nil &&
 		caps.TextDocument.Completion.CompletionItem.SnippetSupport {
@@ -152,24 +143,46 @@
 	o.LineFoldingOnly = caps.TextDocument.FoldingRange.LineFoldingOnly
 }
 
-func (o *SessionOptions) set(name string, value interface{}) OptionResult {
+func (o *Options) 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
+
 	case "noIncrementalSync":
 		if v, ok := result.asBool(); ok && v {
 			o.TextDocumentSyncKind = protocol.Full
 		}
 	case "watchFileChanges":
 		result.setBool(&o.WatchFileChanges)
-	case "wantCompletionDocumentation":
+	case "completionDocumentation":
 		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":
+	case "deepCompletion":
+		result.setBool(&o.Completion.Deep)
+	case "fuzzyMatching":
+		result.setBool(&o.Completion.FuzzyMatching)
+	case "completeUnimported":
 		result.setBool(&o.Completion.Unimported)
 
 	case "hoverKind":
@@ -204,39 +217,25 @@
 			o.DisabledAnalyses[fmt.Sprint(a)] = struct{}{}
 		}
 
+	// Deprecated settings.
 	case "wantSuggestedFixes":
 		result.State = OptionDeprecated
 
-	default:
-		return o.DefaultViewOptions.set(name, value)
-	}
-	return result
-}
+	case "disableDeepCompletion":
+		result.State = OptionDeprecated
+		result.Replacement = "deepCompletion"
 
-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 "disableFuzzyMatching":
+		result.State = OptionDeprecated
+		result.Replacement = "fuzzyMatching"
 
-	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
+	case "wantCompletionDocumentation":
+		result.State = OptionDeprecated
+		result.Replacement = "completionDocumentation"
+
+	case "wantUnimportedCompletions":
+		result.State = OptionDeprecated
+		result.Replacement = "completeUnimported"
 
 	default:
 		result.State = OptionUnexpected
@@ -262,9 +261,3 @@
 		*b = v
 	}
 }
-
-func (r *OptionResult) setNotBool(b *bool) {
-	if v, ok := r.asBool(); ok {
-		*b = !v
-	}
-}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 9734b79..36de197 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -49,10 +49,9 @@
 	cache := cache.New()
 	session := cache.NewSession(ctx)
 	options := session.Options()
-	vo := options.DefaultViewOptions
-	vo.Env = data.Config.Env
+	options.Env = data.Config.Env
 	r := &runner{
-		view: session.NewView(ctx, "source_test", span.FileURI(data.Config.Dir), vo),
+		view: session.NewView(ctx, "source_test", span.FileURI(data.Config.Dir), options),
 		data: data,
 		ctx:  ctx,
 	}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 0480cc2..a9c92b3 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -155,7 +155,7 @@
 // A session may have many active views at any given time.
 type Session interface {
 	// NewView creates a new View and returns it.
-	NewView(ctx context.Context, name string, folder span.URI, options ViewOptions) View
+	NewView(ctx context.Context, name string, folder span.URI, options Options) View
 
 	// Cache returns the cache that created this session.
 	Cache() Cache
@@ -196,10 +196,10 @@
 	DidChangeOutOfBand(ctx context.Context, f GoFile, change protocol.FileChangeType)
 
 	// Options returns a copy of the SessionOptions for this session.
-	Options() SessionOptions
+	Options() Options
 
 	// SetOptions sets the options of this session to new values.
-	SetOptions(SessionOptions)
+	SetOptions(Options)
 }
 
 // View represents a single workspace.
@@ -246,7 +246,7 @@
 	RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error, opts *imports.Options) error
 
 	// Options returns a copy of the ViewOptions for this view.
-	Options() ViewOptions
+	Options() Options
 }
 
 // File represents a source file of any type.
diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go
index 27bdc16..42a252a 100644
--- a/internal/lsp/workspace.go
+++ b/internal/lsp/workspace.go
@@ -38,8 +38,8 @@
 		return errors.Errorf("addView called before server initialized")
 	}
 
-	viewOptions := s.session.Options().DefaultViewOptions
-	s.fetchConfig(ctx, name, uri, &viewOptions)
-	s.session.NewView(ctx, name, uri, viewOptions)
+	options := s.session.Options()
+	s.fetchConfig(ctx, name, uri, &options)
+	s.session.NewView(ctx, name, uri, options)
 	return nil
 }