From 1e56d3efac9141316e546de545a05205666402de Mon Sep 17 00:00:00 2001 From: mbana Date: Mon, 18 Feb 2019 19:27:50 +0000 Subject: [PATCH 1/2] `internal/lsp`: error handling --- internal/lsp/cache/file.go | 43 ++++++++++++---------- internal/lsp/cache/view.go | 5 ++- internal/lsp/cmd/definition.go | 10 ++++-- internal/lsp/format.go | 29 +++++++++++---- internal/lsp/imports.go | 11 ++++-- internal/lsp/lsp_test.go | 15 ++++++-- internal/lsp/position.go | 15 +++++--- internal/lsp/server.go | 52 +++++++++++++++++++++------ internal/lsp/source/completion.go | 10 ++++-- internal/lsp/source/definition.go | 25 +++++++++---- internal/lsp/source/diagnostics.go | 16 +++++++-- internal/lsp/source/format.go | 44 ++++++++++++++++++----- internal/lsp/source/signature_help.go | 15 +++++--- internal/lsp/source/view.go | 8 ++--- 14 files changed, 222 insertions(+), 76 deletions(-) diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go index d75c69168fd..1a7512c6ac2 100644 --- a/internal/lsp/cache/file.go +++ b/internal/lsp/cache/file.go @@ -25,63 +25,68 @@ type File struct { } // GetContent returns the contents of the file, reading it from file system if needed. -func (f *File) GetContent() []byte { +func (f *File) GetContent() ([]byte, error) { f.view.mu.Lock() defer f.view.mu.Unlock() - f.read() - return f.content + if f.content != nil { + return f.content, nil + } + content, err := f.read() + if err != nil { + return nil, err + } + f.content = content + return f.content, nil } func (f *File) GetFileSet() *token.FileSet { return f.view.Config.Fset } -func (f *File) GetToken() *token.File { +func (f *File) GetToken() (*token.File, error) { f.view.mu.Lock() defer f.view.mu.Unlock() if f.token == nil { if err := f.view.parse(f.URI); err != nil { - return nil + return nil, err } } - return f.token + return f.token, nil } -func (f *File) GetAST() *ast.File { +func (f *File) GetAST() (*ast.File, error) { f.view.mu.Lock() defer f.view.mu.Unlock() if f.ast == nil { if err := f.view.parse(f.URI); err != nil { - return nil + return nil, err } } - return f.ast + return f.ast, nil } -func (f *File) GetPackage() *packages.Package { +func (f *File) GetPackage() (*packages.Package, error) { f.view.mu.Lock() defer f.view.mu.Unlock() if f.pkg == nil { if err := f.view.parse(f.URI); err != nil { - return nil + return nil, err } } - return f.pkg + return f.pkg, nil } // read is the internal part of Read that presumes the lock is already held -func (f *File) read() { - if f.content != nil { - return - } +func (f *File) read() ([]byte, error) { // we don't know the content yet, so read it filename, err := f.URI.Filename() if err != nil { - return + return nil, err } content, err := ioutil.ReadFile(filename) if err != nil { - return + return nil, err } - f.content = content + // f.content = content + return content, nil } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 38f4a5b8a91..687f2c853e6 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -46,6 +46,9 @@ func (v *View) FileSet() *token.FileSet { } func (v *View) GetAnalysisCache() *source.AnalysisCache { + v.mu.Lock() + defer v.mu.Unlock() + if v.analysisCache == nil { v.analysisCache = source.NewAnalysisCache() } @@ -91,8 +94,8 @@ func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) ( // adds the file to the managed set if needed. func (v *View) GetFile(ctx context.Context, uri source.URI) (source.File, error) { v.mu.Lock() + defer v.mu.Unlock() f := v.getFile(uri) - v.mu.Unlock() return f, nil } diff --git a/internal/lsp/cmd/definition.go b/internal/lsp/cmd/definition.go index 29202e886c7..861de8386bf 100644 --- a/internal/lsp/cmd/definition.go +++ b/internal/lsp/cmd/definition.go @@ -65,7 +65,10 @@ func (d *definition) Run(ctx context.Context, args ...string) error { if err != nil { return err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return err + } pos := tok.Pos(from.Start.Offset) ident, err := source.Identifier(ctx, view, f, pos) if err != nil { @@ -115,7 +118,10 @@ func buildDefinition(view source.View, ident *source.IdentifierInfo) (*Definitio func buildGuruDefinition(view source.View, ident *source.IdentifierInfo) (*guru.Definition, error) { loc := newLocation(view.FileSet(), ident.Declaration.Range) - pkg := ident.File.GetPackage() + pkg, err := ident.File.GetPackage() + if err != nil { + return nil, err + } // guru does not support ranges loc.End = loc.Start // Behavior that attempts to match the expected output for guru. For an example diff --git a/internal/lsp/format.go b/internal/lsp/format.go index be88dacf1c0..a362de36b07 100644 --- a/internal/lsp/format.go +++ b/internal/lsp/format.go @@ -2,6 +2,7 @@ package lsp import ( "context" + "errors" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" @@ -17,7 +18,10 @@ func formatRange(ctx context.Context, v source.View, uri protocol.DocumentURI, r if err != nil { return nil, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return nil, err + } var r source.Range if rng == nil { r.Start = tok.Pos(0) @@ -29,15 +33,26 @@ func formatRange(ctx context.Context, v source.View, uri protocol.DocumentURI, r if err != nil { return nil, err } - return toProtocolEdits(f, edits), nil + + protocolEdits, err := toProtocolEdits(f, edits) + if err != nil { + return nil, err + } + return protocolEdits, nil } -func toProtocolEdits(f source.File, edits []source.TextEdit) []protocol.TextEdit { +func toProtocolEdits(f source.File, edits []source.TextEdit) ([]protocol.TextEdit, error) { if edits == nil { - return nil + return nil, errors.New("toProtocolEdits, edits == nil") + } + tok, err := f.GetToken() + if err != nil { + return nil, err + } + content, err := f.GetContent() + if err != nil { + return nil, err } - tok := f.GetToken() - content := f.GetContent() // When a file ends with an empty line, the newline character is counted // as part of the previous line. This causes the formatter to insert // another unnecessary newline on each formatting. We handle this case by @@ -56,5 +71,5 @@ func toProtocolEdits(f source.File, edits []source.TextEdit) []protocol.TextEdit NewText: edit.NewText, } } - return result + return result, nil } diff --git a/internal/lsp/imports.go b/internal/lsp/imports.go index fad98c82e8f..b4648a2172f 100644 --- a/internal/lsp/imports.go +++ b/internal/lsp/imports.go @@ -20,7 +20,10 @@ func organizeImports(ctx context.Context, v source.View, uri protocol.DocumentUR if err != nil { return nil, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return nil, err + } r := source.Range{ Start: tok.Pos(0), End: tok.Pos(tok.Size()), @@ -29,5 +32,9 @@ func organizeImports(ctx context.Context, v source.View, uri protocol.DocumentUR if err != nil { return nil, err } - return toProtocolEdits(f, edits), nil + protocolEdits, err := toProtocolEdits(f, edits) + if err != nil { + return nil, err + } + return protocolEdits, nil } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 75d22925755..42162ca71bc 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -396,7 +396,11 @@ func (f formats) test(t *testing.T, s *server) { }) } } - split := strings.SplitAfter(string(f.GetContent()), "\n") + contents, err := f.GetContent() + if err != nil { + t.Error(err) + } + split := strings.SplitAfter(string(contents), "\n") got := strings.Join(diff.ApplyEdits(split, ops), "") if gofmted != got { t.Errorf("format failed for %s: expected '%v', got '%v'", filename, gofmted, got) @@ -440,6 +444,11 @@ func (d definitions) test(t *testing.T, s *server, typ bool) { } func (d definitions) collect(fset *token.FileSet, src, target packagestest.Range) { - loc := toProtocolLocation(fset, source.Range(src)) - d[loc] = toProtocolLocation(fset, source.Range(target)) + srcLocation, err := toProtocolLocation(fset, source.Range(src)) + if err == nil { + targetLocation, err := toProtocolLocation(fset, source.Range(target)) + if err == nil { + d[*srcLocation] = *targetLocation + } + } } diff --git a/internal/lsp/position.go b/internal/lsp/position.go index fb0d9bdba51..f06a9b738c5 100644 --- a/internal/lsp/position.go +++ b/internal/lsp/position.go @@ -6,6 +6,7 @@ package lsp import ( "context" + "fmt" "go/token" "net/url" @@ -36,18 +37,24 @@ func fromProtocolLocation(ctx context.Context, v *cache.View, loc protocol.Locat if err != nil { return source.Range{}, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return source.Range{}, err + } return fromProtocolRange(tok, loc.Range), nil } // toProtocolLocation converts from a source range back to a protocol location. -func toProtocolLocation(fset *token.FileSet, r source.Range) protocol.Location { +func toProtocolLocation(fset *token.FileSet, r source.Range) (*protocol.Location, error) { tok := fset.File(r.Start) + if tok == nil { + return nil, fmt.Errorf("cannot get File for position=%+v", r.Start) + } uri := source.ToURI(tok.Name()) - return protocol.Location{ + return &protocol.Location{ URI: protocol.DocumentURI(uri), Range: toProtocolRange(tok, r), - } + }, nil } // fromProtocolRange converts a protocol range to a source range. diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 3db22fff13c..cda3c90a1a4 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -79,6 +79,10 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara } s.initialized = true // mark server as initialized now + if params.Trace != "verbose" { + params.Trace = "verbose" + } + // Check if the client supports snippets in completion items. s.snippetsSupported = params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport s.signatureHelpEnabled = true @@ -108,7 +112,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara Tests: true, }) - return &protocol.InitializeResult{ + initializeResult := &protocol.InitializeResult{ Capabilities: protocol.ServerCapabilities{ CodeActionProvider: true, CompletionProvider: protocol.CompletionOptions{ @@ -127,7 +131,9 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara }, TypeDefinitionProvider: true, }, - }, nil + } + + return initializeResult, nil } func (s *server) Initialized(context.Context, *protocol.InitializedParams) error { @@ -220,7 +226,10 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara if err != nil { return nil, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return nil, err + } pos := fromProtocolPosition(tok, params.Position) items, prefix, err := source.Completion(ctx, f, pos) if err != nil { @@ -245,7 +254,10 @@ func (s *server) Hover(ctx context.Context, params *protocol.TextDocumentPositio if err != nil { return nil, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return nil, err + } pos := fromProtocolPosition(tok, params.Position) ident, err := source.Identifier(ctx, s.view, f, pos) if err != nil { @@ -256,13 +268,14 @@ func (s *server) Hover(ctx context.Context, params *protocol.TextDocumentPositio return nil, err } markdown := "```go\n" + content + "\n```" - return &protocol.Hover{ + hover := &protocol.Hover{ Contents: protocol.MarkupContent{ Kind: protocol.Markdown, Value: markdown, }, Range: toProtocolRange(tok, ident.Range), - }, nil + } + return hover, nil } func (s *server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) { @@ -274,7 +287,10 @@ func (s *server) SignatureHelp(ctx context.Context, params *protocol.TextDocumen if err != nil { return nil, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return nil, err + } pos := fromProtocolPosition(tok, params.Position) info, err := source.SignatureHelp(ctx, f, pos) if err != nil { @@ -292,13 +308,20 @@ func (s *server) Definition(ctx context.Context, params *protocol.TextDocumentPo if err != nil { return nil, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return nil, err + } pos := fromProtocolPosition(tok, params.Position) ident, err := source.Identifier(ctx, s.view, f, pos) if err != nil { return nil, err } - return []protocol.Location{toProtocolLocation(s.view.FileSet(), ident.Declaration.Range)}, nil + protocolLocation, err := toProtocolLocation(s.view.FileSet(), ident.Declaration.Range) + if err != nil { + return nil, err + } + return []protocol.Location{*protocolLocation}, nil } func (s *server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { @@ -310,13 +333,20 @@ func (s *server) TypeDefinition(ctx context.Context, params *protocol.TextDocume if err != nil { return nil, err } - tok := f.GetToken() + tok, err := f.GetToken() + if err != nil { + return nil, err + } pos := fromProtocolPosition(tok, params.Position) ident, err := source.Identifier(ctx, s.view, f, pos) if err != nil { return nil, err } - return []protocol.Location{toProtocolLocation(s.view.FileSet(), ident.Type.Range)}, nil + protocolLocation, err := toProtocolLocation(s.view.FileSet(), ident.Type.Range) + if err != nil { + return nil, err + } + return []protocol.Location{*protocolLocation}, nil } func (s *server) Implementation(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) { diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go index a6799ad2fca..a748a175f16 100644 --- a/internal/lsp/source/completion.go +++ b/internal/lsp/source/completion.go @@ -47,8 +47,14 @@ type finder func(types.Object, float64, []CompletionItem) []CompletionItem // completion. For instance, some clients may tolerate imperfect matches as // valid completion results, since users may make typos. func Completion(ctx context.Context, f File, pos token.Pos) (items []CompletionItem, prefix string, err error) { - file := f.GetAST() - pkg := f.GetPackage() + file, err := f.GetAST() + if err != nil { + return nil, "", err + } + pkg, err := f.GetPackage() + if err != nil { + return nil, "", err + } path, _ := astutil.PathEnclosingInterval(file, pos, pos) if path == nil { return nil, "", fmt.Errorf("cannot find node enclosing position") diff --git a/internal/lsp/source/definition.go b/internal/lsp/source/definition.go index bc233c0c839..a20fd995aef 100644 --- a/internal/lsp/source/definition.go +++ b/internal/lsp/source/definition.go @@ -50,17 +50,30 @@ func Identifier(ctx context.Context, v View, f File, pos token.Pos) (*Identifier func (i *IdentifierInfo) Hover(q types.Qualifier) (string, error) { if q == nil { - fAST := i.File.GetAST() - pkg := i.File.GetPackage() + fAST, err := i.File.GetAST() + if err != nil { + return "", err + } + pkg, err := i.File.GetPackage() + if err != nil { + return "", err + } q = qualifier(fAST, pkg.Types, pkg.TypesInfo) } - return types.ObjectString(i.Declaration.Object, q), nil + hover := types.ObjectString(i.Declaration.Object, q) + return hover, nil } // identifier checks a single position for a potential identifier. func identifier(ctx context.Context, v View, f File, pos token.Pos) (*IdentifierInfo, error) { - fAST := f.GetAST() - pkg := f.GetPackage() + fAST, err := f.GetAST() + if err != nil { + return nil, err + } + pkg, err := f.GetPackage() + if err != nil { + return nil, err + } path, _ := astutil.PathEnclosingInterval(fAST, pos, pos) result := &IdentifierInfo{ File: f, @@ -97,7 +110,7 @@ func identifier(ctx context.Context, v View, f File, pos token.Pos) (*Identifier } } } - var err error + if result.Declaration.Range, err = objToRange(ctx, v, result.Declaration.Object); err != nil { return nil, err } diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go index 99a8fd9fd5e..f95922e2628 100644 --- a/internal/lsp/source/diagnostics.go +++ b/internal/lsp/source/diagnostics.go @@ -50,7 +50,10 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic, if err != nil { return nil, err } - pkg := f.GetPackage() + pkg, err := f.GetPackage() + if err != nil { + return nil, err + } // Prepare the reports we will send for this package. reports := make(map[string][]Diagnostic) for _, filename := range pkg.GoFiles { @@ -79,8 +82,15 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic, if err != nil { continue } - diagTok := diagFile.GetToken() - end, err := identifierEnd(diagFile.GetContent(), pos.Line, pos.Column) + diagTok, err := diagFile.GetToken() + if err != nil { + return nil, err + } + diagFileContent, err := diagFile.GetContent() + if err != nil { + return nil, err + } + end, err := identifierEnd(diagFileContent, pos.Line, pos.Column) // Don't set a range if it's anything other than a type error. if err != nil || diag.Kind != packages.TypeError { end = 0 diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go index 3c4ae69cd54..5e8ec624225 100644 --- a/internal/lsp/source/format.go +++ b/internal/lsp/source/format.go @@ -21,7 +21,10 @@ import ( // Format formats a file with a given range. func Format(ctx context.Context, f File, rng Range) ([]TextEdit, error) { - fAST := f.GetAST() + fAST, err := f.GetAST() + if err != nil { + return nil, err + } path, exact := astutil.PathEnclosingInterval(fAST, rng.Start, rng.End) if !exact || len(path) == 0 { return nil, fmt.Errorf("no exact AST node matching the specified range") @@ -52,21 +55,46 @@ func Format(ctx context.Context, f File, rng Range) ([]TextEdit, error) { if err := format.Node(buf, fset, node); err != nil { return nil, err } - return computeTextEdits(rng, f, buf.String()), nil + edits, err := computeTextEdits(rng, f, buf.String()) + if err != nil { + return nil, err + } + return edits, nil } // Imports formats a file using the goimports tool. func Imports(ctx context.Context, f File, rng Range) ([]TextEdit, error) { - formatted, err := imports.Process(f.GetToken().Name(), f.GetContent(), nil) + tok, err := f.GetToken() + if err != nil { + return nil, err + } + content, err := f.GetContent() + if err != nil { + return nil, err + } + formatted, err := imports.Process(tok.Name(), content, nil) + if err != nil { + return nil, err + } + edits, err := computeTextEdits(rng, f, string(formatted)) if err != nil { return nil, err } - return computeTextEdits(rng, f, string(formatted)), nil + return edits, nil } -func computeTextEdits(rng Range, file File, formatted string) (edits []TextEdit) { - u := strings.SplitAfter(string(file.GetContent()), "\n") - tok := file.GetToken() +func computeTextEdits(rng Range, file File, formatted string) ([]TextEdit, error) { + edits := []TextEdit{} + + contents, err := file.GetContent() + if err != nil { + return nil, err + } + u := strings.SplitAfter(string(contents), "\n") + tok, err := file.GetToken() + if err != nil { + return nil, err + } f := strings.SplitAfter(formatted, "\n") for _, op := range diff.Operations(u, f) { start := lineStart(tok, op.I1+1) @@ -97,5 +125,5 @@ func computeTextEdits(rng Range, file File, formatted string) (edits []TextEdit) }) } } - return edits + return edits, nil } diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go index 4478fed2771..3fd203dde02 100644 --- a/internal/lsp/source/signature_help.go +++ b/internal/lsp/source/signature_help.go @@ -25,8 +25,14 @@ type ParameterInformation struct { } func SignatureHelp(ctx context.Context, f File, pos token.Pos) (*SignatureInformation, error) { - fAST := f.GetAST() - pkg := f.GetPackage() + fAST, err := f.GetAST() + if err != nil { + return nil, err + } + pkg, err := f.GetPackage() + if err != nil { + return nil, err + } // Find a call expression surrounding the query position. var callExpr *ast.CallExpr @@ -104,9 +110,10 @@ func SignatureHelp(ctx context.Context, f File, pos token.Pos) (*SignatureInform if pkg := pkgStringer(obj.Pkg()); pkg != "" { label = pkg + "." + label } - return &SignatureInformation{ + signatureInformation := &SignatureInformation{ Label: label + formatParams(sig.Params(), sig.Variadic(), pkgStringer), Parameters: paramInfo, ActiveParameter: activeParam, - }, nil + } + return signatureInformation, nil } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 5a427170f20..717327b3c71 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -27,11 +27,11 @@ type View interface { // building blocks for most queries. Users of the source package can abstract // the loading of packages into their own caching systems. type File interface { - GetAST() *ast.File + GetAST() (*ast.File, error) GetFileSet() *token.FileSet - GetPackage() *packages.Package - GetToken() *token.File - GetContent() []byte + GetPackage() (*packages.Package, error) + GetToken() (*token.File, error) + GetContent() ([]byte, error) } // Range represents a start and end position. From f6e21a22c214366888bcd9dbc1cd90aff8874d82 Mon Sep 17 00:00:00 2001 From: mbana Date: Tue, 19 Feb 2019 08:42:09 +0000 Subject: [PATCH 2/2] Add instructions on how to develop `cmd/gopls`: * Add Markdown `cmd/gopls/GOPLS.md` file with instructions on how to install `cmd/gopls`. * Add script `cmd/gopls/install-gopls.sh` to automate install. --- cmd/gopls/GOPLS.md | 16 ++++++++++++++++ cmd/gopls/install-gopls.sh | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 cmd/gopls/GOPLS.md create mode 100755 cmd/gopls/install-gopls.sh diff --git a/cmd/gopls/GOPLS.md b/cmd/gopls/GOPLS.md new file mode 100644 index 00000000000..b423e3319b3 --- /dev/null +++ b/cmd/gopls/GOPLS.md @@ -0,0 +1,16 @@ +# `GOPLS.md` + +## development + +1. Create a fork of , e.g., . +2. Clone your fork. +3. Modify some code in the `internal/lsp` directory. A good candidate is the `Initialize` function +in `internal/lsp/server.go`. +4. Run the `cmd/gopls/install-gopls.sh` script to update the `gopls` binary. + +```sh +git clone git@github.com:banaio/tools.git +cd tools +vim internal/lsp/server.go # modify some code +cmd/gopls/install-gopls.sh +``` diff --git a/cmd/gopls/install-gopls.sh b/cmd/gopls/install-gopls.sh new file mode 100755 index 00000000000..c100a1acbf7 --- /dev/null +++ b/cmd/gopls/install-gopls.sh @@ -0,0 +1,24 @@ +#!/bin/bash -e +RACE=false +if [[ "${1}" == "-race" ]]; then + RACE=true +fi + +echo -e "\\033[92m ---> currrent cmd/gopls \\033[0m" +find "$(command -v gopls)" -printf "%c %p\\n" + +echo -e "\\033[92m ---> testing internal/lsp (race=${RACE}) \\033[0m" +if ${RACE}; then + go test -race "$(pwd)"/internal/lsp/... +else + go test "$(pwd)"/internal/lsp/... +fi + +if ${RACE}; then + go install -a -race "$(pwd)"/cmd/gopls +else + go install -a "$(pwd)"/cmd/gopls +fi + +echo -e "\\033[92m ---> installed cmd/gopls (race=${RACE}) \\033[0m" +find "$(command -v gopls)" -printf "%c %p\\n"