internal/lsp: refactor server.go to separate into LSP categories

This change separates the different behaviors of server.go by the
categories defined in the spec. This allows us to differentiate more
easily between the language features and the text synchronization code.

I also renamed the "Symbols" function to "Symbol", which fits with the
specification
(https://microsoft.github.io/language-server-protocol/specification#workspace_symbol),
and makes clearer the distinction between DocumentSymbols and Symbol.

Change-Id: I926b8a772c478f6ae426352fb12dc4403f0e736a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/172637
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Rebecca Stambler 2019-04-17 15:37:20 -04:00
parent 6149f385e4
commit 34437f544f
11 changed files with 585 additions and 431 deletions

View File

@ -0,0 +1,81 @@
// 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 (
"context"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
func (s *Server) definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
ident, err := source.Identifier(ctx, view, f, rng.Start)
if err != nil {
return nil, err
}
decSpan, err := ident.Declaration.Range.Span()
if err != nil {
return nil, err
}
_, decM, err := newColumnMap(ctx, view, decSpan.URI())
if err != nil {
return nil, err
}
loc, err := decM.Location(decSpan)
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}
func (s *Server) typeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
ident, err := source.Identifier(ctx, view, f, rng.Start)
if err != nil {
return nil, err
}
identSpan, err := ident.Type.Range.Span()
if err != nil {
return nil, err
}
_, identM, err := newColumnMap(ctx, view, identSpan.URI())
if err != nil {
return nil, err
}
loc, err := identM.Location(identSpan)
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}

View File

@ -1,3 +1,7 @@
// Copyright 2018 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 package lsp
import ( import (
@ -9,6 +13,27 @@ import (
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
spn := span.New(uri, span.Point{}, span.Point{})
return formatRange(ctx, view, spn)
}
func (s *Server) rangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
_, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.RangeSpan(params.Range)
if err != nil {
return nil, err
}
return formatRange(ctx, view, spn)
}
// formatRange formats a document with a given range. // formatRange formats a document with a given range.
func formatRange(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) { func formatRange(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) {
f, m, err := newColumnMap(ctx, v, s.URI()) f, m, err := newColumnMap(ctx, v, s.URI())
@ -69,16 +94,3 @@ func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]s
} }
return result, nil return result, nil
} }
func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
f, err := v.GetFile(ctx, uri)
if err != nil {
return nil, nil, err
}
tok := f.GetToken(ctx)
if tok == nil {
return nil, nil, fmt.Errorf("no file information for %v", f.URI())
}
m := protocol.NewColumnMapper(f.URI(), f.GetFileSet(ctx), tok, f.GetContent(ctx))
return f, m, nil
}

190
internal/lsp/general.go Normal file
View File

