diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go new file mode 100644 index 00000000..5a1fe948 --- /dev/null +++ b/internal/lsp/definition.go @@ -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 +} diff --git a/internal/lsp/format.go b/internal/lsp/format.go index 0277f5ce..409d048a 100644 --- a/internal/lsp/format.go +++ b/internal/lsp/format.go @@ -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 import ( @@ -9,6 +13,27 @@ import ( "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. func formatRange(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) { f, m, err := newColumnMap(ctx, v, s.URI()) @@ -69,16 +94,3 @@ func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]s } 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 -} diff --git a/internal/lsp/general.go b/internal/lsp/general.go new file mode 100644 index 00000000..ffe5da60 --- /dev/null +++ b/internal/lsp/general.go @@ -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 +} diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go index 403ac281..288587d5 100644 --- a/internal/lsp/highlight.go +++ b/internal/lsp/highlight.go @@ -5,10 +5,32 @@ 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) 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 { result := make([]protocol.DocumentHighlight, 0, len(spans)) kind := protocol.Text diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go new file mode 100644 index 00000000..8348251b --- /dev/null +++ b/internal/lsp/hover.go @@ -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 +} diff --git a/internal/lsp/protocol/server.go b/internal/lsp/protocol/server.go index 3344b538..6eb7747c 100644 --- a/internal/lsp/protocol/server.go +++ b/internal/lsp/protocol/server.go @@ -20,7 +20,7 @@ type Server interface { DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error - Symbols(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) + Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error) DidOpen(context.Context, *DidOpenTextDocumentParams) 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) return } - resp, err := server.Symbols(ctx, ¶ms) + resp, err := server.Symbol(ctx, ¶ms) if err := conn.Reply(ctx, r, resp, err); err != nil { 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) } -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 if err := s.Conn.Call(ctx, "workspace/symbol", params, &result); err != nil { return nil, err diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 092df560..326d1913 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -5,19 +5,11 @@ package lsp import ( - "bytes" "context" "fmt" - "go/ast" - "go/parser" - "go/token" "net" - "os" - "path" - "strings" "sync" - "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" @@ -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 { Conn *jsonrpc2.Conn client protocol.Client log xlog.Logger initializedMu sync.Mutex - initialized bool // set once the server has received "initialize" request + isInitialized bool // set once the server has received "initialize" request // Configurations. // TODO(rstambler): Separate these into their own struct? @@ -92,139 +88,26 @@ type Server struct { undelivered map[span.URI][]source.Diagnostic } -func (s *Server) Run(ctx context.Context) error { - return s.Conn.Run(ctx) -} +// General func (s *Server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) { - s.initializedMu.Lock() - 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 + return s.initialize(ctx, params) } 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 + return s.initialized(ctx, params) } -func (s *Server) Shutdown(context.Context) error { - s.initializedMu.Lock() - defer s.initializedMu.Unlock() - if !s.initialized { - return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized") - } - s.initialized = false - return nil +func (s *Server) Shutdown(ctx context.Context) error { + return s.shutdown(ctx) } func (s *Server) Exit(ctx context.Context) error { - if s.initialized { - os.Exit(1) - } - os.Exit(0) - return nil + return s.exit(ctx) } +// Workspace + func (s *Server) DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error { return notImplemented("DidChangeWorkspaceFolders") } @@ -237,78 +120,22 @@ func (s *Server) DidChangeWatchedFiles(context.Context, *protocol.DidChangeWatch return notImplemented("DidChangeWatchedFiles") } -func (s *Server) Symbols(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { - return nil, notImplemented("Symbols") +func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { + return nil, notImplemented("Symbol") } func (s *Server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) (interface{}, error) { return nil, notImplemented("ExecuteCommand") } +// Text Synchronization + func (s *Server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { 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 { - 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) + return s.didChange(ctx, params) } 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") } -func (s *Server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error { - return nil // ignore +func (s *Server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { + return s.didSave(ctx, params) } 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) + return s.didClose(ctx, params) } +// Language Features + func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { 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) { - 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 + return s.hover(ctx, params) } 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 + return s.signatureHelp(ctx, params) } 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 + return s.definition(ctx, params) } 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 + return s.typeDefinition(ctx, params) } 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) { - 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 + return s.documentHighlight(ctx, params) } 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 + return s.documentSymbol(ctx, params) } 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) { - uri := span.NewURI(params.TextDocument.URI) - view := s.findView(ctx, uri) - spn := span.New(uri, span.Point{}, span.Point{}) - return formatRange(ctx, view, spn) + return s.formatting(ctx, params) } 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) + return s.rangeFormatting(ctx, params) } 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") } -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 { 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] -} diff --git a/internal/lsp/signature_help.go b/internal/lsp/signature_help.go index 7479db4d..74713212 100644 --- a/internal/lsp/signature_help.go +++ b/internal/lsp/signature_help.go @@ -5,10 +5,35 @@ 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) 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 { if info == nil { return &protocol.SignatureHelp{} diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go index 6ac09e0c..da57ec24 100644 --- a/internal/lsp/symbols.go +++ b/internal/lsp/symbols.go @@ -5,10 +5,24 @@ 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) 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 { result := make([]protocol.DocumentSymbol, 0, len(symbols)) for _, s := range symbols { diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go new file mode 100644 index 00000000..f7f6074b --- /dev/null +++ b/internal/lsp/text_synchronization.go @@ -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) +} diff --git a/internal/lsp/util.go b/internal/lsp/util.go new file mode 100644 index 00000000..e1c97adb --- /dev/null +++ b/internal/lsp/util.go @@ -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 +}