mirror of
https://github.com/golang/go
synced 2024-11-18 23:54:41 -07:00
ecd3fc4348
We use file identities pervasively throughout gopls. Prior to this change, the identity is the modification date of an unopened file, or the hash of an opened file. That means that opening a file changes its identity, which causes unnecessary churn in the cache. Unfortunately, there isn't an easy way to fix this. Changing the cache key to something else, such as the modification time, means that we won't unify cache entries if a change is made and then undone. The approach here is to read files eagerly in GetFile, so that we know their hashes immediately. That resolves the churn, but means that we do a ton of file IO at startup. Incidental changes: Remove the FileSystem interface; there was only one implementation and it added a fair amount of cruft. We have many other places that assume os.Stat and such work. Add direct accessors to FileHandle for URI, Kind, and Version. Most uses of (FileHandle).Identity were for stuff that we derive solely from the URI, and this helped me disentangle them. It is a *ton* of churn, though. I can revert it if you want. Change-Id: Ia2133bc527f71daf81c9d674951726a232ca5bc9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/237037 Run-TryBot: Heschi Kreinick <heschi@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
321 lines
9.7 KiB
Go
321 lines
9.7 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"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
|
|
"golang.org/x/tools/internal/event"
|
|
"golang.org/x/tools/internal/jsonrpc2"
|
|
"golang.org/x/tools/internal/lsp/debug"
|
|
"golang.org/x/tools/internal/lsp/debug/tag"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
"golang.org/x/tools/internal/lsp/source"
|
|
"golang.org/x/tools/internal/span"
|
|
)
|
|
|
|
func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
|
|
s.stateMu.Lock()
|
|
if s.state >= serverInitializing {
|
|
defer s.stateMu.Unlock()
|
|
return nil, fmt.Errorf("%w: initialize called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state)
|
|
}
|
|
s.state = serverInitializing
|
|
s.stateMu.Unlock()
|
|
|
|
s.supportsWorkDoneProgress = params.Capabilities.Window.WorkDoneProgress
|
|
s.inProgress = map[string]*WorkDone{}
|
|
|
|
options := s.session.Options()
|
|
defer func() { s.session.SetOptions(options) }()
|
|
|
|
// TODO: Handle results here.
|
|
source.SetOptions(&options, params.InitializationOptions)
|
|
options.ForClientCapabilities(params.Capabilities)
|
|
|
|
if !params.RootURI.SpanURI().IsFile() {
|
|
return nil, fmt.Errorf("unsupported URI scheme: %v (gopls only supports file URIs)", params.RootURI)
|
|
}
|
|
s.pendingFolders = params.WorkspaceFolders
|
|
if len(s.pendingFolders) == 0 {
|
|
if params.RootURI != "" {
|
|
s.pendingFolders = []protocol.WorkspaceFolder{{
|
|
URI: string(params.RootURI),
|
|
Name: path.Base(params.RootURI.SpanURI().Filename()),
|
|
}}
|
|
} else {
|
|
// No folders and no root--we are in single file mode.
|
|
// TODO: https://golang.org/issue/34160.
|
|
return nil, errors.New("gopls does not yet support editing a single file. Please open a directory.")
|
|
}
|
|
}
|
|
|
|
var codeActionProvider interface{} = true
|
|
if ca := params.Capabilities.TextDocument.CodeAction; len(ca.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(),
|
|
}
|
|
}
|
|
var renameOpts interface{} = true
|
|
if r := params.Capabilities.TextDocument.Rename; r.PrepareSupport {
|
|
renameOpts = protocol.RenameOptions{
|
|
PrepareProvider: r.PrepareSupport,
|
|
}
|
|
}
|
|
|
|
goplsVer := &bytes.Buffer{}
|
|
debug.PrintVersionInfo(ctx, goplsVer, true, debug.PlainText)
|
|
|
|
return &protocol.InitializeResult{
|
|
Capabilities: protocol.ServerCapabilities{
|
|
CodeActionProvider: codeActionProvider,
|
|
CompletionProvider: protocol.CompletionOptions{
|
|
TriggerCharacters: []string{"."},
|
|
},
|
|
DefinitionProvider: true,
|
|
TypeDefinitionProvider: true,
|
|
ImplementationProvider: true,
|
|
DocumentFormattingProvider: true,
|
|
DocumentSymbolProvider: true,
|
|
WorkspaceSymbolProvider: true,
|
|
ExecuteCommandProvider: protocol.ExecuteCommandOptions{
|
|
Commands: options.SupportedCommands,
|
|
},
|
|
FoldingRangeProvider: true,
|
|
HoverProvider: true,
|
|
DocumentHighlightProvider: true,
|
|
DocumentLinkProvider: protocol.DocumentLinkOptions{},
|
|
ReferencesProvider: true,
|
|
RenameProvider: renameOpts,
|
|
SignatureHelpProvider: protocol.SignatureHelpOptions{
|
|
TriggerCharacters: []string{"(", ","},
|
|
},
|
|
TextDocumentSync: &protocol.TextDocumentSyncOptions{
|
|
Change: protocol.Incremental,
|
|
OpenClose: true,
|
|
Save: protocol.SaveOptions{
|
|
IncludeText: false,
|
|
},
|
|
},
|
|
Workspace: protocol.WorkspaceGn{
|
|
WorkspaceFolders: protocol.WorkspaceFoldersGn{
|
|
Supported: true,
|
|
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
|
|
},
|
|
},
|
|
},
|
|
ServerInfo: struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version,omitempty"`
|
|
}{
|
|
Name: "gopls",
|
|
Version: goplsVer.String(),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error {
|
|
s.stateMu.Lock()
|
|
if s.state >= serverInitialized {
|
|
defer s.stateMu.Unlock()
|
|
return fmt.Errorf("%w: initalized called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state)
|
|
}
|
|
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.DynamicWatchedFilesSupported {
|
|
registrations = append(registrations, protocol.Registration{
|
|
ID: "workspace/didChangeWatchedFiles",
|
|
Method: "workspace/didChangeWatchedFiles",
|
|
RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{
|
|
Watchers: []protocol.FileSystemWatcher{{
|
|
GlobPattern: "**/*.go",
|
|
Kind: float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate),
|
|
}},
|
|
},
|
|
})
|
|
}
|
|
|
|
if len(registrations) > 0 {
|
|
s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
|
|
Registrations: registrations,
|
|
})
|
|
}
|
|
|
|
// TODO: this event logging may be unnecessary. The version info is included in the initialize response.
|
|
buf := &bytes.Buffer{}
|
|
debug.PrintVersionInfo(ctx, buf, true, debug.PlainText)
|
|
event.Log(ctx, buf.String())
|
|
|
|
s.addFolders(ctx, s.pendingFolders)
|
|
s.pendingFolders = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) {
|
|
originalViews := len(s.session.Views())
|
|
viewErrors := make(map[span.URI]error)
|
|
|
|
for _, folder := range folders {
|
|
uri := span.URIFromURI(folder.URI)
|
|
view, snapshot, err := s.addView(ctx, folder.Name, uri)
|
|
if err != nil {
|
|
viewErrors[uri] = err
|
|
continue
|
|
}
|
|
// Print each view's environment.
|
|
buf := &bytes.Buffer{}
|
|
if err := view.WriteEnv(ctx, buf); err != nil {
|
|
event.Error(ctx, "failed to write environment", err, tag.Directory.Of(view.Folder()))
|
|
continue
|
|
}
|
|
event.Log(ctx, buf.String())
|
|
|
|
// Diagnose the newly created view.
|
|
go s.diagnoseDetached(snapshot)
|
|
}
|
|
if len(viewErrors) > 0 {
|
|
errMsg := fmt.Sprintf("Error loading workspace folders (expected %v, got %v)\n", len(folders), len(s.session.Views())-originalViews)
|
|
for uri, err := range viewErrors {
|
|
errMsg += fmt.Sprintf("failed to load view for %s: %v\n", uri, err)
|
|
}
|
|
s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
|
|
Type: protocol.Error,
|
|
Message: errMsg,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, o *source.Options) error {
|
|
if !s.session.Options().ConfigurationSupported {
|
|
return nil
|
|
}
|
|
v := protocol.ParamConfiguration{
|
|
ConfigurationParams: protocol.ConfigurationParams{
|
|
Items: []protocol.ConfigurationItem{{
|
|
ScopeURI: string(folder),
|
|
Section: "gopls",
|
|
}, {
|
|
ScopeURI: string(folder),
|
|
Section: fmt.Sprintf("gopls-%s", name),
|
|
}},
|
|
},
|
|
}
|
|
configs, err := s.client.Configuration(ctx, &v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, config := range configs {
|
|
results := source.SetOptions(o, config)
|
|
for _, result := range results {
|
|
if result.Error != nil {
|
|
s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
|
|
Type: protocol.Error,
|
|
Message: result.Error.Error(),
|
|
})
|
|
}
|
|
switch result.State {
|
|
case source.OptionUnexpected:
|
|
s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
|
|
Type: protocol.Error,
|
|
Message: fmt.Sprintf("unexpected config %s", result.Name),
|
|
})
|
|
case source.OptionDeprecated:
|
|
msg := fmt.Sprintf("config %s is deprecated", result.Name)
|
|
if result.Replacement != "" {
|
|
msg = fmt.Sprintf("%s, use %s instead", msg, result.Replacement)
|
|
}
|
|
s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
|
|
Type: protocol.Warning,
|
|
Message: msg,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// beginFileRequest checks preconditions for a file-oriented request and routes
|
|
// it to a snapshot.
|
|
// We don't want to return errors for benign conditions like wrong file type,
|
|
// so callers should do if !ok { return err } rather than if err != nil.
|
|
func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, error) {
|
|
uri := pURI.SpanURI()
|
|
if !uri.IsFile() {
|
|
// Not a file URI. Stop processing the request, but don't return an error.
|
|
return nil, nil, false, nil
|
|
}
|
|
view, err := s.session.ViewOf(uri)
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
snapshot := view.Snapshot()
|
|
fh, err := snapshot.GetFile(ctx, uri)
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
if expectKind != source.UnknownKind && fh.Kind() != expectKind {
|
|
// Wrong kind of file. Nothing to do.
|
|
return nil, nil, false, nil
|
|
}
|
|
return snapshot, fh, true, nil
|
|
}
|
|
|
|
func (s *Server) shutdown(ctx context.Context) error {
|
|
s.stateMu.Lock()
|
|
defer s.stateMu.Unlock()
|
|
if s.state < serverInitialized {
|
|
event.Log(ctx, "server shutdown without initialization")
|
|
}
|
|
if s.state != serverShutDown {
|
|
// 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()
|
|
//TODO: would be nice to have a better way to find the conn close method
|
|
s.client.(io.Closer).Close()
|
|
if s.state != serverShutDown {
|
|
//TODO: we shoud be able to do better than this
|
|
os.Exit(1)
|
|
}
|
|
// we don't terminate the process on a normal exit, we just allow it to
|
|
// close naturally if needed after the connection is closed.
|
|
return nil
|
|
}
|