@ -0,0 +1,190 @@
// 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 (
"context"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
)
func (s *Server) initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
s.initializedMu.Lock()
defer s.initializedMu.Unlock()
if s.isInitialized {
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
}
s.isInitialized = true // mark server as initialized now
// TODO(rstambler): Change this default to protocol.Incremental (or add a
// flag). Disabled for now to simplify debugging.
s.textDocumentSyncKind = protocol.Full
s.setClientCapabilities(params.Capabilities)
// We need a "detached" context so it does not get timeout cancelled.
// TODO(iancottrell): Do we need to copy any values across?
viewContext := context.Background()
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, fmt.Errorf("single file mode not supported yet")
}
}
for _, folder := range folders {
uri := span.NewURI(folder.URI)
folderPath, err := uri.Filename()
if err != nil {
return nil, err
}
s.views = append(s.views, cache.NewView(viewContext, s.log, folder.Name, uri, &packages.Config{
Context: ctx,
Dir: folderPath,
Env: os.Environ(),
Mode: packages.LoadImports,
Fset: token.NewFileSet(),
Overlay: make(map[string][]byte),
ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
},
Tests: true,
}))
}
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
CodeActionProvider: true,
CompletionProvider: &protocol.CompletionOptions{
TriggerCharacters: []string{"."},
},
DefinitionProvider: true,
DocumentFormattingProvider: true,
DocumentRangeFormattingProvider: true,
DocumentSymbolProvider: true,
HoverProvider: true,
DocumentHighlightProvider: true,
SignatureHelpProvider: &protocol.SignatureHelpOptions{
TriggerCharacters: []string{"(", ","},
},
TextDocumentSync: &protocol.TextDocumentSyncOptions{
Change: s.textDocumentSyncKind,
OpenClose: true,
},
TypeDefinitionProvider: true,
},
}, nil
}
func (s *Server) setClientCapabilities(caps protocol.ClientCapabilities) {
// Check if the client supports snippets in completion items.
s.snippetsSupported = caps.TextDocument.Completion.CompletionItem.SnippetSupport
// Check if the client supports configuration messages.
s.configurationSupported = caps.Workspace.Configuration
s.dynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
}
func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error {
if s.configurationSupported {
if s.dynamicConfigurationSupported {
s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
Registrations: []protocol.Registration{{
ID: "workspace/didChangeConfiguration",
Method: "workspace/didChangeConfiguration",
}},
})
}
for _, view := range s.views {
config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
Items: []protocol.ConfigurationItem{{
ScopeURI: protocol.NewURI(view.Folder),
Section: "gopls",
}},
})
if err != nil {
return err
}
if err := s.processConfig(view, config[0]); err != nil {
return err
}
}
}
return nil
}
func (s *Server) processConfig(view *cache.View, 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 fmt.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 fmt.Errorf("invalid config gopls.env type %T", env)
}
for k, v := range menv {
view.Config.Env = applyEnv(view.Config.Env, k, v)
}
}
// Check if placeholders are enabled.
if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
s.usePlaceholders = usePlaceholders
}
return nil
}
func applyEnv(env []string, k string, v interface{}) []string {
prefix := k + "="
value := prefix + fmt.Sprint(v)
for i, s := range env {
if strings.HasPrefix(s, prefix) {
env[i] = value
return env
}
}
return append(env, value)
}
func (s *Server) shutdown(ctx context.Context) error {
// TODO(rstambler): Cancel contexts here?
s.initializedMu.Lock()
defer s.initializedMu.Unlock()
if !s.isInitialized {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
}
s.isInitialized = false
return nil
}
func (s *Server) exit(ctx context.Context) error {
if s.isInitialized {
os.Exit(1)
}
os.Exit(0)
return nil
}

View File

@ -5,10 +5,32 @@
package lsp package lsp
import ( import (
"context"
"golang.org/x/tools/internal/lsp/protocol" "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/span"
) )
func (s *Server) documentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
spans := source.Highlight(ctx, f, rng.Start)
return toProtocolHighlight(m, spans), nil
}
func toProtocolHighlight(m *protocol.ColumnMapper, spans []span.Span) []protocol.DocumentHighlight { func toProtocolHighlight(m *protocol.ColumnMapper, spans []span.Span) []protocol.DocumentHighlight {
result := make([]protocol.DocumentHighlight, 0, len(spans)) result := make([]protocol.DocumentHighlight, 0, len(spans))
kind := protocol.Text kind := protocol.Text

54
internal/lsp/hover.go Normal file
View File

@ -0,0 +1,54 @@
// 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 (
"context"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
identRange, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
ident, err := source.Identifier(ctx, view, f, identRange.Start)
if err != nil {
return nil, err
}
content, err := ident.Hover(ctx, nil)
if err != nil {
return nil, err
}
markdown := "```go\n" + content + "\n```"
identSpan, err := ident.Range.Span()
if err != nil {
return nil, err
}
rng, err := m.Range(identSpan)
if err != nil {
return nil, err
}
return &protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.Markdown,
Value: markdown,
},
Range: &rng,
}, nil
}

View File

