| // Copyright 2018 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 cmd handles the gopls command line. |
| // It contains a handler for each of the modes, along with all the flag handling |
| // and the command line output format. |
| package cmd |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "log" |
| "math/rand" |
| "os" |
| "path/filepath" |
| "reflect" |
| "sort" |
| "strings" |
| "sync" |
| "text/tabwriter" |
| "time" |
| |
| "golang.org/x/tools/gopls/internal/cache" |
| "golang.org/x/tools/gopls/internal/debug" |
| "golang.org/x/tools/gopls/internal/filecache" |
| "golang.org/x/tools/gopls/internal/lsprpc" |
| "golang.org/x/tools/gopls/internal/protocol" |
| "golang.org/x/tools/gopls/internal/protocol/command" |
| "golang.org/x/tools/gopls/internal/server" |
| "golang.org/x/tools/gopls/internal/settings" |
| "golang.org/x/tools/gopls/internal/util/browser" |
| bugpkg "golang.org/x/tools/gopls/internal/util/bug" |
| "golang.org/x/tools/internal/diff" |
| "golang.org/x/tools/internal/jsonrpc2" |
| "golang.org/x/tools/internal/tool" |
| ) |
| |
| // Application is the main application as passed to tool.Main |
| // It handles the main command line parsing and dispatch to the sub commands. |
| type Application struct { |
| // Core application flags |
| |
| // Embed the basic profiling flags supported by the tool package |
| tool.Profile |
| |
| // We include the server configuration directly for now, so the flags work |
| // even without the verb. |
| // TODO: Remove this when we stop allowing the serve verb by default. |
| Serve Serve |
| |
| // the options configuring function to invoke when building a server |
| options func(*settings.Options) |
| |
| // Support for remote LSP server. |
| 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."` |
| |
| // Verbose enables verbose logging. |
| Verbose bool `flag:"v,verbose" help:"verbose output"` |
| |
| // VeryVerbose enables a higher level of verbosity in logging output. |
| VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"` |
| |
| // Control ocagent export of telemetry |
| OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"` |
| |
| // PrepareOptions is called to update the options when a new view is built. |
| // It is primarily to allow the behavior of gopls to be modified by hooks. |
| PrepareOptions func(*settings.Options) |
| |
| // editFlags holds flags that control how file edit operations |
| // are applied, in particular when the server makes an ApplyEdits |
| // downcall to the client. Present only for commands that apply edits. |
| editFlags *EditFlags |
| } |
| |
| // EditFlags defines flags common to {code{action,lens},format,imports,rename} |
| // that control how edits are applied to the client's files. |
| // |
| // The type is exported for flag reflection. |
| // |
| // The -write, -diff, and -list flags are orthogonal but any |
| // of them suppresses the default behavior, which is to print |
| // the edited file contents. |
| type EditFlags struct { |
| Write bool `flag:"w,write" help:"write edited content to source files"` |
| Preserve bool `flag:"preserve" help:"with -write, make copies of original files"` |
| Diff bool `flag:"d,diff" help:"display diffs instead of edited file content"` |
| List bool `flag:"l,list" help:"display names of edited files"` |
| } |
| |
| func (app *Application) verbose() bool { |
| return app.Verbose || app.VeryVerbose |
| } |
| |
| // New returns a new Application ready to run. |
| func New() *Application { |
| app := &Application{ |
| OCAgent: "off", //TODO: Remove this line to default the exporter to on |
| |
| Serve: Serve{ |
| RemoteListenTimeout: 1 * time.Minute, |
| }, |
| } |
| app.Serve.app = app |
| return app |
| } |
| |
| // Name implements tool.Application returning the binary name. |
| func (app *Application) Name() string { return "gopls" } |
| |
| // Usage implements tool.Application returning empty extra argument usage. |
| func (app *Application) Usage() string { return "" } |
| |
| // ShortHelp implements tool.Application returning the main binary help. |
| func (app *Application) ShortHelp() string { |
| return "" |
| } |
| |
| // DetailedHelp implements tool.Application returning the main binary help. |
| // This includes the short help for all the sub commands. |
| func (app *Application) DetailedHelp(f *flag.FlagSet) { |
| w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) |
| defer w.Flush() |
| |
| fmt.Fprint(w, ` |
| gopls is a Go language server. |
| |
| It is typically used with an editor to provide language features. When no |
| command is specified, gopls will default to the 'serve' command. The language |
| features can also be accessed via the gopls command-line interface. |
| |
| For documentation of all its features, see: |
| |
| https://github.com/golang/tools/blob/master/gopls/doc/features |
| |
| Usage: |
| gopls help [<subject>] |
| |
| Command: |
| `) |
| fmt.Fprint(w, "\nMain\t\n") |
| for _, c := range app.mainCommands() { |
| fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) |
| } |
| fmt.Fprint(w, "\t\nFeatures\t\n") |
| for _, c := range app.featureCommands() { |
| fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) |
| } |
| if app.verbose() { |
| fmt.Fprint(w, "\t\nInternal Use Only\t\n") |
| for _, c := range app.internalCommands() { |
| fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) |
| } |
| } |
| fmt.Fprint(w, "\nflags:\n") |
| printFlagDefaults(f) |
| } |
| |
| // this is a slightly modified version of flag.PrintDefaults to give us control |
| func printFlagDefaults(s *flag.FlagSet) { |
| var flags [][]*flag.Flag |
| seen := map[flag.Value]int{} |
| s.VisitAll(func(f *flag.Flag) { |
| if i, ok := seen[f.Value]; !ok { |
| seen[f.Value] = len(flags) |
| flags = append(flags, []*flag.Flag{f}) |
| } else { |
| flags[i] = append(flags[i], f) |
| } |
| }) |
| for _, entry := range flags { |
| sort.SliceStable(entry, func(i, j int) bool { |
| return len(entry[i].Name) < len(entry[j].Name) |
| }) |
| var b strings.Builder |
| for i, f := range entry { |
| switch i { |
| case 0: |
| b.WriteString(" -") |
| default: |
| b.WriteString(",-") |
| } |
| b.WriteString(f.Name) |
| } |
| |
| f := entry[0] |
| name, usage := flag.UnquoteUsage(f) |
| if len(name) > 0 { |
| b.WriteString("=") |
| b.WriteString(name) |
| } |
| // Boolean flags of one ASCII letter are so common we |
| // treat them specially, putting their usage on the same line. |
| if b.Len() <= 4 { // space, space, '-', 'x'. |
| b.WriteString("\t") |
| } else { |
| // Four spaces before the tab triggers good alignment |
| // for both 4- and 8-space tab stops. |
| b.WriteString("\n \t") |
| } |
| b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) |
| if !isZeroValue(f, f.DefValue) { |
| if reflect.TypeOf(f.Value).Elem().Name() == "stringValue" { |
| fmt.Fprintf(&b, " (default %q)", f.DefValue) |
| } else { |
| fmt.Fprintf(&b, " (default %v)", f.DefValue) |
| } |
| } |
| fmt.Fprint(s.Output(), b.String(), "\n") |
| } |
| } |
| |
| // isZeroValue is copied from the flags package |
| func isZeroValue(f *flag.Flag, value string) bool { |
| // Build a zero value of the flag's Value type, and see if the |
| // result of calling its String method equals the value passed in. |
| // This works unless the Value type is itself an interface type. |
| typ := reflect.TypeOf(f.Value) |
| var z reflect.Value |
| if typ.Kind() == reflect.Ptr { |
| z = reflect.New(typ.Elem()) |
| } else { |
| z = reflect.Zero(typ) |
| } |
| return value == z.Interface().(flag.Value).String() |
| } |
| |
| // Run takes the args after top level flag processing, and invokes the correct |
| // sub command as specified by the first argument. |
| // If no arguments are passed it will invoke the server sub command, as a |
| // temporary measure for compatibility. |
| func (app *Application) Run(ctx context.Context, args ...string) error { |
| // In the category of "things we can do while waiting for the Go command": |
| // Pre-initialize the filecache, which takes ~50ms to hash the gopls |
| // executable, and immediately runs a gc. |
| filecache.Start() |
| |
| ctx = debug.WithInstance(ctx, app.OCAgent) |
| if len(args) == 0 { |
| s := flag.NewFlagSet(app.Name(), flag.ExitOnError) |
| return tool.Run(ctx, s, &app.Serve, args) |
| } |
| command, args := args[0], args[1:] |
| for _, c := range app.Commands() { |
| if c.Name() == command { |
| s := flag.NewFlagSet(app.Name(), flag.ExitOnError) |
| return tool.Run(ctx, s, c, args) |
| } |
| } |
| return tool.CommandLineErrorf("Unknown command %v", command) |
| } |
| |
| // Commands returns the set of commands supported by the gopls tool on the |
| // command line. |
| // The command is specified by the first non flag argument. |
| func (app *Application) Commands() []tool.Application { |
| var commands []tool.Application |
| commands = append(commands, app.mainCommands()...) |
| commands = append(commands, app.featureCommands()...) |
| commands = append(commands, app.internalCommands()...) |
| return commands |
| } |
| |
| func (app *Application) mainCommands() []tool.Application { |
| return []tool.Application{ |
| &app.Serve, |
| &version{app: app}, |
| &bug{app: app}, |
| &help{app: app}, |
| &apiJSON{app: app}, |
| &licenses{app: app}, |
| } |
| } |
| |
| func (app *Application) internalCommands() []tool.Application { |
| return []tool.Application{ |
| &vulncheck{app: app}, |
| } |
| } |
| |
| func (app *Application) featureCommands() []tool.Application { |
| return []tool.Application{ |
| &callHierarchy{app: app}, |
| &check{app: app}, |
| &codeaction{app: app}, |
| &codelens{app: app}, |
| &definition{app: app}, |
| &execute{app: app}, |
| &fix{app: app}, // (non-functional) |
| &foldingRanges{app: app}, |
| &format{app: app}, |
| &highlight{app: app}, |
| &implementation{app: app}, |
| &imports{app: app}, |
| newRemote(app, ""), |
| newRemote(app, "inspect"), |
| &links{app: app}, |
| &prepareRename{app: app}, |
| &references{app: app}, |
| &rename{app: app}, |
| &semtok{app: app}, |
| &signature{app: app}, |
| &stats{app: app}, |
| &symbols{app: app}, |
| |
| &workspaceSymbol{app: app}, |
| } |
| } |
| |
| var ( |
| internalMu sync.Mutex |
| internalConnections = make(map[string]*connection) |
| ) |
| |
| // connect creates and initializes a new in-process gopls session. |
| func (app *Application) connect(ctx context.Context) (*connection, error) { |
| client := newClient(app) |
| var svr protocol.Server |
| if app.Remote == "" { |
| // local |
| options := settings.DefaultOptions(app.options) |
| svr = server.New(cache.NewSession(ctx, cache.New(nil)), client, options) |
| ctx = protocol.WithClient(ctx, client) |
| |
| } else { |
| // remote |
| netConn, err := lsprpc.ConnectToRemote(ctx, app.Remote) |
| if err != nil { |
| return nil, err |
| } |
| stream := jsonrpc2.NewHeaderStream(netConn) |
| jsonConn := jsonrpc2.NewConn(stream) |
| svr = protocol.ServerDispatcher(jsonConn) |
| ctx = protocol.WithClient(ctx, client) |
| jsonConn.Go(ctx, |
| protocol.Handlers( |
| protocol.ClientHandler(client, jsonrpc2.MethodNotFound))) |
| } |
| conn := newConnection(svr, client) |
| return conn, conn.initialize(ctx, app.options) |
| } |
| |
| func (c *connection) initialize(ctx context.Context, options func(*settings.Options)) error { |
| wd, err := os.Getwd() |
| if err != nil { |
| return fmt.Errorf("finding workdir: %v", err) |
| } |
| params := &protocol.ParamInitialize{} |
| params.RootURI = protocol.URIFromPath(wd) |
| params.Capabilities.Workspace.Configuration = true |
| |
| // Make sure to respect configured options when sending initialize request. |
| opts := settings.DefaultOptions(options) |
| // If you add an additional option here, you must update the map key in connect. |
| params.Capabilities.TextDocument.Hover = &protocol.HoverClientCapabilities{ |
| ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat}, |
| } |
| params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport |
| params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{} |
| params.Capabilities.TextDocument.SemanticTokens.Formats = []protocol.TokenFormat{"relative"} |
| params.Capabilities.TextDocument.SemanticTokens.Requests.Range = &protocol.Or_ClientSemanticTokensRequestOptions_range{Value: true} |
| //params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true |
| params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true} |
| params.Capabilities.TextDocument.SemanticTokens.TokenTypes = protocol.SemanticTypes() |
| params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = protocol.SemanticModifiers() |
| params.Capabilities.Window.WorkDoneProgress = true |
| |
| params.InitializationOptions = map[string]interface{}{ |
| "symbolMatcher": string(opts.SymbolMatcher), |
| } |
| if _, err := c.Server.Initialize(ctx, params); err != nil { |
| return err |
| } |
| if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| type connection struct { |
| protocol.Server |
| client *cmdClient |
| } |
| |
| // registerProgressHandler registers a handler for progress notifications. |
| // The caller must call unregister when the handler is no longer needed. |
| func (cli *cmdClient) registerProgressHandler(handler func(*protocol.ProgressParams)) (token protocol.ProgressToken, unregister func()) { |
| token = fmt.Sprintf("tok%d", rand.Uint64()) |
| |
| // register |
| cli.progressHandlersMu.Lock() |
| if cli.progressHandlers == nil { |
| cli.progressHandlers = make(map[protocol.ProgressToken]func(*protocol.ProgressParams)) |
| } |
| cli.progressHandlers[token] = handler |
| cli.progressHandlersMu.Unlock() |
| |
| unregister = func() { |
| cli.progressHandlersMu.Lock() |
| delete(cli.progressHandlers, token) |
| cli.progressHandlersMu.Unlock() |
| } |
| return token, unregister |
| } |
| |
| // cmdClient defines the protocol.Client interface behavior of the gopls CLI tool. |
| type cmdClient struct { |
| app *Application |
| |
| progressHandlersMu sync.Mutex |
| progressHandlers map[protocol.ProgressToken]func(*protocol.ProgressParams) |
| iwlToken protocol.ProgressToken |
| iwlDone chan struct{} |
| |
| filesMu sync.Mutex // guards files map |
| files map[protocol.DocumentURI]*cmdFile |
| } |
| |
| type cmdFile struct { |
| uri protocol.DocumentURI |
| mapper *protocol.Mapper |
| err error |
| diagnosticsMu sync.Mutex |
| diagnostics []protocol.Diagnostic |
| } |
| |
| func newClient(app *Application) *cmdClient { |
| return &cmdClient{ |
| app: app, |
| files: make(map[protocol.DocumentURI]*cmdFile), |
| iwlDone: make(chan struct{}), |
| } |
| } |
| |
| func newConnection(server protocol.Server, client *cmdClient) *connection { |
| return &connection{ |
| Server: server, |
| client: client, |
| } |
| } |
| |
| func (c *cmdClient) CodeLensRefresh(context.Context) error { return nil } |
| |
| func (c *cmdClient) FoldingRangeRefresh(context.Context) error { return nil } |
| |
| func (c *cmdClient) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } |
| |
| func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { |
| fmt.Fprintf(os.Stderr, "%s: %s\n", p.Type, p.Message) |
| return nil |
| } |
| |
| func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { |
| return nil, nil |
| } |
| |
| func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error { |
| // This logic causes server logging to be double-prefixed with a timestamp. |
| // 2023/11/08 10:50:21 Error:2023/11/08 10:50:21 <actual message> |
| // TODO(adonovan): print just p.Message, plus a newline if needed? |
| switch p.Type { |
| case protocol.Error: |
| log.Print("Error:", p.Message) |
| case protocol.Warning: |
| log.Print("Warning:", p.Message) |
| case protocol.Info: |
| if c.app.verbose() { |
| log.Print("Info:", p.Message) |
| } |
| case protocol.Log: |
| if c.app.verbose() { |
| log.Print("Log:", p.Message) |
| } |
| default: |
| if c.app.verbose() { |
| log.Print(p.Message) |
| } |
| } |
| return nil |
| } |
| |
| func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil } |
| |
| func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error { |
| return nil |
| } |
| |
| func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error { |
| return nil |
| } |
| |
| func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) { |
| return nil, nil |
| } |
| |
| func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { |
| results := make([]interface{}, len(p.Items)) |
| for i, item := range p.Items { |
| if item.Section != "gopls" { |
| continue |
| } |
| m := map[string]interface{}{ |
| "analyses": map[string]any{ |
| "fillreturns": true, |
| "nonewvars": true, |
| "noresultvalues": true, |
| "undeclaredname": true, |
| }, |
| } |
| if c.app.VeryVerbose { |
| m["verboseOutput"] = true |
| } |
| results[i] = m |
| } |
| return results, nil |
| } |
| |
| func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { |
| if err := c.applyWorkspaceEdit(&p.Edit); err != nil { |
| return &protocol.ApplyWorkspaceEditResult{FailureReason: err.Error()}, nil |
| } |
| return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil |
| } |
| |
| // applyWorkspaceEdit applies a complete WorkspaceEdit to the client's |
| // files, honoring the preferred edit mode specified by cli.app.editMode. |
| // (Used by rename and by ApplyEdit downcalls.) |
| // |
| // See also: |
| // - changedFiles in ../test/marker/marker_test.go for the golden-file capturing variant |
| // - applyWorkspaceEdit in ../test/integration/fake/editor.go for the Editor variant |
| func (cli *cmdClient) applyWorkspaceEdit(wsedit *protocol.WorkspaceEdit) error { |
| |
| create := func(uri protocol.DocumentURI, content []byte) error { |
| edits := []diff.Edit{{Start: 0, End: 0, New: string(content)}} |
| return updateFile(uri.Path(), nil, content, edits, cli.app.editFlags) |
| } |
| |
| delete := func(uri protocol.DocumentURI, content []byte) error { |
| edits := []diff.Edit{{Start: 0, End: len(content), New: ""}} |
| return updateFile(uri.Path(), content, nil, edits, cli.app.editFlags) |
| } |
| |
| for _, c := range wsedit.DocumentChanges { |
| switch { |
| case c.TextDocumentEdit != nil: |
| f := cli.openFile(c.TextDocumentEdit.TextDocument.URI) |
| if f.err != nil { |
| return f.err |
| } |
| // TODO(adonovan): sanity-check c.TextDocumentEdit.TextDocument.Version |
| edits := protocol.AsTextEdits(c.TextDocumentEdit.Edits) |
| if err := applyTextEdits(f.mapper, edits, cli.app.editFlags); err != nil { |
| return err |
| } |
| |
| case c.CreateFile != nil: |
| if err := create(c.CreateFile.URI, []byte{}); err != nil { |
| return err |
| } |
| |
| case c.RenameFile != nil: |
| // Analyze as creation + deletion. (NB: loses file mode.) |
| f := cli.openFile(c.RenameFile.OldURI) |
| if f.err != nil { |
| return f.err |
| } |
| if err := create(c.RenameFile.NewURI, f.mapper.Content); err != nil { |
| return err |
| } |
| if err := delete(f.mapper.URI, f.mapper.Content); err != nil { |
| return err |
| } |
| |
| case c.DeleteFile != nil: |
| f := cli.openFile(c.DeleteFile.URI) |
| if f.err != nil { |
| return f.err |
| } |
| if err := delete(f.mapper.URI, f.mapper.Content); err != nil { |
| return err |
| } |
| |
| default: |
| return fmt.Errorf("unknown DocumentChange: %#v", c) |
| } |
| } |
| return nil |
| } |
| |
| // applyTextEdits applies a list of edits to the mapper file content, |
| // using the preferred edit mode. It is a no-op if there are no edits. |
| func applyTextEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, flags *EditFlags) error { |
| if len(edits) == 0 { |
| return nil |
| } |
| newContent, diffEdits, err := protocol.ApplyEdits(mapper, edits) |
| if err != nil { |
| return err |
| } |
| return updateFile(mapper.URI.Path(), mapper.Content, newContent, diffEdits, flags) |
| } |
| |
| // updateFile performs a content update operation on the specified file. |
| // If the old content is nil, the operation creates the file. |
| // If the new content is nil, the operation deletes the file. |
| // The flags control whether the operation is written, or merely listed, diffed, or printed. |
| func updateFile(filename string, old, new []byte, edits []diff.Edit, flags *EditFlags) error { |
| if flags.List { |
| fmt.Println(filename) |
| } |
| |
| if flags.Write { |
| if flags.Preserve && old != nil { // edit or delete |
| if err := os.WriteFile(filename+".orig", old, 0666); err != nil { |
| return err |
| } |
| } |
| |
| if new != nil { |
| // create or edit |
| if err := os.WriteFile(filename, new, 0666); err != nil { |
| return err |
| } |
| } else { |
| // delete |
| if err := os.Remove(filename); err != nil { |
| return err |
| } |
| } |
| } |
| |
| if flags.Diff { |
| // For diffing, creations and deletions are equivalent |
| // updating an empty file and making an existing file empty. |
| unified, err := diff.ToUnified(filename+".orig", filename, string(old), edits, diff.DefaultContextLines) |
| if err != nil { |
| return err |
| } |
| fmt.Print(unified) |
| } |
| |
| // No flags: just print edited file content. |
| // |
| // This makes no sense for multiple files. |
| // (We should probably change the default to -diff.) |
| if !(flags.List || flags.Write || flags.Diff) { |
| os.Stdout.Write(new) |
| } |
| |
| return nil |
| } |
| |
| func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error { |
| // Don't worry about diagnostics without versions. |
| if p.Version == 0 { |
| return nil |
| } |
| |
| c.filesMu.Lock() |
| file := c.getFile(p.URI) |
| c.filesMu.Unlock() |
| |
| file.diagnosticsMu.Lock() |
| defer file.diagnosticsMu.Unlock() |
| file.diagnostics = append(file.diagnostics, p.Diagnostics...) |
| |
| // Perform a crude in-place deduplication. |
| // TODO(golang/go#60122): replace the gopls.diagnose_files |
| // command with support for textDocument/diagnostic, |
| // so that we don't need to do this de-duplication. |
| type key [6]interface{} |
| seen := make(map[key]bool) |
| out := file.diagnostics[:0] |
| for _, d := range file.diagnostics { |
| var codeHref string |
| if desc := d.CodeDescription; desc != nil { |
| codeHref = desc.Href |
| } |
| k := key{d.Range, d.Severity, d.Code, codeHref, d.Source, d.Message} |
| if !seen[k] { |
| seen[k] = true |
| out = append(out, d) |
| } |
| } |
| file.diagnostics = out |
| |
| return nil |
| } |
| |
| func (c *cmdClient) Progress(_ context.Context, params *protocol.ProgressParams) error { |
| token, ok := params.Token.(string) |
| if !ok { |
| return fmt.Errorf("unexpected progress token: %[1]T %[1]v", params.Token) |
| } |
| |
| c.progressHandlersMu.Lock() |
| handler := c.progressHandlers[token] |
| c.progressHandlersMu.Unlock() |
| if handler == nil { |
| handler = c.defaultProgressHandler |
| } |
| handler(params) |
| return nil |
| } |
| |
| // defaultProgressHandler is the default handler of progress messages, |
| // used during the initialize request. |
| func (c *cmdClient) defaultProgressHandler(params *protocol.ProgressParams) { |
| switch v := params.Value.(type) { |
| case *protocol.WorkDoneProgressBegin: |
| if v.Title == server.DiagnosticWorkTitle(server.FromInitialWorkspaceLoad) { |
| c.progressHandlersMu.Lock() |
| c.iwlToken = params.Token |
| c.progressHandlersMu.Unlock() |
| } |
| |
| case *protocol.WorkDoneProgressEnd: |
| c.progressHandlersMu.Lock() |
| iwlToken := c.iwlToken |
| c.progressHandlersMu.Unlock() |
| |
| if params.Token == iwlToken { |
| close(c.iwlDone) |
| } |
| } |
| } |
| |
| func (c *cmdClient) ShowDocument(ctx context.Context, params *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { |
| var success bool |
| if params.External { |
| // Open URI in external browser. |
| success = browser.Open(params.URI) |
| } else { |
| // Open file in editor, optionally taking focus and selecting a range. |
| // (cmdClient has no editor. Should it fork+exec $EDITOR?) |
| log.Printf("Server requested that client editor open %q (takeFocus=%t, selection=%+v)", |
| params.URI, params.TakeFocus, params.Selection) |
| success = true |
| } |
| return &protocol.ShowDocumentResult{Success: success}, nil |
| } |
| |
| func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error { |
| return nil |
| } |
| |
| func (c *cmdClient) DiagnosticRefresh(context.Context) error { |
| return nil |
| } |
| |
| func (c *cmdClient) InlayHintRefresh(context.Context) error { |
| return nil |
| } |
| |
| func (c *cmdClient) SemanticTokensRefresh(context.Context) error { |
| return nil |
| } |
| |
| func (c *cmdClient) InlineValueRefresh(context.Context) error { |
| return nil |
| } |
| |
| func (c *cmdClient) getFile(uri protocol.DocumentURI) *cmdFile { |
| file, found := c.files[uri] |
| if !found || file.err != nil { |
| file = &cmdFile{ |
| uri: uri, |
| } |
| c.files[uri] = file |
| } |
| if file.mapper == nil { |
| content, err := os.ReadFile(uri.Path()) |
| if err != nil { |
| file.err = fmt.Errorf("getFile: %v: %v", uri, err) |
| return file |
| } |
| file.mapper = protocol.NewMapper(uri, content) |
| } |
| return file |
| } |
| |
| func (c *cmdClient) openFile(uri protocol.DocumentURI) *cmdFile { |
| c.filesMu.Lock() |
| defer c.filesMu.Unlock() |
| return c.getFile(uri) |
| } |
| |
| // TODO(adonovan): provide convenience helpers to: |
| // - map a (URI, protocol.Range) to a MappedRange; |
| // - parse a command-line argument to a MappedRange. |
| func (c *connection) openFile(ctx context.Context, uri protocol.DocumentURI) (*cmdFile, error) { |
| file := c.client.openFile(uri) |
| if file.err != nil { |
| return nil, file.err |
| } |
| |
| p := &protocol.DidOpenTextDocumentParams{ |
| TextDocument: protocol.TextDocumentItem{ |
| URI: uri, |
| LanguageID: "go", |
| Version: 1, |
| Text: string(file.mapper.Content), |
| }, |
| } |
| if err := c.Server.DidOpen(ctx, p); err != nil { |
| // TODO(adonovan): is this assignment concurrency safe? |
| file.err = fmt.Errorf("%v: %v", uri, err) |
| return nil, file.err |
| } |
| return file, nil |
| } |
| |
| func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { |
| // use range to avoid limits on full |
| resp, err := c.Server.SemanticTokensRange(ctx, p) |
| if err != nil { |
| return nil, err |
| } |
| return resp, nil |
| } |
| |
| func (c *connection) diagnoseFiles(ctx context.Context, files []protocol.DocumentURI) error { |
| cmd := command.NewDiagnoseFilesCommand("Diagnose files", command.DiagnoseFilesArgs{ |
| Files: files, |
| }) |
| _, err := c.executeCommand(ctx, cmd) |
| return err |
| } |
| |
| func (c *connection) terminate(ctx context.Context) { |
| //TODO: do we need to handle errors on these calls? |
| c.Shutdown(ctx) |
| //TODO: right now calling exit terminates the process, we should rethink that |
| //server.Exit(ctx) |
| } |
| |
| // Implement io.Closer. |
| func (c *cmdClient) Close() error { |
| return nil |
| } |
| |
| // -- conversions to span (UTF-8) domain -- |
| |
| // locationSpan converts a protocol (UTF-16) Location to a (UTF-8) span. |
| // Precondition: the URIs of Location and Mapper match. |
| func (f *cmdFile) locationSpan(loc protocol.Location) (span, error) { |
| // TODO(adonovan): check that l.URI matches m.URI. |
| return f.rangeSpan(loc.Range) |
| } |
| |
| // rangeSpan converts a protocol (UTF-16) range to a (UTF-8) span. |
| // The resulting span has valid Positions and Offsets. |
| func (f *cmdFile) rangeSpan(r protocol.Range) (span, error) { |
| start, end, err := f.mapper.RangeOffsets(r) |
| if err != nil { |
| return span{}, err |
| } |
| return f.offsetSpan(start, end) |
| } |
| |
| // offsetSpan converts a byte-offset interval to a (UTF-8) span. |
| // The resulting span contains line, column, and offset information. |
| func (f *cmdFile) offsetSpan(start, end int) (span, error) { |
| if start > end { |
| return span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) |
| } |
| startPoint, err := offsetPoint(f.mapper, start) |
| if err != nil { |
| return span{}, fmt.Errorf("start: %v", err) |
| } |
| endPoint, err := offsetPoint(f.mapper, end) |
| if err != nil { |
| return span{}, fmt.Errorf("end: %v", err) |
| } |
| return newSpan(f.mapper.URI, startPoint, endPoint), nil |
| } |
| |
| // offsetPoint converts a byte offset to a span (UTF-8) point. |
| // The resulting point contains line, column, and offset information. |
| func offsetPoint(m *protocol.Mapper, offset int) (point, error) { |
| if !(0 <= offset && offset <= len(m.Content)) { |
| return point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) |
| } |
| line, col8 := m.OffsetLineCol8(offset) |
| return newPoint(line, col8, offset), nil |
| } |
| |
| // -- conversions from span (UTF-8) domain -- |
| |
| // spanLocation converts a (UTF-8) span to a protocol (UTF-16) range. |
| // Precondition: the URIs of spanLocation and Mapper match. |
| func (f *cmdFile) spanLocation(s span) (protocol.Location, error) { |
| rng, err := f.spanRange(s) |
| if err != nil { |
| return protocol.Location{}, err |
| } |
| return f.mapper.RangeLocation(rng), nil |
| } |
| |
| // spanRange converts a (UTF-8) span to a protocol (UTF-16) range. |
| // Precondition: the URIs of span and Mapper match. |
| func (f *cmdFile) spanRange(s span) (protocol.Range, error) { |
| // Assert that we aren't using the wrong mapper. |
| // We check only the base name, and case insensitively, |
| // because we can't assume clean paths, no symbolic links, |
| // case-sensitive directories. The authoritative answer |
| // requires querying the file system, and we don't want |
| // to do that. |
| if !strings.EqualFold(filepath.Base(string(f.mapper.URI)), filepath.Base(string(s.URI()))) { |
| return protocol.Range{}, bugpkg.Errorf("mapper is for file %q instead of %q", f.mapper.URI, s.URI()) |
| } |
| start, err := pointPosition(f.mapper, s.Start()) |
| if err != nil { |
| return protocol.Range{}, fmt.Errorf("start: %w", err) |
| } |
| end, err := pointPosition(f.mapper, s.End()) |
| if err != nil { |
| return protocol.Range{}, fmt.Errorf("end: %w", err) |
| } |
| return protocol.Range{Start: start, End: end}, nil |
| } |
| |
| // pointPosition converts a valid span (UTF-8) point to a protocol (UTF-16) position. |
| func pointPosition(m *protocol.Mapper, p point) (protocol.Position, error) { |
| if p.HasPosition() { |
| return m.LineCol8Position(p.Line(), p.Column()) |
| } |
| if p.HasOffset() { |
| return m.OffsetPosition(p.Offset()) |
| } |
| return protocol.Position{}, fmt.Errorf("point has neither offset nor line/column") |
| } |
| |
| // TODO(adonovan): delete in 2025. |
| type fix struct{ app *Application } |
| |
| func (*fix) Name() string { return "fix" } |
| func (cmd *fix) Parent() string { return cmd.app.Name() } |
| func (*fix) Usage() string { return "" } |
| func (*fix) ShortHelp() string { return "apply suggested fixes (obsolete)" } |
| func (*fix) DetailedHelp(flags *flag.FlagSet) { |
| fmt.Fprintf(flags.Output(), `No longer supported; use "gopls codeaction" instead.`) |
| } |
| func (*fix) Run(ctx context.Context, args ...string) error { |
| return tool.CommandLineErrorf(`no longer supported; use "gopls codeaction" instead`) |
| } |