diff --git a/internal/langserver/handlers/complete_test.go b/internal/langserver/handlers/complete_test.go index bc5c01f3..2324e13b 100644 --- a/internal/langserver/handlers/complete_test.go +++ b/internal/langserver/handlers/complete_test.go @@ -457,50 +457,82 @@ func TestModuleCompletion_withValidDataAndSnippets(t *testing.T) { } var testModuleSchemaOutput = `{ - "format_version": "0.1", - "provider_schemas": { - "test/test": { - "provider": { - "version": 0, - "block": { - "attributes": { - "anonymous": { - "type": "number", - "description": "Desc 1", - "description_kind": "plaintext", - "optional": true - }, - "base_url": { - "type": "string", - "description": "Desc **2**", - "description_kind": "markdown", - "optional": true + "format_version": "0.1", + "provider_schemas": { + "test/test": { + "provider": { + "version": 0, + "block": { + "attributes": { + "anonymous": { + "type": "number", + "description": "Desc 1", + "description_kind": "plaintext", + "optional": true + }, + "base_url": { + "type": "string", + "description": "Desc **2**", + "description_kind": "markdown", + "optional": true + }, + "individual": { + "type": "bool", + "description": "Desc _3_", + "description_kind": "markdown", + "optional": true + } + } + } }, - "individual": { - "type": "bool", - "description": "Desc _3_", - "description_kind": "markdown", - "optional": true + "resource_schemas": { + "test_resource_1": { + "version": 0, + "block": { + "description": "Resource 1 description", + "description_kind": "markdown", + "attributes": { + "deprecated_attr": { + "deprecated": true + } + } + } + }, + "test_resource_2": { + "version": 0, + "block": { + "description_kind": "markdown", + "attributes": { + "optional_attr": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "block_types": { + "setting": { + "nesting_mode": "set", + "block": { + "attributes": { + "name": { + "type": "string", + "description_kind": "plain", + "required": true + }, + "value": { + "type": "string", + "description_kind": "plain", + "required": true + } + } + } + } + } + } + } } - } - } - } - } - }, - "resource_schemas": { - "test_resource_1": { - "version": 0, - "block": { - "description": "Resource 1 description", - "description_kind": "markdown", - "attributes": { - "deprecated_attr": { - "deprecated": true - } } - } } - } }` func TestVarsCompletion_withValidData(t *testing.T) { diff --git a/internal/langserver/handlers/go_to_ref_target.go b/internal/langserver/handlers/go_to_ref_target.go index 30be5f4f..fb20862c 100644 --- a/internal/langserver/handlers/go_to_ref_target.go +++ b/internal/langserver/handlers/go_to_ref_target.go @@ -3,18 +3,42 @@ package handlers import ( "context" + "github.com/hashicorp/hcl-lang/decoder" "github.com/hashicorp/hcl-lang/lang" lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) -func (svc *service) GoToReferenceTarget(ctx context.Context, params lsp.TextDocumentPositionParams) (interface{}, error) { +func (svc *service) GoToDefinition(ctx context.Context, params lsp.TextDocumentPositionParams) (interface{}, error) { cc, err := ilsp.ClientCapabilities(ctx) if err != nil { return nil, err } + targets, err := svc.goToReferenceTarget(ctx, params) + if err != nil { + return nil, err + } + + return ilsp.RefTargetsToLocationLinks(targets, cc.TextDocument.Definition.LinkSupport), nil +} + +func (svc *service) GoToDeclaration(ctx context.Context, params lsp.TextDocumentPositionParams) (interface{}, error) { + cc, err := ilsp.ClientCapabilities(ctx) + if err != nil { + return nil, err + } + + targets, err := svc.goToReferenceTarget(ctx, params) + if err != nil { + return nil, err + } + + return ilsp.RefTargetsToLocationLinks(targets, cc.TextDocument.Declaration.LinkSupport), nil +} + +func (svc *service) goToReferenceTarget(ctx context.Context, params lsp.TextDocumentPositionParams) (decoder.ReferenceTargets, error) { fs, err := lsctx.DocumentStorage(ctx) if err != nil { return nil, err @@ -34,10 +58,6 @@ func (svc *service) GoToReferenceTarget(ctx context.Context, params lsp.TextDocu Path: doc.Dir(), LanguageID: doc.LanguageID(), } - targets, err := svc.decoder.ReferenceTargetsForOriginAtPos(path, doc.Filename(), fPos.Position()) - if err != nil { - return nil, err - } - return ilsp.RefTargetsToLocationLinks(targets, cc.TextDocument.Declaration.LinkSupport), nil + return svc.decoder.ReferenceTargetsForOriginAtPos(path, doc.Filename(), fPos.Position()) } diff --git a/internal/langserver/handlers/go_to_ref_target_test.go b/internal/langserver/handlers/go_to_ref_target_test.go index 64c13760..c52abbc3 100644 --- a/internal/langserver/handlers/go_to_ref_target_test.go +++ b/internal/langserver/handlers/go_to_ref_target_test.go @@ -1,12 +1,14 @@ package handlers import ( + "encoding/json" "fmt" "path/filepath" "testing" "time" "github.com/hashicorp/go-version" + tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/langserver" "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" @@ -49,11 +51,7 @@ func TestDefinition_basic(t *testing.T) { ls.Call(t, &langserver.CallRequest{ Method: "initialize", ReqParams: fmt.Sprintf(`{ - "capabilities": { - "definition": { - "linkSupport": true - } - }, + "capabilities": {}, "rootUri": %q, "processId": 12345 }`, tmpDir.URI())}) @@ -106,6 +104,284 @@ output "foo" { }`, tmpDir.URI())) } +func TestDefinition_withLinkToDefLessBlock(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + var testSchema tfjson.ProviderSchemas + err := json.Unmarshal([]byte(testModuleSchemaOutput), &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.15.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": { + "textDocument": { + "definition": { + "linkSupport": true + } + } + }, + "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": `+fmt.Sprintf("%q", + `resource "test_resource_2" "foo" { + setting { + name = "foo" + value = "bar" + } +} + +output "foo" { + value = test_resource_2.foo.setting +}`)+`, + "uri": "%s/main.tf" + } + }`, tmpDir.URI())}) + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/definition", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + }, + "position": { + "line": 8, + "character": 35 + } + }`, tmpDir.URI())}, fmt.Sprintf(`{ + "jsonrpc": "2.0", + "id": 3, + "result": [ + { + "originSelectionRange": { + "start": { + "line": 8, + "character": 12 + }, + "end": { + "line": 8, + "character": 39 + } + }, + "targetUri": "%s/main.tf", + "targetRange": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 5 + } + }, + "targetSelectionRange": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 5 + } + } + } + ] + }`, tmpDir.URI())) +} + +func TestDefinition_withLinkToDefBlock(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + var testSchema tfjson.ProviderSchemas + err := json.Unmarshal([]byte(testModuleSchemaOutput), &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.15.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": { + "textDocument": { + "definition": { + "linkSupport": true + } + } + }, + "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": `+fmt.Sprintf("%q", + `resource "test_resource_2" "foo" { + setting { + name = "foo" + value = "bar" + } +} + +output "foo" { + value = test_resource_2.foo +}`)+`, + "uri": "%s/main.tf" + } + }`, tmpDir.URI())}) + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/definition", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + }, + "position": { + "line": 8, + "character": 30 + } + }`, tmpDir.URI())}, fmt.Sprintf(`{ + "jsonrpc": "2.0", + "id": 3, + "result": [ + { + "originSelectionRange": { + "start": { + "line": 8, + "character": 12 + }, + "end": { + "line": 8, + "character": 31 + } + }, + "targetUri": "%s/main.tf", + "targetRange": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 5, + "character": 1 + } + }, + "targetSelectionRange": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 32 + } + } + } + ] + }`, tmpDir.URI())) +} + func TestDefinition_moduleInputToVariable(t *testing.T) { modPath, err := filepath.Abs(filepath.Join("testdata", "single-submodule")) if err != nil { @@ -126,11 +402,7 @@ func TestDefinition_moduleInputToVariable(t *testing.T) { ls.Call(t, &langserver.CallRequest{ Method: "initialize", ReqParams: fmt.Sprintf(`{ - "capabilities": { - "definition": { - "linkSupport": true - } - }, + "capabilities": {}, "rootUri": %q, "processId": 12345 }`, modUri.URI())}) @@ -225,11 +497,7 @@ func TestDeclaration_basic(t *testing.T) { ls.Call(t, &langserver.CallRequest{ Method: "initialize", ReqParams: fmt.Sprintf(`{ - "capabilities": { - "definition": { - "linkSupport": true - } - }, + "capabilities": {}, "rootUri": %q, "processId": 12345 }`, tmpDir.URI())}) @@ -281,3 +549,142 @@ output "foo" { }] }`, tmpDir.URI())) } + +func TestDeclaration_withLinkSupport(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + var testSchema tfjson.ProviderSchemas + err := json.Unmarshal([]byte(testModuleSchemaOutput), &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.15.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": { + "textDocument": { + "declaration": { + "linkSupport": true + } + } + }, + "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": `+fmt.Sprintf("%q", + `resource "test_resource_2" "foo" { + setting { + name = "foo" + value = "bar" + } +} + +output "foo" { + value = test_resource_2.foo.setting +}`)+`, + "uri": "%s/main.tf" + } + }`, tmpDir.URI())}) + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/declaration", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + }, + "position": { + "line": 8, + "character": 35 + } + }`, tmpDir.URI())}, fmt.Sprintf(`{ + "jsonrpc": "2.0", + "id": 3, + "result": [ + { + "originSelectionRange": { + "start": { + "line": 8, + "character": 12 + }, + "end": { + "line": 8, + "character": 39 + } + }, + "targetUri": "%s/main.tf", + "targetRange": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 5 + } + }, + "targetSelectionRange": { + "start": { + "line": 1, + "character": 4 + }, + "end": { + "line": 4, + "character": 5 + } + } + } + ] + }`, tmpDir.URI())) +} diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index 2c8550b0..544d4d2a 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -194,7 +194,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) - return handle(ctx, req, svc.GoToReferenceTarget) + return handle(ctx, req, svc.GoToDeclaration) }, "textDocument/definition": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() @@ -205,7 +205,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) - return handle(ctx, req, svc.GoToReferenceTarget) + return handle(ctx, req, svc.GoToDefinition) }, "textDocument/completion": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() diff --git a/internal/lsp/location_links.go b/internal/lsp/location_links.go index 977b9a41..90cf5924 100644 --- a/internal/lsp/location_links.go +++ b/internal/lsp/location_links.go @@ -31,6 +31,7 @@ func refTargetToLocationLink(target *decoder.ReferenceTarget) lsp.LocationLink { OriginSelectionRange: HCLRangeToLSP(target.OriginRange), TargetURI: lsp.DocumentURI(targetUri), TargetRange: HCLRangeToLSP(target.Range), + TargetSelectionRange: HCLRangeToLSP(target.Range), } if target.DefRangePtr != nil { diff --git a/internal/state/provider_schema.go b/internal/state/provider_schema.go index 7c5525fc..475f0c71 100644 --- a/internal/state/provider_schema.go +++ b/internal/state/provider_schema.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-registry-address" + tfaddr "github.com/hashicorp/terraform-registry-address" tfschema "github.com/hashicorp/terraform-schema/schema" )