From 16c5e0f7d110d27323e11cae5489d256bc6134d3 Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Mon, 9 Sep 2019 09:28:25 -0400 Subject: [PATCH] internal/lsp: process configuration options more thoroughly Change-Id: Ic3e2f948f857c697564fa6ab02008444dd9392c7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/194458 Run-TryBot: Ian Cottrell TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/general.go | 138 ++---------------------- internal/lsp/source/options.go | 185 ++++++++++++++++++++++++++++++++- internal/lsp/workspace.go | 7 +- 3 files changed, 191 insertions(+), 139 deletions(-) diff --git a/internal/lsp/general.go b/internal/lsp/general.go index 1e7ac99fa9..69f973f40d 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -7,7 +7,6 @@ package lsp import ( "bytes" "context" - "fmt" "os" "path" @@ -17,7 +16,6 @@ import ( "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 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitia) ( 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 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitia) ( }, 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 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa 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 @@ func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, 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 a2b58d4e83..062f020963 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 @@ type SessionOptions struct { 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 CompletionOptions struct { type HoverKind int +type Options interface { + set(name string, value interface{}) OptionResult +} + const ( SingleLine = HoverKind(iota) NoDocumentation @@ -89,3 +98,173 @@ const ( // 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 bfbf0652dc..27bdc16340 100644 --- a/internal/lsp/workspace.go +++ b/internal/lsp/workspace.go @@ -38,11 +38,8 @@ func (s *Server) addView(ctx context.Context, name string, uri span.URI) error { 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 }