1
0
mirror of https://github.com/golang/go synced 2024-10-01 07:38:32 -06:00
go/internal/lsp/general.go
Muir Manders dd2b5c81c5 internal/lsp: simplify snippet config/generation
I moved the "usePlaceholders" config field on to CompletionOptions.
This way the completion code generates a single snippet with a little
conditional logic based on the "WantPlaceholders" option instead of
juggling the generation of two almost identical "plain" and
"placeholder" snippets at the same time. It also reduces the work done
generating completion candidates a little.

I also made a minor tweak to the snippet builder where empty
placeholders are now always represented as e.g "${1:}" instead of
"${1}" or "${1:}", depending on if you passed a callback to
WritePlaceholder() or not.

Change-Id: Ib84cc0cd729a11b9e13ad3ac4b6fd2d82460acd5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/193697
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2019-09-10 04:45:52 +00:00

363 lines
11 KiB
Go

// 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 lsp
import (
"bytes"
"context"
"fmt"
"os"
"path"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/protocol"
"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"
)
func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitia) (*protocol.InitializeResult, error) {
s.stateMu.Lock()
state := s.state
s.stateMu.Unlock()
if state >= serverInitializing {
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
}
s.stateMu.Lock()
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.
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)
folders := params.WorkspaceFolders
if len(folders) == 0 {
if params.RootURI != "" {
folders = []protocol.WorkspaceFolder{{
URI: params.RootURI,
Name: path.Base(params.RootURI),
}}
} else {
// no folders and no root, single file mode
//TODO(iancottrell): not sure how to do single file mode yet
//issue: golang.org/issue/31168
return nil, errors.Errorf("single file mode not supported yet")
}
}
for _, folder := range folders {
if err := s.addView(ctx, folder.Name, span.NewURI(folder.URI)); err != nil {
return nil, err
}
}
var codeActionProvider interface{}
if params.Capabilities.TextDocument.CodeAction.CodeActionLiteralSupport != nil &&
len(params.Capabilities.TextDocument.CodeAction.CodeActionLiteralSupport.CodeActionKind.ValueSet) > 0 {
// If the client has specified CodeActionLiteralSupport,
// send the code actions we support.
//
// Using CodeActionOptions is only valid if codeActionLiteralSupport is set.
codeActionProvider = &protocol.CodeActionOptions{
CodeActionKinds: s.getSupportedCodeActions(),
}
} else {
codeActionProvider = true
}
var renameOpts interface{}
if params.Capabilities.TextDocument.Rename.PrepareSupport {
renameOpts = &protocol.RenameOptions{
PrepareProvider: true,
}
} else {
renameOpts = true
}
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
CodeActionProvider: codeActionProvider,
CompletionProvider: &protocol.CompletionOptions{
TriggerCharacters: []string{"."},
},
DefinitionProvider: true,
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
FoldingRangeProvider: true,
HoverProvider: true,
DocumentHighlightProvider: true,
DocumentLinkProvider: &protocol.DocumentLinkOptions{},
ReferencesProvider: true,
RenameProvider: renameOpts,
SignatureHelpProvider: &protocol.SignatureHelpOptions{
TriggerCharacters: []string{"(", ","},
},
TextDocumentSync: &protocol.TextDocumentSyncOptions{
Change: options.TextDocumentSyncKind,
OpenClose: true,
Save: &protocol.SaveOptions{
IncludeText: false,
},
},
TypeDefinitionProvider: true,
Workspace: &struct {
WorkspaceFolders *struct {
Supported bool "json:\"supported,omitempty\""
ChangeNotifications string "json:\"changeNotifications,omitempty\""
} "json:\"workspaceFolders,omitempty\""
}{
WorkspaceFolders: &struct {
Supported bool "json:\"supported,omitempty\""
ChangeNotifications string "json:\"changeNotifications,omitempty\""
}{
Supported: true,
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
},
},
},
}, 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
s.stateMu.Unlock()
options := s.session.Options()
defer func() { s.session.SetOptions(options) }()
var registrations []protocol.Registration
if options.ConfigurationSupported && options.DynamicConfigurationSupported {
registrations = append(registrations,
protocol.Registration{
ID: "workspace/didChangeConfiguration",
Method: "workspace/didChangeConfiguration",
},
protocol.Registration{
ID: "workspace/didChangeWorkspaceFolders",
Method: "workspace/didChangeWorkspaceFolders",
},
)
}
if options.WatchFileChanges && options.DynamicWatchedFilesSupported {
registrations = append(registrations, protocol.Registration{
ID: "workspace/didChangeWatchedFiles",
Method: "workspace/didChangeWatchedFiles",
RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{
Watchers: []protocol.FileSystemWatcher{{
GlobPattern: "**/*.go",
Kind: float64(protocol.WatchChange),
}},
},
})
}
if len(registrations) > 0 {
s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
Registrations: registrations,
})
}
if options.ConfigurationSupported {
for _, view := range s.session.Views() {
if err := s.fetchConfig(ctx, view, &options); err != nil {
return err
}
}
}
buf := &bytes.Buffer{}
debug.PrintVersionInfo(buf, true, debug.PlainText)
log.Print(ctx, buf.String())
return nil
}
func (s *Server) fetchConfig(ctx context.Context, view source.View, options *source.SessionOptions) error {
v := protocol.ParamConfig{
protocol.ConfigurationParams{
Items: []protocol.ConfigurationItem{{
ScopeURI: protocol.NewURI(view.Folder()),
Section: "gopls",
}, {
ScopeURI: protocol.NewURI(view.Folder()),
Section: view.Name(),
},
},
}, protocol.PartialResultParams{},
}
configs, err := s.client.Configuration(ctx, &v)
if err != nil {
return err
}
for _, config := range configs {
if err := s.processConfig(ctx, view, options, config); err != nil {
return err
}
}
return nil
}
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
}
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)
}
env := view.Env()
for k, v := range menv {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
view.SetEnv(env)
}
// 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))
}
view.SetBuildFlags(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()
if s.state < serverInitialized {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
}
// drop all the active views
s.session.Shutdown(ctx)
s.state = serverShutDown
return nil
}
func (s *Server) exit(ctx context.Context) error {
s.stateMu.Lock()
defer s.stateMu.Unlock()
if s.state != serverShutDown {
os.Exit(1)
}
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
}
}