github.com/v2fly/tools@v0.100.0/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/v2fly/tools/internal/jsonrpc2"
    23  	"github.com/v2fly/tools/internal/lsp"
    24  	"github.com/v2fly/tools/internal/lsp/cache"
    25  	"github.com/v2fly/tools/internal/lsp/debug"
    26  	"github.com/v2fly/tools/internal/lsp/lsprpc"
    27  	"github.com/v2fly/tools/internal/lsp/protocol"
    28  	"github.com/v2fly/tools/internal/lsp/source"
    29  	"github.com/v2fly/tools/internal/span"
    30  	"github.com/v2fly/tools/internal/tool"
    31  	"github.com/v2fly/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  		&licenses{app: app},
   175  	}
   176  }
   177  
   178  func (app *Application) featureCommands() []tool.Application {
   179  	return []tool.Application{
   180  		&callHierarchy{app: app},
   181  		&check{app: app},
   182  		&definition{app: app},
   183  		&foldingRanges{app: app},
   184  		&format{app: app},
   185  		&highlight{app: app},
   186  		&implementation{app: app},
   187  		&imports{app: app},
   188  		newRemote(app, ""),
   189  		newRemote(app, "inspect"),
   190  		&links{app: app},
   191  		&prepareRename{app: app},
   192  		&references{app: app},
   193  		&rename{app: app},
   194  		&semtok{app: app},
   195  		&signature{app: app},
   196  		&suggestedFix{app: app},
   197  		&symbols{app: app},
   198  		newWorkspace(app),
   199  		&workspaceSymbol{app: app},
   200  	}
   201  }
   202  
   203  var (
   204  	internalMu          sync.Mutex
   205  	internalConnections = make(map[string]*connection)
   206  )
   207  
   208  func (app *Application) connect(ctx context.Context) (*connection, error) {
   209  	switch {
   210  	case app.Remote == "":
   211  		connection := newConnection(app)
   212  		connection.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), connection.Client)
   213  		ctx = protocol.WithClient(ctx, connection.Client)
   214  		return connection, connection.initialize(ctx, app.options)
   215  	case strings.HasPrefix(app.Remote, "internal@"):
   216  		internalMu.Lock()
   217  		defer internalMu.Unlock()
   218  		opts := source.DefaultOptions().Clone()
   219  		if app.options != nil {
   220  			app.options(opts)
   221  		}
   222  		key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher)
   223  		if c := internalConnections[key]; c != nil {
   224  			return c, nil
   225  		}
   226  		remote := app.Remote[len("internal@"):]
   227  		ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server
   228  		connection, err := app.connectRemote(ctx, remote)
   229  		if err != nil {
   230  			return nil, err
   231  		}
   232  		internalConnections[key] = connection
   233  		return connection, nil
   234  	default:
   235  		return app.connectRemote(ctx, app.Remote)
   236  	}
   237  }
   238  
   239  // CloseTestConnections terminates shared connections used in command tests. It
   240  // should only be called from tests.
   241  func CloseTestConnections(ctx context.Context) {
   242  	for _, c := range internalConnections {
   243  		c.Shutdown(ctx)
   244  		c.Exit(ctx)
   245  	}
   246  }
   247  
   248  func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
   249  	connection := newConnection(app)
   250  	conn, err := lsprpc.ConnectToRemote(ctx, remote)
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  	stream := jsonrpc2.NewHeaderStream(conn)
   255  	cc := jsonrpc2.NewConn(stream)
   256  	connection.Server = protocol.ServerDispatcher(cc)
   257  	ctx = protocol.WithClient(ctx, connection.Client)
   258  	cc.Go(ctx,
   259  		protocol.Handlers(
   260  			protocol.ClientHandler(connection.Client,
   261  				jsonrpc2.MethodNotFound)))
   262  	return connection, connection.initialize(ctx, app.options)
   263  }
   264  
   265  var matcherString = map[source.SymbolMatcher]string{
   266  	source.SymbolFuzzy:           "fuzzy",
   267  	source.SymbolCaseSensitive:   "caseSensitive",
   268  	source.SymbolCaseInsensitive: "caseInsensitive",
   269  }
   270  
   271  func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
   272  	params := &protocol.ParamInitialize{}
   273  	params.RootURI = protocol.URIFromPath(c.Client.app.wd)
   274  	params.Capabilities.Workspace.Configuration = true
   275  
   276  	// Make sure to respect configured options when sending initialize request.
   277  	opts := source.DefaultOptions().Clone()
   278  	if options != nil {
   279  		options(opts)
   280  	}
   281  	// If you add an additional option here, you must update the map key in connect.
   282  	params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{
   283  		ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
   284  	}
   285  	params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport
   286  	params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{}
   287  	params.Capabilities.TextDocument.SemanticTokens.Formats = []string{"relative"}
   288  	params.Capabilities.TextDocument.SemanticTokens.Requests.Range = true
   289  	params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
   290  	params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes()
   291  	params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers()
   292  	params.InitializationOptions = map[string]interface{}{
   293  		"symbolMatcher": matcherString[opts.SymbolMatcher],
   294  	}
   295  	if _, err := c.Server.Initialize(ctx, params); err != nil {
   296  		return err
   297  	}
   298  	if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
   299  		return err
   300  	}
   301  	return nil
   302  }
   303  
   304  type connection struct {
   305  	protocol.Server
   306  	Client *cmdClient
   307  }
   308  
   309  type cmdClient struct {
   310  	protocol.Server
   311  	app  *Application
   312  	fset *token.FileSet
   313  
   314  	diagnosticsMu   sync.Mutex
   315  	diagnosticsDone chan struct{}
   316  
   317  	filesMu sync.Mutex
   318  	files   map[span.URI]*cmdFile
   319  }
   320  
   321  type cmdFile struct {
   322  	uri         span.URI
   323  	mapper      *protocol.ColumnMapper
   324  	err         error
   325  	added       bool
   326  	diagnostics []protocol.Diagnostic
   327  }
   328  
   329  func newConnection(app *Application) *connection {
   330  	return &connection{
   331  		Client: &cmdClient{
   332  			app:   app,
   333  			fset:  token.NewFileSet(),
   334  			files: make(map[span.URI]*cmdFile),
   335  		},
   336  	}
   337  }
   338  
   339  // fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file.
   340  func fileURI(uri protocol.DocumentURI) span.URI {
   341  	sURI := uri.SpanURI()
   342  	if !sURI.IsFile() {
   343  		panic(fmt.Sprintf("%q is not a file URI", uri))
   344  	}
   345  	return sURI
   346  }
   347  
   348  func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil }
   349  
   350  func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
   351  	return nil, nil
   352  }
   353  
   354  func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error {
   355  	switch p.Type {
   356  	case protocol.Error:
   357  		log.Print("Error:", p.Message)
   358  	case protocol.Warning:
   359  		log.Print("Warning:", p.Message)
   360  	case protocol.Info:
   361  		if c.app.verbose() {
   362  			log.Print("Info:", p.Message)
   363  		}
   364  	case protocol.Log:
   365  		if c.app.verbose() {
   366  			log.Print("Log:", p.Message)
   367  		}
   368  	default:
   369  		if c.app.verbose() {
   370  			log.Print(p.Message)
   371  		}
   372  	}
   373  	return nil
   374  }
   375  
   376  func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil }
   377  
   378  func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error {
   379  	return nil
   380  }
   381  
   382  func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error {
   383  	return nil
   384  }
   385  
   386  func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) {
   387  	return nil, nil
   388  }
   389  
   390  func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
   391  	results := make([]interface{}, len(p.Items))
   392  	for i, item := range p.Items {
   393  		if item.Section != "gopls" {
   394  			continue
   395  		}
   396  		env := map[string]interface{}{}
   397  		for _, value := range c.app.env {
   398  			l := strings.SplitN(value, "=", 2)
   399  			if len(l) != 2 {
   400  				continue
   401  			}
   402  			env[l[0]] = l[1]
   403  		}
   404  		m := map[string]interface{}{
   405  			"env": env,
   406  			"analyses": map[string]bool{
   407  				"fillreturns":    true,
   408  				"nonewvars":      true,
   409  				"noresultvalues": true,
   410  				"undeclaredname": true,
   411  			},
   412  		}
   413  		if c.app.VeryVerbose {
   414  			m["verboseOutput"] = true
   415  		}
   416  		results[i] = m
   417  	}
   418  	return results, nil
   419  }
   420  
   421  func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) {
   422  	return &protocol.ApplyWorkspaceEditResponse{Applied: false, FailureReason: "not implemented"}, nil
   423  }
   424  
   425  func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
   426  	if p.URI == "gopls://diagnostics-done" {
   427  		close(c.diagnosticsDone)
   428  	}
   429  	// Don't worry about diagnostics without versions.
   430  	if p.Version == 0 {
   431  		return nil
   432  	}
   433  
   434  	c.filesMu.Lock()
   435  	defer c.filesMu.Unlock()
   436  
   437  	file := c.getFile(ctx, fileURI(p.URI))
   438  	file.diagnostics = p.Diagnostics
   439  	return nil
   440  }
   441  
   442  func (c *cmdClient) Progress(context.Context, *protocol.ProgressParams) error {
   443  	return nil
   444  }
   445  
   446  func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error {
   447  	return nil
   448  }
   449  
   450  func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile {
   451  	file, found := c.files[uri]
   452  	if !found || file.err != nil {
   453  		file = &cmdFile{
   454  			uri: uri,
   455  		}
   456  		c.files[uri] = file
   457  	}
   458  	if file.mapper == nil {
   459  		fname := uri.Filename()
   460  		content, err := ioutil.ReadFile(fname)
   461  		if err != nil {
   462  			file.err = errors.Errorf("getFile: %v: %v", uri, err)
   463  			return file
   464  		}
   465  		f := c.fset.AddFile(fname, -1, len(content))
   466  		f.SetLinesForContent(content)
   467  		converter := span.NewContentConverter(fname, content)
   468  		file.mapper = &protocol.ColumnMapper{
   469  			URI:       uri,
   470  			Converter: converter,
   471  			Content:   content,
   472  		}
   473  	}
   474  	return file
   475  }
   476  
   477  func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile {
   478  	c.Client.filesMu.Lock()
   479  	defer c.Client.filesMu.Unlock()
   480  
   481  	file := c.Client.getFile(ctx, uri)
   482  	// This should never happen.
   483  	if file == nil {
   484  		return &cmdFile{
   485  			uri: uri,
   486  			err: fmt.Errorf("no file found for %s", uri),
   487  		}
   488  	}
   489  	if file.err != nil || file.added {
   490  		return file
   491  	}
   492  	file.added = true
   493  	p := &protocol.DidOpenTextDocumentParams{
   494  		TextDocument: protocol.TextDocumentItem{
   495  			URI:        protocol.URIFromSpanURI(uri),
   496  			LanguageID: source.DetectLanguage("", file.uri.Filename()).String(),
   497  			Version:    1,
   498  			Text:       string(file.mapper.Content),
   499  		},
   500  	}
   501  	if err := c.Server.DidOpen(ctx, p); err != nil {
   502  		file.err = errors.Errorf("%v: %v", uri, err)
   503  	}
   504  	return file
   505  }
   506  
   507  func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) {
   508  	// use range to avoid limits on full
   509  	resp, err := c.Server.SemanticTokensRange(ctx, p)
   510  	if err != nil {
   511  		return nil, err
   512  	}
   513  	return resp, nil
   514  }
   515  
   516  func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error {
   517  	var untypedFiles []interface{}
   518  	for _, file := range files {
   519  		untypedFiles = append(untypedFiles, string(file))
   520  	}
   521  	c.Client.diagnosticsMu.Lock()
   522  	defer c.Client.diagnosticsMu.Unlock()
   523  
   524  	c.Client.diagnosticsDone = make(chan struct{})
   525  	_, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles})
   526  	<-c.Client.diagnosticsDone
   527  	return err
   528  }
   529  
   530  func (c *connection) terminate(ctx context.Context) {
   531  	if strings.HasPrefix(c.Client.app.Remote, "internal@") {
   532  		// internal connections need to be left alive for the next test
   533  		return
   534  	}
   535  	//TODO: do we need to handle errors on these calls?
   536  	c.Shutdown(ctx)
   537  	//TODO: right now calling exit terminates the process, we should rethink that
   538  	//server.Exit(ctx)
   539  }
   540  
   541  // Implement io.Closer.
   542  func (c *cmdClient) Close() error {
   543  	return nil
   544  }