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