github.com/jd-ly/tools@v0.5.7/internal/lsp/cmd/cmd.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package cmd handles the gopls command line.
     6  // It contains a handler for each of the modes, along with all the flag handling
     7  // and the command line output format.
     8  package cmd
     9  
    10  import (
    11  	"context"
    12  	"flag"
    13  	"fmt"
    14  	"go/token"
    15  	"io/ioutil"
    16  	"log"
    17  	"os"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/jd-ly/tools/internal/jsonrpc2"
    23  	"github.com/jd-ly/tools/internal/lsp"
    24  	"github.com/jd-ly/tools/internal/lsp/cache"
    25  	"github.com/jd-ly/tools/internal/lsp/debug"
    26  	"github.com/jd-ly/tools/internal/lsp/lsprpc"
    27  	"github.com/jd-ly/tools/internal/lsp/protocol"
    28  	"github.com/jd-ly/tools/internal/lsp/source"
    29  	"github.com/jd-ly/tools/internal/span"
    30  	"github.com/jd-ly/tools/internal/tool"
    31  	"github.com/jd-ly/tools/internal/xcontext"
    32  	errors "golang.org/x/xerrors"
    33  )
    34  
    35  // Application is the main application as passed to tool.Main
    36  // It handles the main command line parsing and dispatch to the sub commands.
    37  type Application struct {
    38  	// Core application flags
    39  
    40  	// Embed the basic profiling flags supported by the tool package
    41  	tool.Profile
    42  
    43  	// We include the server configuration directly for now, so the flags work
    44  	// even without the verb.
    45  	// TODO: Remove this when we stop allowing the serve verb by default.
    46  	Serve Serve
    47  
    48  	// the options configuring function to invoke when building a server
    49  	options func(*source.Options)
    50  
    51  	// The name of the binary, used in help and telemetry.
    52  	name string
    53  
    54  	// The working directory to run commands in.
    55  	wd string
    56  
    57  	// The environment variables to use.
    58  	env []string
    59  
    60  	// Support for remote LSP server.
    61  	Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."`
    62  
    63  	// Verbose enables verbose logging.
    64  	Verbose bool `flag:"v" help:"verbose output"`
    65  
    66  	// VeryVerbose enables a higher level of verbosity in logging output.
    67  	VeryVerbose bool `flag:"vv" help:"very verbose output"`
    68  
    69  	// Control ocagent export of telemetry
    70  	OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"`
    71  
    72  	// PrepareOptions is called to update the options when a new view is built.
    73  	// It is primarily to allow the behavior of gopls to be modified by hooks.
    74  	PrepareOptions func(*source.Options)
    75  }
    76  
    77  func (app *Application) verbose() bool {
    78  	return app.Verbose || app.VeryVerbose
    79  }
    80  
    81  // New returns a new Application ready to run.
    82  func New(name, wd string, env []string, options func(*source.Options)) *Application {
    83  	if wd == "" {
    84  		wd, _ = os.Getwd()
    85  	}
    86  	app := &Application{
    87  		options: options,
    88  		name:    name,
    89  		wd:      wd,
    90  		env:     env,
    91  		OCAgent: "off", //TODO: Remove this line to default the exporter to on
    92  
    93  		Serve: Serve{
    94  			RemoteListenTimeout: 1 * time.Minute,
    95  		},
    96  	}
    97  	return app
    98  }
    99  
   100  // Name implements tool.Application returning the binary name.
   101  func (app *Application) Name() string { return app.name }
   102  
   103  // Usage implements tool.Application returning empty extra argument usage.
   104  func (app *Application) Usage() string { return "<command> [command-flags] [command-args]" }
   105  
   106  // ShortHelp implements tool.Application returning the main binary help.
   107  func (app *Application) ShortHelp() string {
   108  	return "The Go Language source tools."
   109  }
   110  
   111  // DetailedHelp implements tool.Application returning the main binary help.
   112  // This includes the short help for all the sub commands.
   113  func (app *Application) DetailedHelp(f *flag.FlagSet) {
   114  	fmt.Fprint(f.Output(), `
   115  gopls is a Go language server. It is typically used with an editor to provide
   116  language features. When no command is specified, gopls will default to the 'serve'
   117  command. The language features can also be accessed via the gopls command-line interface.
   118  
   119  Available commands are:
   120  `)
   121  	fmt.Fprint(f.Output(), `
   122  main:
   123  `)
   124  	for _, c := range app.mainCommands() {
   125  		fmt.Fprintf(f.Output(), "  %s : %v\n", c.Name(), c.ShortHelp())
   126  	}
   127  	fmt.Fprint(f.Output(), `
   128  features:
   129  `)
   130  	for _, c := range app.featureCommands() {
   131  		fmt.Fprintf(f.Output(), "  %s : %v\n", c.Name(), c.ShortHelp())
   132  	}
   133  	fmt.Fprint(f.Output(), `
   134  gopls flags are:
   135  `)
   136  	f.PrintDefaults()
   137  }
   138  
   139  // Run takes the args after top level flag processing, and invokes the correct
   140  // sub command as specified by the first argument.
   141  // If no arguments are passed it will invoke the server sub command, as a
   142  // temporary measure for compatibility.
   143  func (app *Application) Run(ctx context.Context, args ...string) error {
   144  	ctx = debug.WithInstance(ctx, app.wd, app.OCAgent)
   145  	app.Serve.app = app
   146  	if len(args) == 0 {
   147  		return tool.Run(ctx, &app.Serve, args)
   148  	}
   149  	command, args := args[0], args[1:]
   150  	for _, c := range app.commands() {
   151  		if c.Name() == command {
   152  			return tool.Run(ctx, c, args)
   153  		}
   154  	}
   155  	return tool.CommandLineErrorf("Unknown command %v", command)
   156  }
   157  
   158  // commands returns the set of commands supported by the gopls tool on the
   159  // command line.
   160  // The command is specified by the first non flag argument.
   161  func (app *Application) commands() []tool.Application {
   162  	var commands []tool.Application
   163  	commands = append(commands, app.mainCommands()...)
   164  	commands = append(commands, app.featureCommands()...)
   165  	return commands
   166  }
   167  
   168  func (app *Application) mainCommands() []tool.Application {
   169  	return []tool.Application{
   170  		&app.Serve,
   171  		&version{app: app},
   172  		&bug{},
   173  		&apiJSON{},
   174  	}
   175  }
   176  
   177  func (app *Application) featureCommands() []tool.Application {
   178  	return []tool.Application{
   179  		&callHierarchy{app: app},
   180  		&check{app: app},
   181  		&definition{app: app},
   182  		&foldingRanges{app: app},
   183  		&format{app: app},
   184  		&highlight{app: app},
   185  		&implementation{app: app},
   186  		&imports{app: app},
   187  		&inspect{app: app},
   188  		&links{app: app},
   189  		&prepareRename{app: app},
   190  		&references{app: app},
   191  		&rename{app: app},
   192  		&semtok{app: app},
   193  		&signature{app: app},
   194  		&suggestedFix{app: app},
   195  		&symbols{app: app},
   196  		&workspace{app: app},
   197  		&workspaceSymbol{app: app},
   198  	}
   199  }
   200  
   201  var (
   202  	internalMu          sync.Mutex
   203  	internalConnections = make(map[string]*connection)
   204  )
   205  
   206  func (app *Application) connect(ctx context.Context) (*connection, error) {
   207  	switch {
   208  	case app.Remote == "":
   209  		connection := newConnection(app)
   210  		connection.Server = lsp.NewServer(cache.New(ctx, app.options).NewSession(ctx), connection.Client)
   211  		ctx = protocol.WithClient(ctx, connection.Client)
   212  		return connection, connection.initialize(ctx, app.options)
   213  	case strings.HasPrefix(app.Remote, "internal@"):
   214  		internalMu.Lock()
   215  		defer internalMu.Unlock()
   216  		opts := source.DefaultOptions().Clone()
   217  		if app.options != nil {
   218  			app.options(opts)
   219  		}
   220  		key := fmt.Sprintf("%s %v", app.wd, opts)
   221  		if c := internalConnections[key]; c != nil {
   222  			return c, nil
   223  		}
   224  		remote := app.Remote[len("internal@"):]
   225  		ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server
   226  		connection, err := app.connectRemote(ctx, remote)
   227  		if err != nil {
   228  			return nil, err
   229  		}
   230  		internalConnections[key] = connection
   231  		return connection, nil
   232  	default:
   233  		return app.connectRemote(ctx, app.Remote)
   234  	}
   235  }
   236  
   237  // CloseTestConnections terminates shared connections used in command tests. It
   238  // should only be called from tests.
   239  func CloseTestConnections(ctx context.Context) {
   240  	for _, c := range internalConnections {
   241  		c.Shutdown(ctx)
   242  		c.Exit(ctx)
   243  	}
   244  }
   245  
   246  func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
   247  	connection := newConnection(app)
   248  	network, addr := parseAddr(remote)
   249  	conn, err := lsprpc.ConnectToRemote(ctx, network, addr)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	stream := jsonrpc2.NewHeaderStream(conn)
   254  	cc := jsonrpc2.NewConn(stream)
   255  	connection.Server = protocol.ServerDispatcher(cc)
   256  	ctx = protocol.WithClient(ctx, connection.Client)
   257  	cc.Go(ctx,
   258  		protocol.Handlers(
   259  			protocol.ClientHandler(connection.Client,
   260  				jsonrpc2.MethodNotFound)))
   261  	return connection, connection.initialize(ctx, app.options)
   262  }
   263  
   264  var matcherString = map[source.SymbolMatcher]string{
   265  	source.SymbolFuzzy:           "fuzzy",
   266  	source.SymbolCaseSensitive:   "caseSensitive",
   267  	source.SymbolCaseInsensitive: "caseInsensitive",
   268  }
   269  
   270  func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
   271  	params := &protocol.ParamInitialize{}
   272  	params.RootURI = protocol.URIFromPath(c.Client.app.wd)
   273  	params.Capabilities.Workspace.Configuration = true
   274  
   275  	// Make sure to respect configured options when sending initialize request.
   276  	opts := source.DefaultOptions().Clone()
   277  	if options != nil {
   278  		options(opts)
   279  	}
   280  	params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{
   281  		ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
   282  	}
   283  	params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport
   284  	params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{}
   285  	params.Capabilities.TextDocument.SemanticTokens.Formats = []string{"relative"}
   286  	params.Capabilities.TextDocument.SemanticTokens.Requests.Range = true
   287  	params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
   288  	params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes()
   289  	params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers()
   290  	params.InitializationOptions = map[string]interface{}{
   291  		"symbolMatcher": matcherString[opts.SymbolMatcher],
   292  	}
   293  	if _, err := c.Server.Initialize(ctx, params); err != nil {
   294  		return err
   295  	}
   296  	if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
   297  		return err
   298  	}
   299  	return nil
   300  }
   301  
   302  type connection struct {
   303  	protocol.Server
   304  	Client *cmdClient
   305  }
   306  
   307  type cmdClient struct {
   308  	protocol.Server
   309  	app  *Application
   310  	fset *token.FileSet
   311  
   312  	diagnosticsMu   sync.Mutex
   313  	diagnosticsDone chan struct{}
   314  
   315  	filesMu sync.Mutex
   316  	files   map[span.URI]*cmdFile
   317  }
   318  
   319  type cmdFile struct {
   320  	uri         span.URI
   321  	mapper      *protocol.ColumnMapper
   322  	err         error
   323  	added       bool
   324  	diagnostics []protocol.Diagnostic
   325  }
   326  
   327  func newConnection(app *Application) *connection {
   328  	return &connection{
   329  		Client: &cmdClient{
   330  			app:   app,
   331  			fset:  token.NewFileSet(),
   332  			files: make(map[span.URI]*cmdFile),
   333  		},
   334  	}
   335  }
   336  
   337  // fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file.
   338  func fileURI(uri protocol.DocumentURI) span.URI {
   339  	sURI := uri.SpanURI()
   340  	if !sURI.IsFile() {
   341  		panic(fmt.Sprintf("%q is not a file URI", uri))
   342  	}
   343  	return sURI
   344  }
   345  
   346  func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil }
   347  
   348  func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
   349  	return nil, nil
   350  }
   351  
   352  func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error {
   353  	switch p.Type {
   354  	case protocol.Error:
   355  		log.Print("Error:", p.Message)
   356  	case protocol.Warning:
   357  		log.Print("Warning:", p.Message)
   358  	case protocol.Info:
   359  		if c.app.verbose() {
   360  			log.Print("Info:", p.Message)
   361  		}
   362  	case protocol.Log:
   363  		if c.app.verbose() {
   364  			log.Print("Log:", p.Message)
   365  		}
   366  	default:
   367  		if c.app.verbose() {
   368  			log.Print(p.Message)
   369  		}
   370  	}
   371  	return nil
   372  }
   373  
   374  func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil }
   375  
   376  func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error {
   377  	return nil
   378  }
   379  
   380  func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error {
   381  	return nil
   382  }
   383  
   384  func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) {
   385  	return nil, nil
   386  }
   387  
   388  func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
   389  	results := make([]interface{}, len(p.Items))
   390  	for i, item := range p.Items {
   391  		if item.Section != "gopls" {
   392  			continue
   393  		}
   394  		env := map[string]interface{}{}
   395  		for _, value := range c.app.env {
   396  			l := strings.SplitN(value, "=", 2)
   397  			if len(l) != 2 {
   398  				continue
   399  			}
   400  			env[l[0]] = l[1]
   401  		}
   402  		m := map[string]interface{}{
   403  			"env": env,
   404  			"analyses": map[string]bool{
   405  				"fillreturns":    true,
   406  				"nonewvars":      true,
   407  				"noresultvalues": true,
   408  				"undeclaredname": true,
   409  			},
   410  		}
   411  		if c.app.VeryVerbose {
   412  			m["verboseOutput"] = true
   413  		}
   414  		results[i] = m
   415  	}
   416  	return results, nil
   417  }
   418  
   419  func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) {
   420  	return &protocol.ApplyWorkspaceEditResponse{Applied: false, FailureReason: "not implemented"}, nil
   421  }
   422  
   423  func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
   424  	if p.URI == "gopls://diagnostics-done" {
   425  		close(c.diagnosticsDone)
   426  	}
   427  	// Don't worry about diagnostics without versions.
   428  	if p.Version == 0 {
   429  		return nil
   430  	}
   431  
   432  	c.filesMu.Lock()
   433  	defer c.filesMu.Unlock()
   434  
   435  	file := c.getFile(ctx, fileURI(p.URI))
   436  	file.diagnostics = p.Diagnostics
   437  	return nil
   438  }
   439  
   440  func (c *cmdClient) Progress(context.Context, *protocol.ProgressParams) error {
   441  	return nil
   442  }
   443  
   444  func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error {
   445  	return nil
   446  }
   447  
   448  func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile {
   449  	file, found := c.files[uri]
   450  	if !found || file.err != nil {
   451  		file = &cmdFile{
   452  			uri: uri,
   453  		}
   454  		c.files[uri] = file
   455  	}
   456  	if file.mapper == nil {
   457  		fname := uri.Filename()
   458  		content, err := ioutil.ReadFile(fname)
   459  		if err != nil {
   460  			file.err = errors.Errorf("getFile: %v: %v", uri, err)
   461  			return file
   462  		}
   463  		f := c.fset.AddFile(fname, -1, len(content))
   464  		f.SetLinesForContent(content)
   465  		converter := span.NewContentConverter(fname, content)
   466  		file.mapper = &protocol.ColumnMapper{
   467  			URI:       uri,
   468  			Converter: converter,
   469  			Content:   content,
   470  		}
   471  	}
   472  	return file
   473  }
   474  
   475  func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile {
   476  	c.Client.filesMu.Lock()
   477  	defer c.Client.filesMu.Unlock()
   478  
   479  	file := c.Client.getFile(ctx, uri)
   480  	// This should never happen.
   481  	if file == nil {
   482  		return &cmdFile{
   483  			uri: uri,
   484  			err: fmt.Errorf("no file found for %s", uri),
   485  		}
   486  	}
   487  	if file.err != nil || file.added {
   488  		return file
   489  	}
   490  	file.added = true
   491  	p := &protocol.DidOpenTextDocumentParams{
   492  		TextDocument: protocol.TextDocumentItem{
   493  			URI:        protocol.URIFromSpanURI(uri),
   494  			LanguageID: source.DetectLanguage("", file.uri.Filename()).String(),
   495  			Version:    1,
   496  			Text:       string(file.mapper.Content),
   497  		},
   498  	}
   499  	if err := c.Server.DidOpen(ctx, p); err != nil {
   500  		file.err = errors.Errorf("%v: %v", uri, err)
   501  	}
   502  	return file
   503  }
   504  
   505  func (c *connection) semanticTokens(ctx context.Context, file span.URI) (*protocol.SemanticTokens, error) {
   506  	p := &protocol.SemanticTokensParams{
   507  		TextDocument: protocol.TextDocumentIdentifier{
   508  			URI: protocol.URIFromSpanURI(file),
   509  		},
   510  	}
   511  	resp, err := c.Server.SemanticTokensFull(ctx, p)
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  	return resp, nil
   516  }
   517  
   518  func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error {
   519  	var untypedFiles []interface{}
   520  	for _, file := range files {
   521  		untypedFiles = append(untypedFiles, string(file))
   522  	}
   523  	c.Client.diagnosticsMu.Lock()
   524  	defer c.Client.diagnosticsMu.Unlock()
   525  
   526  	c.Client.diagnosticsDone = make(chan struct{})
   527  	_, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles})
   528  	<-c.Client.diagnosticsDone
   529  	return err
   530  }
   531  
   532  func (c *connection) terminate(ctx context.Context) {
   533  	if strings.HasPrefix(c.Client.app.Remote, "internal@") {
   534  		// internal connections need to be left alive for the next test
   535  		return
   536  	}
   537  	//TODO: do we need to handle errors on these calls?
   538  	c.Shutdown(ctx)
   539  	//TODO: right now calling exit terminates the process, we should rethink that
   540  	//server.Exit(ctx)
   541  }
   542  
   543  // Implement io.Closer.
   544  func (c *cmdClient) Close() error {
   545  	return nil
   546  }