golang.org/x/tools/gopls@v0.15.3/internal/cmd/execute.go (about)

     1  // Copyright 2023 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
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"flag"
    11  	"fmt"
    12  	"log"
    13  	"os"
    14  	"strings"
    15  
    16  	"golang.org/x/tools/gopls/internal/protocol"
    17  	"golang.org/x/tools/gopls/internal/protocol/command"
    18  	"golang.org/x/tools/gopls/internal/server"
    19  	"golang.org/x/tools/gopls/internal/util/slices"
    20  	"golang.org/x/tools/internal/tool"
    21  )
    22  
    23  // execute implements the LSP ExecuteCommand verb for gopls.
    24  type execute struct {
    25  	EditFlags
    26  	app *Application
    27  }
    28  
    29  func (e *execute) Name() string      { return "execute" }
    30  func (e *execute) Parent() string    { return e.app.Name() }
    31  func (e *execute) Usage() string     { return "[flags] command argument..." }
    32  func (e *execute) ShortHelp() string { return "Execute a gopls custom LSP command" }
    33  func (e *execute) DetailedHelp(f *flag.FlagSet) {
    34  	fmt.Fprint(f.Output(), `
    35  The execute command sends an LSP ExecuteCommand request to gopls,
    36  with a set of optional JSON argument values.
    37  Some commands return a result, also JSON.
    38  
    39  Available commands are documented at:
    40  
    41  	https://github.com/golang/tools/blob/master/gopls/doc/commands.md
    42  
    43  This interface is experimental and commands may change or disappear without notice.
    44  
    45  Examples:
    46  
    47  	$ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI": "file:///hello.go"}'
    48  	$ gopls execute gopls.run_tests '{"URI": "file:///a_test.go", "Tests": ["Test"]}'
    49  	$ gopls execute gopls.list_known_packages '{"URI": "file:///hello.go"}'
    50  
    51  execute-flags:
    52  `)
    53  	printFlagDefaults(f)
    54  }
    55  
    56  func (e *execute) Run(ctx context.Context, args ...string) error {
    57  	if len(args) == 0 {
    58  		return tool.CommandLineErrorf("execute requires a command name")
    59  	}
    60  	cmd := args[0]
    61  	if !slices.Contains(command.Commands, command.Command(strings.TrimPrefix(cmd, "gopls."))) {
    62  		return tool.CommandLineErrorf("unrecognized command: %s", cmd)
    63  	}
    64  
    65  	// A command may have multiple arguments, though the only one
    66  	// that currently does so is the "legacy" gopls.test,
    67  	// so we don't show an example of it.
    68  	var jsonArgs []json.RawMessage
    69  	for i, arg := range args[1:] {
    70  		var dummy any
    71  		if err := json.Unmarshal([]byte(arg), &dummy); err != nil {
    72  			return fmt.Errorf("argument %d is not valid JSON: %v", i+1, err)
    73  		}
    74  		jsonArgs = append(jsonArgs, json.RawMessage(arg))
    75  	}
    76  
    77  	e.app.editFlags = &e.EditFlags // in case command performs an edit
    78  
    79  	cmdDone, onProgress := commandProgress()
    80  	conn, err := e.app.connect(ctx, onProgress)
    81  	if err != nil {
    82  		return err
    83  	}
    84  	defer conn.terminate(ctx)
    85  
    86  	res, err := conn.executeCommand(ctx, cmdDone, &protocol.Command{
    87  		Command:   cmd,
    88  		Arguments: jsonArgs,
    89  	})
    90  	if err != nil {
    91  		return err
    92  	}
    93  	if res != nil {
    94  		data, err := json.MarshalIndent(res, "", "\t")
    95  		if err != nil {
    96  			log.Fatal(err)
    97  		}
    98  		fmt.Printf("%s\n", data)
    99  	}
   100  	return nil
   101  }
   102  
   103  // -- shared command helpers --
   104  
   105  const cmdProgressToken = "cmd-progress"
   106  
   107  // TODO(adonovan): disentangle this from app.connect, and factor with
   108  // conn.executeCommand used by codelens and execute. Seems like
   109  // connection needs a way to register and unregister independent
   110  // handlers, later than at connect time.
   111  func commandProgress() (<-chan bool, func(p *protocol.ProgressParams)) {
   112  	cmdDone := make(chan bool, 1)
   113  	onProgress := func(p *protocol.ProgressParams) {
   114  		switch v := p.Value.(type) {
   115  		case *protocol.WorkDoneProgressReport:
   116  			// TODO(adonovan): how can we segregate command's stdout and
   117  			// stderr so that structure is preserved?
   118  			fmt.Fprintln(os.Stderr, v.Message)
   119  
   120  		case *protocol.WorkDoneProgressEnd:
   121  			if p.Token == cmdProgressToken {
   122  				// commandHandler.run sends message = canceled | failed | completed
   123  				cmdDone <- v.Message == server.CommandCompleted
   124  			}
   125  		}
   126  	}
   127  	return cmdDone, onProgress
   128  }
   129  
   130  func (conn *connection) executeCommand(ctx context.Context, done <-chan bool, cmd *protocol.Command) (any, error) {
   131  	res, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
   132  		Command:   cmd.Command,
   133  		Arguments: cmd.Arguments,
   134  		WorkDoneProgressParams: protocol.WorkDoneProgressParams{
   135  			WorkDoneToken: cmdProgressToken,
   136  		},
   137  	})
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	// Wait for it to finish (by watching for a progress token).
   143  	//
   144  	// In theory this is only necessary for the two async
   145  	// commands (RunGovulncheck and RunTests), but the tests
   146  	// fail for Test as well (why?), and there is no cost to
   147  	// waiting in all cases. TODO(adonovan): investigate.
   148  	if success := <-done; !success {
   149  		// TODO(adonovan): suppress this message;
   150  		// the command's stderr should suffice.
   151  		return nil, fmt.Errorf("command failed")
   152  	}
   153  
   154  	return res, nil
   155  }