From f1f4a3381fbd0f7a3eaac83bd1a57f5c78273705 Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Fri, 6 Sep 2019 00:17:36 -0400 Subject: [PATCH] internal/lsp: move configuration options to structs This cl is the first in a set that change the configuration behaviour. This one should have no behaviour differences, but makes a lot of preparatory changes. The same options are set to the same values in the same places. The options are now stored on the Session instead of the Server The View supports options, but does not have any yet. Change-Id: Ie966cceca6878861686a1766d63bb8a78021259b Reviewed-on: https://go-review.googlesource.com/c/tools/+/193726 Run-TryBot: Ian Cottrell TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/cache/cache.go | 1 + internal/lsp/cache/session.go | 10 +++ internal/lsp/cache/view.go | 6 ++ internal/lsp/code_action.go | 6 +- internal/lsp/completion.go | 24 +++--- internal/lsp/diagnostics.go | 2 +- internal/lsp/folding_range.go | 2 +- internal/lsp/general.go | 102 ++++++++++++----------- internal/lsp/hover.go | 31 ++----- internal/lsp/lsp_test.go | 67 ++++++++------- internal/lsp/server.go | 22 ----- internal/lsp/source/completion.go | 16 +--- internal/lsp/source/completion_format.go | 4 +- internal/lsp/source/options.go | 77 +++++++++++++++++ internal/lsp/source/source_test.go | 12 +-- internal/lsp/source/view.go | 9 ++ internal/lsp/text_synchronization.go | 3 +- internal/lsp/watched_files.go | 3 +- internal/lsp/workspace.go | 4 +- 19 files changed, 235 insertions(+), 166 deletions(-) create mode 100644 internal/lsp/source/options.go diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index 298be2d7e5..44dae995d8 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -76,6 +76,7 @@ func (c *cache) NewSession(ctx context.Context) source.Session { s := &session{ cache: c, id: strconv.FormatInt(index, 10), + options: source.DefaultSessionOptions, overlays: make(map[span.URI]*overlay), filesWatchMap: NewWatchMap(), } diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index b092631ea2..517538d7ad 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -28,6 +28,8 @@ type session struct { cache *cache id string + options source.SessionOptions + viewMu sync.Mutex views []*view viewMap map[span.URI]source.View @@ -54,6 +56,14 @@ type overlay struct { unchanged bool } +func (s *session) Options() source.SessionOptions { + return s.options +} + +func (s *session) SetOptions(options source.SessionOptions) { + s.options = options +} + func (s *session) Shutdown(ctx context.Context) { s.viewMu.Lock() defer s.viewMu.Unlock() diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index b033c77ff0..888f1238bf 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -30,6 +30,8 @@ type view struct { session *session id string + options source.ViewOptions + // mu protects all mutable state of the view. mu sync.Mutex @@ -118,6 +120,10 @@ func (v *view) Folder() span.URI { return v.folder } +func (v *view) Options() source.ViewOptions { + return v.options +} + // Config returns the configuration used for the view's interaction with the // go/packages API. It is shared across all views. func (v *view) Config(ctx context.Context) *packages.Config { diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go index 6b61894ace..9b6fee47b8 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -21,7 +21,7 @@ import ( func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind { allCodeActionKinds := make(map[protocol.CodeActionKind]struct{}) - for _, kinds := range s.supportedCodeActions { + for _, kinds := range s.session.Options().SupportedCodeActions { for kind := range kinds { allCodeActionKinds[kind] = struct{}{} } @@ -51,7 +51,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara // Determine the supported actions for this file kind. fileKind := f.Handle(ctx).Kind() - supportedCodeActions, ok := s.supportedCodeActions[fileKind] + supportedCodeActions, ok := s.session.Options().SupportedCodeActions[fileKind] if !ok { return nil, fmt.Errorf("no supported code actions for %v file kind", fileKind) } @@ -87,7 +87,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara if wanted[protocol.QuickFix] { // First, add the quick fixes reported by go/analysis. // TODO: Enable this when this actually works. For now, it's needless work. - if s.wantSuggestedFixes { + if s.session.Options().SuggestedFixes { gof, ok := f.(source.GoFile) if !ok { return nil, fmt.Errorf("%s is not a Go file", f.URI()) diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 8d5ccffad7..77850e2609 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -19,17 +19,13 @@ import ( func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { uri := span.NewURI(params.TextDocument.URI) view := s.session.ViewOf(uri) + options := s.session.Options() f, err := getGoFile(ctx, view, uri) if err != nil { return nil, err } - candidates, surrounding, err := source.Completion(ctx, view, f, params.Position, source.CompletionOptions{ - WantDeepCompletion: !s.disableDeepCompletion, - WantFuzzyMatching: !s.disableFuzzyMatching, - NoDocumentation: !s.wantCompletionDocumentation, - WantFullDocumentation: s.hoverKind == fullDocumentation, - WantUnimported: s.wantUnimportedCompletions, - }) + options.Completion.FullDocumentation = options.HoverKind == source.FullDocumentation + candidates, surrounding, err := source.Completion(ctx, view, f, params.Position, options.Completion) if err != nil { log.Print(ctx, "no completions found", tag.Of("At", params.Position), tag.Of("Failure", err)) } @@ -50,12 +46,12 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara return &protocol.CompletionList{ // When using deep completions/fuzzy matching, report results as incomplete so // client fetches updated completions after every key stroke. - IsIncomplete: !s.disableDeepCompletion, - Items: s.toProtocolCompletionItems(candidates, rng), + IsIncomplete: options.Completion.Deep, + Items: s.toProtocolCompletionItems(candidates, rng, options), }, nil } -func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range) []protocol.CompletionItem { +func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range, options source.SessionOptions) []protocol.CompletionItem { var ( items = make([]protocol.CompletionItem, 0, len(candidates)) numDeepCompletionsSeen int @@ -64,7 +60,7 @@ func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, r // Limit the number of deep completions to not overwhelm the user in cases // with dozens of deep completion matches. if candidate.Depth > 0 { - if s.disableDeepCompletion { + if !options.Completion.Deep { continue } if numDeepCompletionsSeen >= source.MaxDeepCompletions { @@ -73,8 +69,8 @@ func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, r numDeepCompletionsSeen++ } insertText := candidate.InsertText - if s.insertTextFormat == protocol.SnippetTextFormat { - insertText = candidate.Snippet(s.usePlaceholders) + if options.InsertTextFormat == protocol.SnippetTextFormat { + insertText = candidate.Snippet(options.UsePlaceholders) } item := protocol.CompletionItem{ Label: candidate.Label, @@ -84,7 +80,7 @@ func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, r NewText: insertText, Range: rng, }, - InsertTextFormat: s.insertTextFormat, + InsertTextFormat: options.InsertTextFormat, AdditionalTextEdits: candidate.AdditionalTextEdits, // This is a hack so that the client sorts completion results in the order // according to their score. This can be removed upon the resolution of diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index fdf8d7493f..b771286734 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -27,7 +27,7 @@ func (s *Server) Diagnostics(ctx context.Context, view source.View, uri span.URI if !ok { return } - reports, err := source.Diagnostics(ctx, view, gof, s.disabledAnalyses) + reports, err := source.Diagnostics(ctx, view, gof, s.session.Options().DisabledAnalyses) if err != nil { log.Error(ctx, "failed to compute diagnostics", err, telemetry.File) return diff --git a/internal/lsp/folding_range.go b/internal/lsp/folding_range.go index 4dcc572847..a81961915d 100644 --- a/internal/lsp/folding_range.go +++ b/internal/lsp/folding_range.go @@ -20,7 +20,7 @@ func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRange return nil, err } - ranges, err := source.FoldingRange(ctx, view, f, s.lineFoldingOnly) + ranges, err := source.FoldingRange(ctx, view, f, s.session.Options().LineFoldingOnly) if err != nil { return nil, err } diff --git a/internal/lsp/general.go b/internal/lsp/general.go index ddb760ae21..7e78524fdb 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -32,21 +32,24 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara s.state = serverInitializing s.stateMu.Unlock() + options := s.session.Options() + defer func() { s.session.SetOptions(options) }() + // TODO: Remove the option once we are certain there are no issues here. - s.textDocumentSyncKind = protocol.Incremental + options.TextDocumentSyncKind = protocol.Incremental if opts, ok := params.InitializationOptions.(map[string]interface{}); ok { if opt, ok := opts["noIncrementalSync"].(bool); ok && opt { - s.textDocumentSyncKind = protocol.Full + options.TextDocumentSyncKind = protocol.Full } // Check if user has enabled watching for file changes. - s.watchFileChanges, _ = opts["watchFileChanges"].(bool) + setBool(&options.WatchFileChanges, opts, "watchFileChanges") } // Default to using synopsis as a default for hover information. - s.hoverKind = synopsisDocumentation + options.HoverKind = source.SynopsisDocumentation - s.supportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{ + options.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{ source.Go: { protocol.SourceOrganizeImports: true, protocol.QuickFix: true, @@ -55,7 +58,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara source.Sum: {}, } - s.setClientCapabilities(params.Capabilities) + s.setClientCapabilities(&options, params.Capabilities) folders := params.WorkspaceFolders if len(folders) == 0 { @@ -117,7 +120,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara TriggerCharacters: []string{"(", ","}, }, TextDocumentSync: &protocol.TextDocumentSyncOptions{ - Change: s.textDocumentSyncKind, + Change: options.TextDocumentSyncKind, OpenClose: true, Save: &protocol.SaveOptions{ IncludeText: false, @@ -142,24 +145,24 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara }, nil } -func (s *Server) setClientCapabilities(caps protocol.ClientCapabilities) { +func (s *Server) setClientCapabilities(o *source.SessionOptions, caps protocol.ClientCapabilities) { // Check if the client supports snippets in completion items. - s.insertTextFormat = protocol.PlainTextTextFormat + o.InsertTextFormat = protocol.PlainTextTextFormat if caps.TextDocument.Completion.CompletionItem.SnippetSupport { - s.insertTextFormat = protocol.SnippetTextFormat + o.InsertTextFormat = protocol.SnippetTextFormat } // Check if the client supports configuration messages. - s.configurationSupported = caps.Workspace.Configuration - s.dynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration - s.dynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration + 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. - s.preferredContentFormat = protocol.PlainText + o.PreferredContentFormat = protocol.PlainText if len(caps.TextDocument.Hover.ContentFormat) > 0 { - s.preferredContentFormat = caps.TextDocument.Hover.ContentFormat[0] + o.PreferredContentFormat = caps.TextDocument.Hover.ContentFormat[0] } // Check if the client supports only line folding. - s.lineFoldingOnly = caps.TextDocument.FoldingRange.LineFoldingOnly + o.LineFoldingOnly = caps.TextDocument.FoldingRange.LineFoldingOnly } func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error { @@ -167,8 +170,11 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa s.state = serverInitialized s.stateMu.Unlock() + options := s.session.Options() + defer func() { s.session.SetOptions(options) }() + var registrations []protocol.Registration - if s.configurationSupported && s.dynamicConfigurationSupported { + if options.ConfigurationSupported && options.DynamicConfigurationSupported { registrations = append(registrations, protocol.Registration{ ID: "workspace/didChangeConfiguration", @@ -181,7 +187,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa ) } - if s.watchFileChanges && s.dynamicWatchedFilesSupported { + if options.WatchFileChanges && options.DynamicWatchedFilesSupported { registrations = append(registrations, protocol.Registration{ ID: "workspace/didChangeWatchedFiles", Method: "workspace/didChangeWatchedFiles", @@ -200,9 +206,9 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa }) } - if s.configurationSupported { + if options.ConfigurationSupported { for _, view := range s.session.Views() { - if err := s.fetchConfig(ctx, view); err != nil { + if err := s.fetchConfig(ctx, view, &options); err != nil { return err } } @@ -213,7 +219,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa return nil } -func (s *Server) fetchConfig(ctx context.Context, view source.View) error { +func (s *Server) fetchConfig(ctx context.Context, view source.View, options *source.SessionOptions) error { configs, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{ Items: []protocol.ConfigurationItem{{ ScopeURI: protocol.NewURI(view.Folder()), @@ -228,14 +234,14 @@ func (s *Server) fetchConfig(ctx context.Context, view source.View) error { return err } for _, config := range configs { - if err := s.processConfig(ctx, view, config); err != nil { + if err := s.processConfig(ctx, view, options, config); err != nil { return err } } return nil } -func (s *Server) processConfig(ctx context.Context, view source.View, config interface{}) error { +func (s *Server) processConfig(ctx context.Context, view source.View, options *source.SessionOptions, 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 @@ -274,28 +280,23 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int // Check if the user wants documentation in completion items. // This defaults to true. - s.wantCompletionDocumentation = true - if wantCompletionDocumentation, ok := c["wantCompletionDocumentation"].(bool); ok { - s.wantCompletionDocumentation = wantCompletionDocumentation - } + options.Completion.Documentation = true + setBool(&options.Completion.Documentation, c, "wantCompletionDocumentation") + setBool(&options.UsePlaceholders, c, "usePlaceholders") - // Check if placeholders are enabled. - if usePlaceholders, ok := c["usePlaceholders"].(bool); ok { - s.usePlaceholders = usePlaceholders - } // Set the hover kind. if hoverKind, ok := c["hoverKind"].(string); ok { switch hoverKind { case "NoDocumentation": - s.hoverKind = noDocumentation + options.HoverKind = source.NoDocumentation case "SingleLine": - s.hoverKind = singleLine + options.HoverKind = source.SingleLine case "SynopsisDocumentation": - s.hoverKind = synopsisDocumentation + options.HoverKind = source.SynopsisDocumentation case "FullDocumentation": - s.hoverKind = fullDocumentation + options.HoverKind = source.FullDocumentation case "Structured": - s.hoverKind = 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. @@ -303,28 +304,23 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int } // Check if the user wants to see suggested fixes from go/analysis. - if wantSuggestedFixes, ok := c["wantSuggestedFixes"].(bool); ok { - s.wantSuggestedFixes = wantSuggestedFixes - } + setBool(&options.SuggestedFixes, c, "wantSuggestedFixes") // Check if the user has explicitly disabled any analyses. if disabledAnalyses, ok := c["experimentalDisabledAnalyses"].([]interface{}); ok { - s.disabledAnalyses = make(map[string]struct{}) + options.DisabledAnalyses = make(map[string]struct{}) for _, a := range disabledAnalyses { if a, ok := a.(string); ok { - s.disabledAnalyses[a] = struct{}{} + options.DisabledAnalyses[a] = struct{}{} } } } - s.disableDeepCompletion, _ = c["disableDeepCompletion"].(bool) - s.disableFuzzyMatching, _ = c["disableFuzzyMatching"].(bool) + setNotBool(&options.Completion.Deep, c, "disableDeepCompletion") + setNotBool(&options.Completion.FuzzyMatching, c, "disableFuzzyMatching") // Check if want unimported package completions. - if wantUnimportedCompletions, ok := c["wantUnimportedCompletions"].(bool); ok { - s.wantUnimportedCompletions = wantUnimportedCompletions - } - + setBool(&options.Completion.Unimported, c, "wantUnimportedCompletions") return nil } @@ -349,3 +345,15 @@ func (s *Server) exit(ctx context.Context) error { os.Exit(0) return nil } + +func setBool(b *bool, m map[string]interface{}, name string) { + if v, ok := m[name].(bool); ok { + *b = v + } +} + +func setNotBool(b *bool, m map[string]interface{}, name string) { + if v, ok := m[name].(bool); ok { + *b = !v + } +} diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index 333ce14477..88c5c20645 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -15,22 +15,6 @@ import ( "golang.org/x/tools/internal/telemetry/log" ) -type hoverKind int - -const ( - singleLine = hoverKind(iota) - noDocumentation - synopsisDocumentation - fullDocumentation - - // structured is an experimental setting that returns a structured hover format. - // This format separates the signature from the documentation, so that the client - // can do more manipulation of these fields. - // - // This should only be used by clients that support this behavior. - structured -) - func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) { uri := span.NewURI(params.TextDocument.URI) view := s.session.ViewOf(uri) @@ -58,31 +42,32 @@ func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositio } func (s *Server) toProtocolHoverContents(ctx context.Context, h *source.HoverInformation) protocol.MarkupContent { + options := s.session.Options() content := protocol.MarkupContent{ - Kind: s.preferredContentFormat, + Kind: options.PreferredContentFormat, } signature := h.Signature if content.Kind == protocol.Markdown { signature = fmt.Sprintf("```go\n%s\n```", h.Signature) } - switch s.hoverKind { - case singleLine: + switch options.HoverKind { + case source.SingleLine: content.Value = h.SingleLine - case noDocumentation: + case source.NoDocumentation: content.Value = signature - case synopsisDocumentation: + case source.SynopsisDocumentation: if h.Synopsis != "" { content.Value = fmt.Sprintf("%s\n%s", h.Synopsis, signature) } else { content.Value = signature } - case fullDocumentation: + case source.FullDocumentation: if h.FullDocumentation != "" { content.Value = fmt.Sprintf("%s\n%s", signature, h.FullDocumentation) } else { content.Value = signature } - case structured: + case source.Structured: b, err := json.Marshal(h) if err != nil { log.Error(ctx, "failed to marshal structured hover", err) diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 746348bb64..ba1c0266cb 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -55,23 +55,27 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { for filename, content := range data.Config.Overlay { session.SetOverlay(span.FileURI(filename), content) } + options := session.Options() + options.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{ + source.Go: { + protocol.SourceOrganizeImports: true, + protocol.QuickFix: true, + }, + source.Mod: {}, + source.Sum: {}, + } + options.HoverKind = source.SynopsisDocumentation + session.SetOptions(options) + r := &runner{ server: &Server{ session: session, undelivered: make(map[span.URI][]source.Diagnostic), - supportedCodeActions: map[source.FileKind]map[protocol.CodeActionKind]bool{ - source.Go: { - protocol.SourceOrganizeImports: true, - protocol.QuickFix: true, - }, - source.Mod: {}, - source.Sum: {}, - }, - hoverKind: synopsisDocumentation, }, data: data, ctx: ctx, } + tests.Run(t, r, data) } @@ -106,14 +110,12 @@ func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) { } func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) { - defer func() { - r.server.disableDeepCompletion = true - r.server.disableFuzzyMatching = true - r.server.wantUnimportedCompletions = false - }() + original := r.server.session.Options() + modified := original + defer func() { r.server.session.SetOptions(original) }() // Set this as a default. - r.server.wantCompletionDocumentation = true + modified.Completion.Documentation = true for src, itemList := range data { var want []source.CompletionItem @@ -121,9 +123,10 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests want = append(want, *items[pos]) } - r.server.disableDeepCompletion = !strings.Contains(string(src.URI()), "deepcomplete") - r.server.disableFuzzyMatching = !strings.Contains(string(src.URI()), "fuzzymatch") - r.server.wantUnimportedCompletions = strings.Contains(string(src.URI()), "unimported") + modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete") + modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch") + modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported") + r.server.session.SetOptions(modified) list := r.runCompletion(t, src) @@ -140,21 +143,15 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests } } - origPlaceHolders := r.server.usePlaceholders - origTextFormat := r.server.insertTextFormat - defer func() { - r.server.usePlaceholders = origPlaceHolders - r.server.insertTextFormat = origTextFormat - }() - - r.server.insertTextFormat = protocol.SnippetTextFormat + modified.InsertTextFormat = protocol.SnippetTextFormat for _, usePlaceholders := range []bool{true, false} { - r.server.usePlaceholders = usePlaceholders + modified.UsePlaceholders = usePlaceholders for src, want := range snippets { - r.server.disableDeepCompletion = !strings.Contains(string(src.URI()), "deepcomplete") - r.server.disableFuzzyMatching = !strings.Contains(string(src.URI()), "fuzzymatch") - r.server.wantUnimportedCompletions = strings.Contains(string(src.URI()), "unimported") + modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete") + modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch") + modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported") + r.server.session.SetOptions(modified) list := r.runCompletion(t, src) @@ -266,11 +263,16 @@ func summarizeCompletionItems(i int, want []source.CompletionItem, got []protoco } func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) { + original := r.server.session.Options() + modified := original + defer func() { r.server.session.SetOptions(original) }() + for _, spn := range data { uri := spn.URI() // Test all folding ranges. - r.server.lineFoldingOnly = false + modified.LineFoldingOnly = false + r.server.session.SetOptions(modified) ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), @@ -283,7 +285,8 @@ func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) { r.foldingRanges(t, "foldingRange", uri, ranges) // Test folding ranges with lineFoldingOnly = true. - r.server.lineFoldingOnly = true + modified.LineFoldingOnly = true + r.server.session.SetOptions(modified) ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(uri), diff --git a/internal/lsp/server.go b/internal/lsp/server.go index aedbcb7b21..c1274fae18 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -76,28 +76,6 @@ type Server struct { stateMu sync.Mutex state serverState - // Configurations. - // TODO(rstambler): Separate these into their own struct? - usePlaceholders bool - hoverKind hoverKind - disableDeepCompletion bool - disableFuzzyMatching 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 - lineFoldingOnly bool - - supportedCodeActions map[source.FileKind]map[protocol.CodeActionKind]bool - - textDocumentSyncKind protocol.TextDocumentSyncKind - session source.Session // undelivered is a cache of any diagnostics that the server diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go index 3d571b5e7b..9c2cfc29f9 100644 --- a/internal/lsp/source/completion.go +++ b/internal/lsp/source/completion.go @@ -266,7 +266,7 @@ func (c *completer) setSurrounding(ident *ast.Ident) { }, } - if c.opts.WantFuzzyMatching { + if c.opts.FuzzyMatching { c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix(), fuzzy.Symbol) } else { c.matcher = prefixMatcher(strings.ToLower(c.surrounding.Prefix())) @@ -379,16 +379,6 @@ type candidate struct { imp *imports.ImportInfo } -type CompletionOptions struct { - WantDeepCompletion bool - WantFuzzyMatching bool - - WantUnimported bool - - NoDocumentation bool - WantFullDocumentation bool -} - // Completion returns a list of possible candidates for completion, given a // a file and a position. // @@ -472,7 +462,7 @@ func Completion(ctx context.Context, view View, f GoFile, pos protocol.Position, startTime: startTime, } - if opts.WantDeepCompletion { + if opts.Deep { // Initialize max search depth to unlimited. c.deepState.maxDepth = -1 } @@ -673,7 +663,7 @@ func (c *completer) lexical() error { } } - if c.opts.WantUnimported { + if c.opts.Unimported { // Suggest packages that have not been imported yet. pkgs, err := CandidateImports(c.ctx, c.view, c.filename) if err != nil { diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go index bdf7166569..508f7bf66f 100644 --- a/internal/lsp/source/completion_format.go +++ b/internal/lsp/source/completion_format.go @@ -114,7 +114,7 @@ func (c *completer) item(cand candidate) (CompletionItem, error) { placeholderSnippet: placeholderSnippet, } // If the user doesn't want documentation for completion items. - if c.opts.NoDocumentation { + if !c.opts.Documentation { return item, nil } declRange, err := objToRange(c.ctx, c.view, obj) @@ -160,7 +160,7 @@ func (c *completer) item(cand candidate) (CompletionItem, error) { return item, nil } item.Documentation = hover.Synopsis - if c.opts.WantFullDocumentation { + if c.opts.FullDocumentation { item.Documentation = hover.FullDocumentation } return item, nil diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go new file mode 100644 index 0000000000..f977bd13c6 --- /dev/null +++ b/internal/lsp/source/options.go @@ -0,0 +1,77 @@ +// 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 source + +import "golang.org/x/tools/internal/lsp/protocol" + +var ( + DefaultSessionOptions = SessionOptions{ + TextDocumentSyncKind: protocol.Incremental, + HoverKind: SynopsisDocumentation, + InsertTextFormat: protocol.PlainTextTextFormat, + SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{ + Go: { + protocol.SourceOrganizeImports: true, + protocol.QuickFix: true, + }, + Mod: {}, + Sum: {}, + }, + Completion: CompletionOptions{ + Documentation: true, + }, + } + DefaultViewOptions = ViewOptions{} +) + +type SessionOptions struct { + Env []string + BuildFlags []string + UsePlaceholders bool + HoverKind HoverKind + SuggestedFixes bool + DisabledAnalyses map[string]struct{} + + WatchFileChanges bool + InsertTextFormat protocol.InsertTextFormat + ConfigurationSupported bool + DynamicConfigurationSupported bool + DynamicWatchedFilesSupported bool + PreferredContentFormat protocol.MarkupKind + LineFoldingOnly bool + + SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool + + TextDocumentSyncKind protocol.TextDocumentSyncKind + + Completion CompletionOptions +} + +type ViewOptions struct { +} + +type CompletionOptions struct { + Deep bool + FuzzyMatching bool + Unimported bool + Documentation bool + FullDocumentation bool +} + +type HoverKind int + +const ( + SingleLine = HoverKind(iota) + NoDocumentation + SynopsisDocumentation + FullDocumentation + + // structured is an experimental setting that returns a structured hover format. + // This format separates the signature from the documentation, so that the client + // can do more manipulation of these fields. + // + // This should only be used by clients that support this behavior. + Structured +) diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index a85bcbce82..9865a0e8e4 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -102,9 +102,10 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests Line: float64(src.Start().Line() - 1), Character: float64(src.Start().Column() - 1), }, source.CompletionOptions{ - WantDeepCompletion: deepComplete, - WantFuzzyMatching: fuzzyMatch, - WantUnimported: unimported, + Documentation: true, + Deep: deepComplete, + FuzzyMatching: fuzzyMatch, + Unimported: unimported, }) if err != nil { t.Fatalf("failed for %v: %v", src, err) @@ -156,8 +157,9 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests Line: float64(src.Start().Line() - 1), Character: float64(src.Start().Column() - 1), }, source.CompletionOptions{ - WantDeepCompletion: strings.Contains(string(src.URI()), "deepcomplete"), - WantFuzzyMatching: strings.Contains(string(src.URI()), "fuzzymatch"), + Documentation: true, + Deep: strings.Contains(string(src.URI()), "deepcomplete"), + FuzzyMatching: strings.Contains(string(src.URI()), "fuzzymatch"), }) if err != nil { t.Fatalf("failed for %v: %v", src, err) diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index d32662e4d3..205a8de7ae 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -190,6 +190,12 @@ type Session interface { // DidChangeOutOfBand is called when a file under the root folder // changes. The file is not necessarily open in the editor. DidChangeOutOfBand(uri span.URI) + + // Options returns a copy of the SessionOptions for this session. + Options() SessionOptions + + // SetOptions sets the options of this session to new values. + SetOptions(SessionOptions) } // View represents a single workspace. @@ -243,6 +249,9 @@ type View interface { // RunProcessEnvFunc runs fn with the process env for this view inserted into opts. // Note: the process env contains cached module and filesystem state. 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 } // File represents a source file of any type. diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go index fa0e9d536a..9f622b5e24 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -42,6 +42,7 @@ func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocume } func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { + options := s.session.Options() if len(params.ContentChanges) < 1 { return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided") } @@ -54,7 +55,7 @@ func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDo // We only accept an incremental change if the server expected it. if !isFullChange { - switch s.textDocumentSyncKind { + switch options.TextDocumentSyncKind { case protocol.Full: return errors.Errorf("expected a full content change, received incremental changes for %s", uri) case protocol.Incremental: diff --git a/internal/lsp/watched_files.go b/internal/lsp/watched_files.go index 37c82e43b8..6c962bf3f8 100644 --- a/internal/lsp/watched_files.go +++ b/internal/lsp/watched_files.go @@ -14,7 +14,8 @@ import ( ) func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { - if !s.watchFileChanges { + options := s.session.Options() + if !options.WatchFileChanges { return nil } diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go index 5812756900..e357fe35f4 100644 --- a/internal/lsp/workspace.go +++ b/internal/lsp/workspace.go @@ -35,8 +35,10 @@ func (s *Server) addView(ctx context.Context, name string, uri span.URI) error { s.stateMu.Lock() state := s.state s.stateMu.Unlock() + options := s.session.Options() + defer func() { s.session.SetOptions(options) }() if state >= serverInitialized { - s.fetchConfig(ctx, view) + s.fetchConfig(ctx, view, &options) } return nil }