From f9eb59f7d5eeb23c3f0af94176961d6189bdcf1a Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 15 Feb 2021 11:31:59 +0000 Subject: [PATCH] Add docs links to document and hover (#402) * Add docs links to document and hover * Bump hcl-lang and terraform-schema to latest revisions --- go.mod | 4 +- go.sum | 9 +- internal/cmd/completion_command.go | 3 +- internal/context/context.go | 23 ++++ internal/decoder/decoder.go | 35 ++++++ internal/langserver/handlers/complete.go | 4 +- internal/langserver/handlers/document_link.go | 55 +++++++++ .../langserver/handlers/document_link_test.go | 112 ++++++++++++++++++ internal/langserver/handlers/hover.go | 4 +- internal/langserver/handlers/initialize.go | 7 ++ .../langserver/handlers/semantic_tokens.go | 4 +- internal/langserver/handlers/service.go | 16 +++ internal/langserver/handlers/symbols.go | 4 +- internal/lsp/links.go | 24 ++++ internal/terraform/module/module_ops.go | 19 --- 15 files changed, 288 insertions(+), 35 deletions(-) create mode 100644 internal/decoder/decoder.go create mode 100644 internal/langserver/handlers/document_link.go create mode 100644 internal/langserver/handlers/document_link_test.go create mode 100644 internal/lsp/links.go diff --git a/go.mod b/go.mod index b9368e99b..b75bb041d 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,11 @@ require ( github.com/google/uuid v1.2.0 github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-version v1.2.1 - github.com/hashicorp/hcl-lang v0.0.0-20201209145723-0c4061e492db + github.com/hashicorp/hcl-lang v0.0.0-20210213170001-bd00c3f68680 github.com/hashicorp/hcl/v2 v2.8.2 github.com/hashicorp/terraform-exec v0.13.0 github.com/hashicorp/terraform-json v0.8.0 - github.com/hashicorp/terraform-schema v0.0.0-20201208163444-44d0347ab290 + github.com/hashicorp/terraform-schema v0.0.0-20210213170531-2b07abee4703 github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 github.com/mitchellh/cli v1.1.2 github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index a795f1770..f4c85ddcf 100644 --- a/go.sum +++ b/go.sum @@ -190,9 +190,8 @@ github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+Db github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl-lang v0.0.0-20201110071249-4e412924f52b/go.mod h1:vd3BPEDWrYMAgAnB0MRlBdZknrpUXf8Jk2PNaHIbwhg= -github.com/hashicorp/hcl-lang v0.0.0-20201209145723-0c4061e492db h1:Euxzz3x8BlYyNKiENK5LdOjA3i9C0UiqALp3TnWbkck= -github.com/hashicorp/hcl-lang v0.0.0-20201209145723-0c4061e492db/go.mod h1:TZ5tpvmgJSHfmIndN4WP9SpZvyWK8tHPBY8LDRyU+pI= +github.com/hashicorp/hcl-lang v0.0.0-20210213170001-bd00c3f68680 h1:QjPFluG2uGJCBKIp5sEFx+UZ98QnuxmVsnLIuHz8a2Y= +github.com/hashicorp/hcl-lang v0.0.0-20210213170001-bd00c3f68680/go.mod h1:TZ5tpvmgJSHfmIndN4WP9SpZvyWK8tHPBY8LDRyU+pI= github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o= github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= @@ -210,8 +209,8 @@ github.com/hashicorp/terraform-json v0.7.0 h1:DgkfLARKMQ/xmzVtSRX9Vz/fzPCL3vskHI github.com/hashicorp/terraform-json v0.7.0/go.mod h1:3defM4kkMfttwiE7VakJDwCd4R+umhSQnvJwORXbprE= github.com/hashicorp/terraform-json v0.8.0 h1:XObQ3PgqU52YLQKEaJ08QtUshAfN3yu4u8ebSW0vztc= github.com/hashicorp/terraform-json v0.8.0/go.mod h1:3defM4kkMfttwiE7VakJDwCd4R+umhSQnvJwORXbprE= -github.com/hashicorp/terraform-schema v0.0.0-20201208163444-44d0347ab290 h1:kAs5ZG+cgtWy3+81Z7G/Blj2imiDLfFBRaqmNs8mD4o= -github.com/hashicorp/terraform-schema v0.0.0-20201208163444-44d0347ab290/go.mod h1:eRHMO4QL4TTka07aC7fH+AXvi/tYlv6udrA8nSFOl6g= +github.com/hashicorp/terraform-schema v0.0.0-20210213170531-2b07abee4703 h1:u1otHGsvYAQcQ3aJl+TJjkFRdxJ1I8n7O9uGOlFw6oE= +github.com/hashicorp/terraform-schema v0.0.0-20210213170531-2b07abee4703/go.mod h1:6UFjY3ZeFEExipVkJOLzMJzBBosso6MoAyLRHgDIllw= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= diff --git a/internal/cmd/completion_command.go b/internal/cmd/completion_command.go index 4c3717e85..a37ef6a8e 100644 --- a/internal/cmd/completion_command.go +++ b/internal/cmd/completion_command.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/hashicorp/terraform-ls/internal/decoder" "github.com/hashicorp/terraform-ls/internal/filesystem" "github.com/hashicorp/terraform-ls/internal/logging" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" @@ -117,7 +118,7 @@ func (c *CompletionCommand) Run(args []string) int { return 1 } - d, err := module.DecoderForModule(mod) + d, err := decoder.DecoderForModule(ctx, mod) if err != nil { c.Ui.Error(fmt.Sprintf("failed to find decoder: %s", err.Error())) return 1 diff --git a/internal/context/context.go b/internal/context/context.go index 8f8a65d8b..ae306a2ad 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -23,6 +23,7 @@ var ( ctxDs = &contextKey{"document storage"} ctxClientCapsSetter = &contextKey{"client capabilities setter"} ctxClientCaps = &contextKey{"client capabilities"} + ctxClientName = &contextKey{"client name"} ctxTfExecPath = &contextKey{"terraform executable path"} ctxTfExecLogPath = &contextKey{"terraform executor log path"} ctxTfExecTimeout = &contextKey{"terraform execution timeout"} @@ -82,6 +83,28 @@ func ClientCapabilities(ctx context.Context) (lsp.ClientCapabilities, error) { return *caps, nil } +func WithClientName(ctx context.Context, namePtr *string) context.Context { + return context.WithValue(ctx, ctxClientName, namePtr) +} + +func ClientName(ctx context.Context) (string, bool) { + name, ok := ctx.Value(ctxClientName).(*string) + if !ok { + return "", false + } + return *name, true +} + +func SetClientName(ctx context.Context, name string) error { + namePtr, ok := ctx.Value(ctxClientName).(*string) + if !ok { + return missingContextErr(ctxClientName) + } + + *namePtr = name + return nil +} + func WithTerraformExecLogPath(ctx context.Context, path string) context.Context { return context.WithValue(ctx, ctxTfExecLogPath, path) } diff --git a/internal/decoder/decoder.go b/internal/decoder/decoder.go new file mode 100644 index 000000000..0ce3f71f3 --- /dev/null +++ b/internal/decoder/decoder.go @@ -0,0 +1,35 @@ +package decoder + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/decoder" + lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/terraform/module" +) + +func DecoderForModule(ctx context.Context, mod module.Module) (*decoder.Decoder, error) { + d := decoder.NewDecoder() + d.SetUtmSource("terraform-ls") + d.UseUtmContent(true) + + clientName, ok := lsctx.ClientName(ctx) + if ok { + d.SetUtmMedium(clientName) + } + + pf, err := mod.ParsedFiles() + if err != nil { + return nil, err + } + + for name, f := range pf { + err := d.LoadFile(name, f) + if err != nil { + return nil, fmt.Errorf("failed to load a file: %w", err) + } + } + + return d, nil +} diff --git a/internal/langserver/handlers/complete.go b/internal/langserver/handlers/complete.go index aaf6c0f1f..444e0dd61 100644 --- a/internal/langserver/handlers/complete.go +++ b/internal/langserver/handlers/complete.go @@ -4,9 +4,9 @@ import ( "context" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/decoder" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" - "github.com/hashicorp/terraform-ls/internal/terraform/module" ) func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.CompletionParams) (lsp.CompletionList, error) { @@ -42,7 +42,7 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple return list, err } - d, err := module.DecoderForModule(mod) + d, err := decoder.DecoderForModule(ctx, mod) if err != nil { return list, err } diff --git a/internal/langserver/handlers/document_link.go b/internal/langserver/handlers/document_link.go new file mode 100644 index 000000000..465c77a8e --- /dev/null +++ b/internal/langserver/handlers/document_link.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "context" + + lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/decoder" + ilsp "github.com/hashicorp/terraform-ls/internal/lsp" + lsp "github.com/hashicorp/terraform-ls/internal/protocol" +) + +func (h *logHandler) TextDocumentLink(ctx context.Context, params lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) { + fs, err := lsctx.DocumentStorage(ctx) + if err != nil { + return nil, err + } + + cc, err := lsctx.ClientCapabilities(ctx) + if err != nil { + return nil, err + } + + mf, err := lsctx.ModuleFinder(ctx) + if err != nil { + return nil, err + } + + file, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) + if err != nil { + return nil, err + } + + mod, err := mf.ModuleByPath(file.Dir()) + if err != nil { + return nil, err + } + + schema, err := mf.SchemaForModule(file.Dir()) + if err != nil { + return nil, err + } + + d, err := decoder.DecoderForModule(ctx, mod) + if err != nil { + return nil, err + } + d.SetSchema(schema) + + links, err := d.LinksInFile(file.Filename()) + if err != nil { + return nil, err + } + + return ilsp.Links(links, cc.TextDocument.DocumentLink), nil +} diff --git a/internal/langserver/handlers/document_link_test.go b/internal/langserver/handlers/document_link_test.go new file mode 100644 index 000000000..d995e240b --- /dev/null +++ b/internal/langserver/handlers/document_link_test.go @@ -0,0 +1,112 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hashicorp/go-version" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/langserver" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" + "github.com/stretchr/testify/mock" +) + +func TestDocumentLink_withValidData(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + var testSchema tfjson.ProviderSchemas + err := json.Unmarshal([]byte(testSchemaOutput), &testSchema) + if err != nil { + t.Fatal(err) + } + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + tmpDir.Dir(): { + { + Method: "Version", + Repeatability: 1, + Arguments: []interface{}{ + mock.AnythingOfType(""), + }, + ReturnArguments: []interface{}{ + version.Must(version.NewVersion("0.12.0")), + nil, + nil, + }, + }, + { + Method: "GetExecPath", + Repeatability: 1, + ReturnArguments: []interface{}{ + "", + }, + }, + { + Method: "ProviderSchemas", + Repeatability: 1, + Arguments: []interface{}{ + mock.AnythingOfType(""), + }, + ReturnArguments: []interface{}{ + &testSchema, + nil, + }, + }, + }, + }, + }})) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI())}) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "version": 0, + "languageId": "terraform", + "text": "provider \"test\" {\n\n}\n", + "uri": "%s/main.tf" + } + }`, tmpDir.URI())}) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/documentLink", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + } + }`, tmpDir.URI())}, `{ + "jsonrpc": "2.0", + "id": 3, + "result": [ + { + "range": { + "start": { + "line": 0, + "character": 9 + }, + "end": { + "line": 0, + "character": 15 + } + }, + "target": "https://registry.terraform.io/providers/hashicorp/test/latest/docs?utm_content=documentLink\u0026utm_source=terraform-ls" + } + ] + }`) +} diff --git a/internal/langserver/handlers/hover.go b/internal/langserver/handlers/hover.go index b654e3348..02c332d8b 100644 --- a/internal/langserver/handlers/hover.go +++ b/internal/langserver/handlers/hover.go @@ -4,9 +4,9 @@ import ( "context" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/decoder" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" - "github.com/hashicorp/terraform-ls/internal/terraform/module" ) func (h *logHandler) TextDocumentHover(ctx context.Context, params lsp.TextDocumentPositionParams) (*lsp.Hover, error) { @@ -40,7 +40,7 @@ func (h *logHandler) TextDocumentHover(ctx context.Context, params lsp.TextDocum return nil, err } - d, err := module.DecoderForModule(mod) + d, err := decoder.DecoderForModule(ctx, mod) if err != nil { return nil, err } diff --git a/internal/langserver/handlers/initialize.go b/internal/langserver/handlers/initialize.go index d21709f2f..c51d10fc2 100644 --- a/internal/langserver/handlers/initialize.go +++ b/internal/langserver/handlers/initialize.go @@ -51,6 +51,13 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam return serverCaps, err } + if params.ClientInfo.Name != "" { + err = lsctx.SetClientName(ctx, params.ClientInfo.Name) + if err != nil { + return serverCaps, err + } + } + clientCaps := params.Capabilities err = lsctx.SetClientCapabilities(ctx, &clientCaps) if err != nil { diff --git a/internal/langserver/handlers/semantic_tokens.go b/internal/langserver/handlers/semantic_tokens.go index 9ad706d0b..8e7c68949 100644 --- a/internal/langserver/handlers/semantic_tokens.go +++ b/internal/langserver/handlers/semantic_tokens.go @@ -6,9 +6,9 @@ import ( "github.com/creachadair/jrpc2/code" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/decoder" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" - "github.com/hashicorp/terraform-ls/internal/terraform/module" ) func (lh *logHandler) TextDocumentSemanticTokensFull(ctx context.Context, params lsp.SemanticTokensParams) (lsp.SemanticTokens, error) { @@ -56,7 +56,7 @@ func (lh *logHandler) TextDocumentSemanticTokensFull(ctx context.Context, params return tks, err } - d, err := module.DecoderForModule(mod) + d, err := decoder.DecoderForModule(ctx, mod) if err != nil { return tks, err } diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index d94a0f9fb..4eb60c282 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -122,6 +122,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { rootDir := "" commandPrefix := "" + clientName := "" var expFeatures settings.ExperimentalFeatures m := map[string]rpch.Func{ @@ -136,6 +137,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithModuleWalker(ctx, svc.walker) ctx = lsctx.WithRootDirectory(ctx, &rootDir) ctx = lsctx.WithCommandPrefix(ctx, &commandPrefix) + ctx = lsctx.WithClientName(ctx, &clientName) ctx = lsctx.WithModuleManager(ctx, svc.modMgr) ctx = lsctx.WithExperimentalFeatures(ctx, &expFeatures) @@ -200,6 +202,19 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return handle(ctx, req, lh.TextDocumentSymbol) }, + "textDocument/documentLink": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { + err := session.CheckInitializationIsConfirmed() + if err != nil { + return nil, err + } + + ctx = lsctx.WithDocumentStorage(ctx, svc.fs) + ctx = lsctx.WithClientCapabilities(ctx, cc) + ctx = lsctx.WithClientName(ctx, &clientName) + ctx = lsctx.WithModuleFinder(ctx, svc.modMgr) + + return handle(ctx, req, lh.TextDocumentLink) + }, "textDocument/completion": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() if err != nil { @@ -220,6 +235,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = lsctx.WithClientCapabilities(ctx, cc) + ctx = lsctx.WithClientName(ctx, &clientName) ctx = lsctx.WithModuleFinder(ctx, svc.modMgr) return handle(ctx, req, lh.TextDocumentHover) diff --git a/internal/langserver/handlers/symbols.go b/internal/langserver/handlers/symbols.go index 1bf99a3f2..d41dd5cf6 100644 --- a/internal/langserver/handlers/symbols.go +++ b/internal/langserver/handlers/symbols.go @@ -4,9 +4,9 @@ import ( "context" lsctx "github.com/hashicorp/terraform-ls/internal/context" + "github.com/hashicorp/terraform-ls/internal/decoder" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" - "github.com/hashicorp/terraform-ls/internal/terraform/module" ) func (h *logHandler) TextDocumentSymbol(ctx context.Context, params lsp.DocumentSymbolParams) ([]lsp.SymbolInformation, error) { @@ -32,7 +32,7 @@ func (h *logHandler) TextDocumentSymbol(ctx context.Context, params lsp.Document return symbols, err } - d, err := module.DecoderForModule(mod) + d, err := decoder.DecoderForModule(ctx, mod) if err != nil { return symbols, err } diff --git a/internal/lsp/links.go b/internal/lsp/links.go new file mode 100644 index 000000000..560c14653 --- /dev/null +++ b/internal/lsp/links.go @@ -0,0 +1,24 @@ +package lsp + +import ( + "github.com/hashicorp/hcl-lang/lang" + lsp "github.com/hashicorp/terraform-ls/internal/protocol" +) + +func Links(links []lang.Link, caps lsp.DocumentLinkClientCapabilities) []lsp.DocumentLink { + docLinks := make([]lsp.DocumentLink, len(links)) + + for i, link := range links { + tooltip := "" + if caps.TooltipSupport { + tooltip = link.Tooltip + } + docLinks[i] = lsp.DocumentLink{ + Range: HCLRangeToLSP(link.Range), + Target: link.URI, + Tooltip: tooltip, + } + } + + return docLinks +} diff --git a/internal/terraform/module/module_ops.go b/internal/terraform/module/module_ops.go index 114a9b70d..2aa04c770 100644 --- a/internal/terraform/module/module_ops.go +++ b/internal/terraform/module/module_ops.go @@ -6,7 +6,6 @@ import ( "path/filepath" "strings" - "github.com/hashicorp/hcl-lang/decoder" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform-ls/internal/terraform/datadir" @@ -194,21 +193,3 @@ func ParseModuleManifest(mod Module) { m.SetModuleManifest(mm, err) } - -func DecoderForModule(mod Module) (*decoder.Decoder, error) { - d := decoder.NewDecoder() - - pf, err := mod.ParsedFiles() - if err != nil { - return nil, err - } - - for name, f := range pf { - err := d.LoadFile(name, f) - if err != nil { - return nil, fmt.Errorf("failed to load a file: %w", err) - } - } - - return d, nil -}