From c477bf9c8a1de6ab2eca00237647ab30c4256371 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 13 Nov 2020 08:35:21 +0000 Subject: [PATCH] langserver: Implement textDocument/hover (#294) * Bump hcl-lang to latest revision * langserver: Implement textDocument/hover --- go.mod | 2 +- go.sum | 4 +- internal/lsp/hover.go | 26 ++++++ langserver/handlers/complete_test.go | 1 - langserver/handlers/handlers_test.go | 1 + langserver/handlers/hover.go | 62 +++++++++++++ langserver/handlers/hover_test.go | 129 +++++++++++++++++++++++++++ langserver/handlers/initialize.go | 1 + langserver/handlers/service.go | 12 +++ 9 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 internal/lsp/hover.go create mode 100644 langserver/handlers/hover.go create mode 100644 langserver/handlers/hover_test.go diff --git a/go.mod b/go.mod index 9a3f9eff1..6e7dd6670 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/go-cmp v0.5.1 github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-version v1.2.1 - github.com/hashicorp/hcl-lang v0.0.0-20201112193825-eb220f038552 + github.com/hashicorp/hcl-lang v0.0.0-20201113080530-b0668b270b47 github.com/hashicorp/hcl/v2 v2.6.0 github.com/hashicorp/terraform-exec v0.11.1-0.20201007122305-ea2094d52cb5 github.com/hashicorp/terraform-json v0.6.0 diff --git a/go.sum b/go.sum index 20f0f2776..8a91f6a15 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,8 @@ 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 h1:EjnMRaTQlomBMNRQfyWoLEg9IdqxeN1R2mb3ZZetCBs= github.com/hashicorp/hcl-lang v0.0.0-20201110071249-4e412924f52b/go.mod h1:vd3BPEDWrYMAgAnB0MRlBdZknrpUXf8Jk2PNaHIbwhg= -github.com/hashicorp/hcl-lang v0.0.0-20201112193825-eb220f038552 h1:JA1B6R+7yNbBxwZsOFy50EWfWKVQWTjAp5QZsSxnZbA= -github.com/hashicorp/hcl-lang v0.0.0-20201112193825-eb220f038552/go.mod h1:vd3BPEDWrYMAgAnB0MRlBdZknrpUXf8Jk2PNaHIbwhg= +github.com/hashicorp/hcl-lang v0.0.0-20201113080530-b0668b270b47 h1:n3STOLqEwfs4QWgzV0cjg4sZDSKVM4IwK38ZDVa+ljM= +github.com/hashicorp/hcl-lang v0.0.0-20201113080530-b0668b270b47/go.mod h1:vd3BPEDWrYMAgAnB0MRlBdZknrpUXf8Jk2PNaHIbwhg= 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= diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go new file mode 100644 index 000000000..30f73cd00 --- /dev/null +++ b/internal/lsp/hover.go @@ -0,0 +1,26 @@ +package lsp + +import ( + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/terraform-ls/internal/mdplain" + "github.com/sourcegraph/go-lsp" +) + +func HoverData(data *lang.HoverData, cc lsp.TextDocumentClientCapabilities) lsp.Hover { + mdSupported := cc.Hover != nil && + len(cc.Hover.ContentFormat) > 0 && + cc.Hover.ContentFormat[0] == "markdown" + + value := data.Content.Value + if data.Content.Kind == lang.MarkdownKind && !mdSupported { + value = mdplain.Clean(value) + } + + content := lsp.RawMarkedString(value) + rng := HCLRangeToLSP(data.Range) + + return lsp.Hover{ + Contents: []lsp.MarkedString{content}, + Range: &rng, + } +} diff --git a/langserver/handlers/complete_test.go b/langserver/handlers/complete_test.go index 1d4060ef3..7e384e8e5 100644 --- a/langserver/handlers/complete_test.go +++ b/langserver/handlers/complete_test.go @@ -34,7 +34,6 @@ func TestCompletion_withoutInitialization(t *testing.T) { func TestCompletion_withValidData(t *testing.T) { tmpDir := TempDir(t) - t.Logf("will init at %s", tmpDir.Dir()) InitPluginCache(t, tmpDir.Dir()) var testSchema tfjson.ProviderSchemas diff --git a/langserver/handlers/handlers_test.go b/langserver/handlers/handlers_test.go index 71c3ced2a..4b215f6ce 100644 --- a/langserver/handlers/handlers_test.go +++ b/langserver/handlers/handlers_test.go @@ -32,6 +32,7 @@ func initializeResponse(t *testing.T, commandPrefix string) string { "openClose": true, "change": 2 }, + "hoverProvider": true, "completionProvider": {}, "documentSymbolProvider":true, "documentFormattingProvider":true, diff --git a/langserver/handlers/hover.go b/langserver/handlers/hover.go new file mode 100644 index 000000000..dd2ec3111 --- /dev/null +++ b/langserver/handlers/hover.go @@ -0,0 +1,62 @@ +package handlers + +import ( + "context" + + lsctx "github.com/hashicorp/terraform-ls/internal/context" + ilsp "github.com/hashicorp/terraform-ls/internal/lsp" + lsp "github.com/sourcegraph/go-lsp" +) + +func (h *logHandler) TextDocumentHover(ctx context.Context, params lsp.TextDocumentPositionParams) (lsp.Hover, error) { + var data lsp.Hover + + fs, err := lsctx.DocumentStorage(ctx) + if err != nil { + return data, err + } + + cc, err := lsctx.ClientCapabilities(ctx) + if err != nil { + return data, err + } + + rmf, err := lsctx.RootModuleFinder(ctx) + if err != nil { + return data, err + } + + file, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)) + if err != nil { + return data, err + } + + rm, err := rmf.RootModuleByPath(file.Dir()) + if err != nil { + return data, err + } + + schema, err := rmf.SchemaForPath(file.Dir()) + if err != nil { + return data, err + } + + d, err := rm.DecoderWithSchema(schema) + if err != nil { + return data, err + } + + fPos, err := ilsp.FilePositionFromDocumentPosition(params, file) + if err != nil { + return data, err + } + + h.logger.Printf("Looking for hover data at %q -> %#v", file.Filename(), fPos.Position()) + hoverData, err := d.HoverAtPos(file.Filename(), fPos.Position()) + h.logger.Printf("received hover data: %#v", data) + if err != nil { + return data, err + } + + return ilsp.HoverData(hoverData, cc.TextDocument), nil +} diff --git a/langserver/handlers/hover_test.go b/langserver/handlers/hover_test.go new file mode 100644 index 000000000..19f6cc0f9 --- /dev/null +++ b/langserver/handlers/hover_test.go @@ -0,0 +1,129 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hashicorp/go-version" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" + "github.com/hashicorp/terraform-ls/internal/terraform/rootmodule" + "github.com/hashicorp/terraform-ls/langserver" + "github.com/hashicorp/terraform-ls/langserver/session" + "github.com/stretchr/testify/mock" +) + +func TestHover_withoutInitialization(t *testing.T) { + ls := langserver.NewLangServerMock(t, NewMockSession(nil)) + stop := ls.Start(t) + defer stop() + + ls.CallAndExpectError(t, &langserver.CallRequest{ + Method: "textDocument/hover", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + }, + "position": { + "character": 0, + "line": 1 + } + }`, TempDir(t).URI())}, session.SessionNotInitialized.Err()) +} + +func TestHover_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{ + RootModules: map[string]*rootmodule.RootModuleMock{ + tmpDir.Dir(): { + TfExecFactory: exec.NewMockExecutor([]*mock.Call{ + { + Method: "Version", + Repeatability: 1, + Arguments: []interface{}{ + mock.AnythingOfType(""), + }, + ReturnArguments: []interface{}{ + version.Must(version.NewVersion("0.12.0")), + 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 + }`, TempDir(t).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" + } + }`, TempDir(t).URI())}) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/hover", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + }, + "position": { + "character": 3, + "line": 0 + } + }`, TempDir(t).URI())}, `{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "contents": [ + "provider Block\n\nA provider block is used to specify a provider configuration" + ], + "range": { + "start": { "line":0, "character":0 }, + "end": { "line":0, "character":8 } + } + } + }`) +} diff --git a/langserver/handlers/initialize.go b/langserver/handlers/initialize.go index 84c6efabf..13d5c3135 100644 --- a/langserver/handlers/initialize.go +++ b/langserver/handlers/initialize.go @@ -26,6 +26,7 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam CompletionProvider: &lsp.CompletionOptions{ ResolveProvider: false, }, + HoverProvider: true, DocumentFormattingProvider: true, DocumentSymbolProvider: true, }, diff --git a/langserver/handlers/service.go b/langserver/handlers/service.go index 6a2107714..390fe0418 100644 --- a/langserver/handlers/service.go +++ b/langserver/handlers/service.go @@ -225,6 +225,18 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return handle(ctx, req, lh.TextDocumentComplete) }, + "textDocument/hover": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { + err := session.CheckInitializationIsConfirmed() + if err != nil { + return nil, err + } + + ctx = lsctx.WithDocumentStorage(ctx, fs) + ctx = lsctx.WithClientCapabilities(ctx, cc) + ctx = lsctx.WithRootModuleFinder(ctx, svc.modMgr) + + return handle(ctx, req, lh.TextDocumentHover) + }, "textDocument/formatting": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() if err != nil {