@ -20,7 +20,7 @@ type Server interface {
DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error
DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error
DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error
Symbols(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error)
ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error) ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error)
DidOpen(context.Context, *DidOpenTextDocumentParams) error DidOpen(context.Context, *DidOpenTextDocumentParams) error
DidChange(context.Context, *DidChangeTextDocumentParams) error DidChange(context.Context, *DidChangeTextDocumentParams) error
@ -140,7 +140,7 @@ func serverHandler(log xlog.Logger, server Server) jsonrpc2.Handler {
sendParseError(ctx, log, conn, r, err) sendParseError(ctx, log, conn, r, err)
return return
} }
resp, err := server.Symbols(ctx, &params) resp, err := server.Symbol(ctx, &params)
if err := conn.Reply(ctx, r, resp, err); err != nil { if err := conn.Reply(ctx, r, resp, err); err != nil {
log.Errorf(ctx, "%v", err) log.Errorf(ctx, "%v", err)
} }
@ -502,7 +502,7 @@ func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *Di
return s.Conn.Notify(ctx, "workspace/didChangeWatchedFiles", params) return s.Conn.Notify(ctx, "workspace/didChangeWatchedFiles", params)
} }
func (s *serverDispatcher) Symbols(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) { func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) {
var result []SymbolInformation var result []SymbolInformation
if err := s.Conn.Call(ctx, "workspace/symbol", params, &result); err != nil { if err := s.Conn.Call(ctx, "workspace/symbol", params, &result); err != nil {
return nil, err return nil, err

View File

@ -5,19 +5,11 @@
package lsp package lsp
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"go/ast"
"go/parser"
"go/token"
"net" "net"
"os"
"path"
"strings"
"sync" "sync"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
@ -67,13 +59,17 @@ func RunServerOnAddress(ctx context.Context, addr string, h func(s *Server)) err
} }
} }
func (s *Server) Run(ctx context.Context) error {
return s.Conn.Run(ctx)
}
type Server struct { type Server struct {
Conn *jsonrpc2.Conn Conn *jsonrpc2.Conn
client protocol.Client client protocol.Client
log xlog.Logger log xlog.Logger
initializedMu sync.Mutex initializedMu sync.Mutex
initialized bool // set once the server has received "initialize" request isInitialized bool // set once the server has received "initialize" request
// Configurations. // Configurations.
// TODO(rstambler): Separate these into their own struct? // TODO(rstambler): Separate these into their own struct?
@ -92,139 +88,26 @@ type Server struct {
undelivered map[span.URI][]source.Diagnostic undelivered map[span.URI][]source.Diagnostic
} }
func (s *Server) Run(ctx context.Context) error { // General
return s.Conn.Run(ctx)
}
func (s *Server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) { func (s *Server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
s.initializedMu.Lock() return s.initialize(ctx, params)
defer s.initializedMu.Unlock()
if s.initialized {
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
}
s.initialized = true // mark server as initialized now
// TODO(rstambler): Change this default to protocol.Incremental (or add a
// flag). Disabled for now to simplify debugging.
s.textDocumentSyncKind = protocol.Full
s.setClientCapabilities(params.Capabilities)
// We need a "detached" context so it does not get timeout cancelled.
// TODO(iancottrell): Do we need to copy any values across?
viewContext := context.Background()
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, fmt.Errorf("single file mode not supported yet")
}
}
for _, folder := range folders {
uri := span.NewURI(folder.URI)
folderPath, err := uri.Filename()
if err != nil {
return nil, err
}
s.views = append(s.views, cache.NewView(viewContext, s.log, folder.Name, uri, &packages.Config{
Context: ctx,
Dir: folderPath,
Env: os.Environ(),
Mode: packages.LoadImports,
Fset: token.NewFileSet(),
Overlay: make(map[string][]byte),
ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
},
Tests: true,
}))
}
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
CodeActionProvider: true,
CompletionProvider: &protocol.CompletionOptions{
TriggerCharacters: []string{"."},
},
DefinitionProvider: true,
DocumentFormattingProvider: true,
DocumentRangeFormattingProvider: true,
DocumentSymbolProvider: true,
HoverProvider: true,
DocumentHighlightProvider: true,
SignatureHelpProvider: &protocol.SignatureHelpOptions{
TriggerCharacters: []string{"(", ","},
},
TextDocumentSync: &protocol.TextDocumentSyncOptions{
Change: s.textDocumentSyncKind,
OpenClose: true,
},
TypeDefinitionProvider: true,
},
}, nil
}
func (s *Server) setClientCapabilities(caps protocol.ClientCapabilities) {
// Check if the client supports snippets in completion items.
s.snippetsSupported = caps.TextDocument.Completion.CompletionItem.SnippetSupport
// Check if the client supports configuration messages.
s.configurationSupported = caps.Workspace.Configuration
s.dynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
} }
func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
if s.configurationSupported { return s.initialized(ctx, params)
if s.dynamicConfigurationSupported {
s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
Registrations: []protocol.Registration{{
ID: "workspace/didChangeConfiguration",
Method: "workspace/didChangeConfiguration",
}},
})
}
for _, view := range s.views {
config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
Items: []protocol.ConfigurationItem{{
ScopeURI: protocol.NewURI(view.Folder),
Section: "gopls",
}},
})
if err != nil {
return err
}
if err := s.processConfig(view, config[0]); err != nil {
return err
}
}
}
return nil
} }
func (s *Server) Shutdown(context.Context) error { func (s *Server) Shutdown(ctx context.Context) error {
s.initializedMu.Lock() return s.shutdown(ctx)
defer s.initializedMu.Unlock()
if !s.initialized {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
}
s.initialized = false
return nil
} }
func (s *Server) Exit(ctx context.Context) error { func (s *Server) Exit(ctx context.Context) error {
if s.initialized { return s.exit(ctx)
os.Exit(1)
}
os.Exit(0)
return nil
} }
// Workspace
func (s *Server) DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error { func (s *Server) DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error {
return notImplemented("DidChangeWorkspaceFolders") return notImplemented("DidChangeWorkspaceFolders")
} }
@ -237,78 +120,22 @@ func (s *Server) DidChangeWatchedFiles(context.Context, *protocol.DidChangeWatch
return notImplemented("DidChangeWatchedFiles") return notImplemented("DidChangeWatchedFiles")
} }
func (s *Server) Symbols(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
return nil, notImplemented("Symbols") return nil, notImplemented("Symbol")
} }
func (s *Server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) (interface{}, error) { func (s *Server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) (interface{}, error) {
return nil, notImplemented("ExecuteCommand") return nil, notImplemented("ExecuteCommand")
} }
// Text Synchronization
func (s *Server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { func (s *Server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), params.TextDocument.Text) return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), params.TextDocument.Text)
} }
func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) {
if len(params.ContentChanges) == 1 && params.ContentChanges[0].Range == nil {
// If range is empty, we expect the full content of file, i.e. a single change with no range.
change := params.ContentChanges[0]
if change.RangeLength != 0 {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
}
return change.Text, nil
}
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
file, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
}
content := file.GetContent(ctx)
for _, change := range params.ContentChanges {
spn, err := m.RangeSpan(*change.Range)
if err != nil {
return "", err
}
if !spn.HasOffset() {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
}
start, end := spn.Start().Offset(), spn.End().Offset()
if end <= start {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
}
var buf bytes.Buffer
buf.Write(content[:start])
buf.WriteString(change.Text)
buf.Write(content[end:])
content = buf.Bytes()
}
return string(content), nil
}
func (s *Server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { func (s *Server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
if len(params.ContentChanges) < 1 { return s.didChange(ctx, params)
return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
}
var text string
switch s.textDocumentSyncKind {
case protocol.Incremental:
var err error
text, err = s.applyChanges(ctx, params)
if err != nil {
return err
}
case protocol.Full:
// We expect the full content of file, i.e. a single change with no range.
change := params.ContentChanges[0]
if change.RangeLength != 0 {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
}
text = change.Text
}
return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), text)
} }
func (s *Server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error { func (s *Server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error {
@ -319,16 +146,16 @@ func (s *Server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocume
return nil, notImplemented("WillSaveWaitUntil") return nil, notImplemented("WillSaveWaitUntil")
} }
func (s *Server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error { func (s *Server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
return nil // ignore return s.didSave(ctx, params)
} }
func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
uri := span.NewURI(params.TextDocument.URI) return s.didClose(ctx, params)
view := s.findView(ctx, uri)
return view.SetContent(ctx, uri, nil)
} }
// Language Features
func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
return s.completion(ctx, params) return s.completion(ctx, params)
} }
@ -338,134 +165,19 @@ func (s *Server) CompletionResolve(context.Context, *protocol.CompletionItem) (*
} }
func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) { func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
uri := span.NewURI(params.TextDocument.URI) return s.hover(ctx, params)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
identRange, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
ident, err := source.Identifier(ctx, view, f, identRange.Start)
if err != nil {
return nil, err
}
content, err := ident.Hover(ctx, nil)
if err != nil {
return nil, err
}
markdown := "```go\n" + content + "\n```"
identSpan, err := ident.Range.Span()
if err != nil {
return nil, err
}
rng, err := m.Range(identSpan)
if err != nil {
return nil, err
}
return &protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.Markdown,
Value: markdown,
},
Range: &rng,
}, nil
} }
func (s *Server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) { func (s *Server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
uri := span.NewURI(params.TextDocument.URI) return s.signatureHelp(ctx, params)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
info, err := source.SignatureHelp(ctx, f, rng.Start)
if err != nil {
s.log.Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
}
return toProtocolSignatureHelp(info), nil
} }
func (s *Server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { func (s *Server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
uri := span.NewURI(params.TextDocument.URI) return s.definition(ctx, params)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
ident, err := source.Identifier(ctx, view, f, rng.Start)
if err != nil {
return nil, err
}
decSpan, err := ident.Declaration.Range.Span()
if err != nil {
return nil, err
}
_, decM, err := newColumnMap(ctx, view, decSpan.URI())
if err != nil {
return nil, err
}
loc, err := decM.Location(decSpan)
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
} }
func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
uri := span.NewURI(params.TextDocument.URI) return s.typeDefinition(ctx, params)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
ident, err := source.Identifier(ctx, view, f, rng.Start)
if err != nil {
return nil, err
}
identSpan, err := ident.Type.Range.Span()
if err != nil {
return nil, err
}
_, identM, err := newColumnMap(ctx, view, identSpan.URI())
if err != nil {
return nil, err
}
loc, err := identM.Location(identSpan)
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
} }
func (s *Server) Implementation(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { func (s *Server) Implementation(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
@ -477,33 +189,11 @@ func (s *Server) References(context.Context, *protocol.ReferenceParams) ([]proto
} }
func (s *Server) DocumentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) { func (s *Server) DocumentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
uri := span.NewURI(params.TextDocument.URI) return s.documentHighlight(ctx, params)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
spans := source.Highlight(ctx, f, rng.Start)
return toProtocolHighlight(m, spans), nil
} }
func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) { func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
uri := span.NewURI(params.TextDocument.URI) return s.documentSymbol(ctx, params)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
symbols := source.DocumentSymbols(ctx, f)
return toProtocolDocumentSymbols(m, symbols), nil
} }
func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
@ -535,24 +225,11 @@ func (s *Server) ColorPresentation(context.Context, *protocol.ColorPresentationP
} }
func (s *Server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { func (s *Server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
uri := span.NewURI(params.TextDocument.URI) return s.formatting(ctx, params)
view := s.findView(ctx, uri)
spn := span.New(uri, span.Point{}, span.Point{})
return formatRange(ctx, view, spn)
} }
func (s *Server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { func (s *Server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
uri := span.NewURI(params.TextDocument.URI) return s.rangeFormatting(ctx, params)
view := s.findView(ctx, uri)
_, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.RangeSpan(params.Range)
if err != nil {
return nil, err
}
return formatRange(ctx, view, spn)
} }
func (s *Server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) { func (s *Server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {
@ -567,67 +244,6 @@ func (s *Server) FoldingRanges(context.Context, *protocol.FoldingRangeParams) ([
return nil, notImplemented("FoldingRanges") return nil, notImplemented("FoldingRanges")
} }
func (s *Server) processConfig(view *cache.View, 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 fmt.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 fmt.Errorf("invalid config gopls.env type %T", env)
}
for k, v := range menv {
view.Config.Env = applyEnv(view.Config.Env, k, v)
}
}
// Check if placeholders are enabled.
if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
s.usePlaceholders = usePlaceholders
}
return nil
}
func applyEnv(env []string, k string, v interface{}) []string {
prefix := k + "="
value := prefix + fmt.Sprint(v)
for i, s := range env {
if strings.HasPrefix(s, prefix) {
env[i] = value
return env
}
}
return append(env, value)
}
func notImplemented(method string) *jsonrpc2.Error { func notImplemented(method string) *jsonrpc2.Error {
return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method) return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method)
} }
func (s *Server) findView(ctx context.Context, uri span.URI) *cache.View {
// first see if a view already has this file
for _, view := range s.views {
if view.FindFile(ctx, uri) != nil {
return view
}
}
var longest *cache.View
for _, view := range s.views {
if longest != nil && len(longest.Folder) > len(view.Folder) {
continue
}
if strings.HasPrefix(string(uri), string(view.Folder)) {
longest = view
}
}
if longest != nil {
return longest
}
//TODO: are there any more heuristics we can use?
return s.views[0]
}

