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 }