View File

@ -5,10 +5,35 @@
package lsp package lsp
import ( import (
"context"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
) )
func (s *Server) signatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(params.Position)
if err != nil {
return nil, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
info, err := source.SignatureHelp(ctx, f, rng.Start)
if err != nil {
s.log.Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
}
return toProtocolSignatureHelp(info), nil
}
func toProtocolSignatureHelp(info *source.SignatureInformation) *protocol.SignatureHelp { func toProtocolSignatureHelp(info *source.SignatureInformation) *protocol.SignatureHelp {
if info == nil { if info == nil {
return &protocol.SignatureHelp{} return &protocol.SignatureHelp{}

View File

@ -5,10 +5,24 @@
package lsp package lsp
import ( import (
"context"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
) )
func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
f, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return nil, err
}
symbols := source.DocumentSymbols(ctx, f)
return toProtocolDocumentSymbols(m, symbols), nil
}
func toProtocolDocumentSymbols(m *protocol.ColumnMapper, symbols []source.Symbol) []protocol.DocumentSymbol { func toProtocolDocumentSymbols(m *protocol.ColumnMapper, symbols []source.Symbol) []protocol.DocumentSymbol {
result := make([]protocol.DocumentSymbol, 0, len(symbols)) result := make([]protocol.DocumentSymbol, 0, len(symbols))
for _, s := range symbols { for _, s := range symbols {

View File

@ -0,0 +1,86 @@
// 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"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
)
func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
if len(params.ContentChanges) < 1 {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
}
var text string
switch s.textDocumentSyncKind {
case protocol.Incremental:
var err error
text, err = s.applyChanges(ctx, params)
if err != nil {
return err
}
case protocol.Full:
// We expect the full content of file, i.e. a single change with no range.
change := params.ContentChanges[0]
if change.RangeLength != 0 {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
}
text = change.Text
}
return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), text)
}
func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) {
if len(params.ContentChanges) == 1 && params.ContentChanges[0].Range == nil {
// If range is empty, we expect the full content of file, i.e. a single change with no range.
change := params.ContentChanges[0]
if change.RangeLength != 0 {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
}
return change.Text, nil
}
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
file, m, err := newColumnMap(ctx, view, uri)
if err != nil {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
}
content := file.GetContent(ctx)
for _, change := range params.ContentChanges {
spn, err := m.RangeSpan(*change.Range)
if err != nil {
return "", err
}
if !spn.HasOffset() {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
}
start, end := spn.Start().Offset(), spn.End().Offset()
if end <= start {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
}
var buf bytes.Buffer
buf.Write(content[:start])
buf.WriteString(change.Text)
buf.Write(content[end:])
content = buf.Bytes()
}
return string(content), nil
}
func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
return nil // ignore
}
func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
return view.SetContent(ctx, uri, nil)
}

54
internal/lsp/util.go Normal file
View File

@ -0,0 +1,54 @@
// 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 (
"context"
"fmt"
"strings"
"golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
// findView returns the view corresponding to the given URI.
// If the file is not already associated with a view, pick one using some heuristics.
func (s *Server) findView(ctx context.Context, uri span.URI) *cache.View {
// first see if a view already has this file
for _, view := range s.views {
if view.FindFile(ctx, uri) != nil {
return view
}
}
var longest *cache.View
for _, view := range s.views {
if longest != nil && len(longest.Folder) > len(view.Folder) {
continue
}
if strings.HasPrefix(string(uri), string(view.Folder)) {
longest = view
}
}
if longest != nil {
return longest
}
//TODO: are there any more heuristics we can use?
return s.views[0]
}
func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
f, err := v.GetFile(ctx, uri)
if err != nil {
return nil, nil, err
}
tok := f.GetToken(ctx)
if tok == nil {
return nil, nil, fmt.Errorf("no file information for %v", f.URI())
}
m := protocol.NewColumnMapper(f.URI(), f.GetFileSet(ctx), tok, f.GetContent(ctx))
return f, m, nil
}