Skip to content

Commit

Permalink
Add support for function signature help (#1077)
Browse files Browse the repository at this point in the history
* Bump hcl-lang to 3faa6c

* Add functions to PathContext

* Add SignatureHelp handler

* Test SignatureHelp handler

* Set signature help trigger characters

* Review feedback

* Move ActiveParameter
  • Loading branch information
dbanck committed Mar 17, 2023
1 parent a377686 commit 68b265f
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 4 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hc-install v0.5.0
github.com/hashicorp/hcl-lang v0.0.0-20230310081458-97626944945e
github.com/hashicorp/hcl-lang v0.0.0-20230315123252-3faa6c8fba28
github.com/hashicorp/hcl/v2 v2.16.2
github.com/hashicorp/terraform-exec v0.18.1
github.com/hashicorp/terraform-json v0.16.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ github.com/hashicorp/hc-install v0.5.0 h1:D9bl4KayIYKEeJ4vUDe9L5huqxZXczKaykSRcm
github.com/hashicorp/hc-install v0.5.0/go.mod h1:JyzMfbzfSBSjoDCRPna1vi/24BEDxFaCPfdHtM5SCdo=
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-20230310081458-97626944945e h1:3VRs8PujogY3D+hKc9ChpKK7rP+y/WTaDAKjORrOxk8=
github.com/hashicorp/hcl-lang v0.0.0-20230310081458-97626944945e/go.mod h1:m+ZB0CmTRHSo14j2INkDA+QZdzGBJRYTrOzf+4Xuj1k=
github.com/hashicorp/hcl-lang v0.0.0-20230315123252-3faa6c8fba28 h1:7RUI5eMB9ULXn6+V9mNyGyM5o33FBzv8pad/W+aEx04=
github.com/hashicorp/hcl-lang v0.0.0-20230315123252-3faa6c8fba28/go.mod h1:m+ZB0CmTRHSo14j2INkDA+QZdzGBJRYTrOzf+4Xuj1k=
github.com/hashicorp/hcl/v2 v2.16.2 h1:mpkHZh/Tv+xet3sy3F9Ld4FyI2tUpWe9x3XtPx9f1a0=
github.com/hashicorp/hcl/v2 v2.16.2/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
Expand Down
1 change: 1 addition & 0 deletions internal/decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func modulePathContext(mod *state.Module, schemaReader state.SchemaReader, modRe
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File, 0),
Functions: coreFunctions(mod),
}

for _, origin := range mod.RefOrigins {
Expand Down
41 changes: 41 additions & 0 deletions internal/decoder/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package decoder

import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/terraform-ls/internal/state"
tfschema "github.com/hashicorp/terraform-schema/schema"
)

func coreFunctions(mod *state.Module) map[string]schema.FunctionSignature {
if mod.TerraformVersion != nil {
s, err := tfschema.FunctionsForVersion(mod.TerraformVersion)
if err == nil {
return s
}
if mod.TerraformVersion.LessThan(tfschema.OldestAvailableVersion) {
return mustFunctionsForVersion(tfschema.OldestAvailableVersion)
}

return mustFunctionsForVersion(tfschema.LatestAvailableVersion)
}

s, err := tfschema.FunctionsForConstraint(mod.Meta.CoreRequirements)
if err == nil {
return s
}
if mod.Meta.CoreRequirements.Check(tfschema.OldestAvailableVersion) {
return mustFunctionsForVersion(tfschema.OldestAvailableVersion)
}

return mustFunctionsForVersion(tfschema.LatestAvailableVersion)
}

func mustFunctionsForVersion(v *version.Version) map[string]schema.FunctionSignature {
s, err := tfschema.FunctionsForVersion(v)
if err != nil {
// this should never happen
panic(err)
}
return s
}
4 changes: 3 additions & 1 deletion internal/langserver/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ func initializeResponse(t *testing.T, commandPrefix string) string {
"completionItem":{}
},
"hoverProvider": true,
"signatureHelpProvider": {},
"signatureHelpProvider": {
"triggerCharacters":["(",","]
},
"declarationProvider": {},
"definitionProvider": true,
"referencesProvider": true,
Expand Down
3 changes: 3 additions & 0 deletions internal/langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ func initializeResult(ctx context.Context) lsp.InitializeResult {
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
},
},
SignatureHelpProvider: lsp.SignatureHelpOptions{
TriggerCharacters: []string{"(", ","},
},
},
}

Expand Down
10 changes: 10 additions & 0 deletions internal/langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,16 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {

return handle(ctx, req, svc.TextDocumentFormatting)
},
"textDocument/signatureHelp": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}

ctx = ilsp.WithClientCapabilities(ctx, cc)

return handle(ctx, req, svc.SignatureHelp)
},
"textDocument/semanticTokens/full": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
Expand Down
38 changes: 38 additions & 0 deletions internal/langserver/handlers/signature_help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package handlers

import (
"context"

ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
)

func (svc *service) SignatureHelp(ctx context.Context, params lsp.SignatureHelpParams) (*lsp.SignatureHelp, error) {
_, err := ilsp.ClientCapabilities(ctx)
if err != nil {
return nil, err
}

dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI)
doc, err := svc.stateStore.DocumentStore.GetDocument(dh)
if err != nil {
return nil, err
}

d, err := svc.decoderForDocument(ctx, doc)
if err != nil {
return nil, err
}

pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc)
if err != nil {
return nil, err
}

sig, err := d.SignatureAtPos(doc.Filename, pos)
if err != nil {
return nil, err
}

return ilsp.ToSignatureHelp(sig), nil
}
150 changes: 150 additions & 0 deletions internal/langserver/handlers/signature_help_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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/langserver/session"
"github.com/hashicorp/terraform-ls/internal/state"
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
"github.com/hashicorp/terraform-ls/internal/walker"
"github.com/stretchr/testify/mock"
)

func TestSignatureHelp_withoutInitialization(t *testing.T) {
ls := langserver.NewLangServerMock(t, NewMockSession(nil))
stop := ls.Start(t)
defer stop()

ls.CallAndExpectError(t, &langserver.CallRequest{
Method: "textDocument/signatureHelp",
ReqParams: fmt.Sprintf(`{
"textDocument": {
"uri": "%s/main.tf"
},
"position": {
"character": 0,
"line": 1
},
"context": {
"isRetrigger": false,
"triggerCharacter": "(",
"triggerKind": 2
}
}`, TempDir(t).URI)}, session.SessionNotInitialized.Err())
}

func TestSignatureHelp_withValidData(t *testing.T) {
tmpDir := TempDir(t)
InitPluginCache(t, tmpDir.Path())

var testSchema tfjson.ProviderSchemas
err := json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema)
if err != nil {
t.Fatal(err)
}

ss, err := state.NewStateStore()
if err != nil {
t.Fatal(err)
}
wc := walker.NewWalkerCollector()

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
TerraformCalls: &exec.TerraformMockCalls{
PerWorkDir: map[string][]*mock.Call{
tmpDir.Path(): {
{
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,
},
},
},
},
},
StateStore: ss,
WalkerCollector: wc,
}))
stop := ls.Start(t)
defer stop()

ls.Call(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
}`, tmpDir.URI)})
waitForWalkerPath(t, ss, wc, tmpDir)
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": "variable \"name\" {\n default = file(\"~/foo\")\n}",
"uri": "%s/main.tf"
}
}`, TempDir(t).URI)})
waitForAllJobs(t, ss)

ls.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "textDocument/signatureHelp",
ReqParams: fmt.Sprintf(`{
"textDocument": {
"uri": "%s/main.tf"
},
"position": {
"character": 16,
"line": 1
},
"context": {
"isRetrigger": false,
"triggerCharacter": "(",
"triggerKind": 2
}
}`, TempDir(t).URI)}, `{
"jsonrpc": "2.0",
"id": 3,
"result": {
"signatures": [{
"label": "file(path string) string",
"documentation": "file reads the contents of a file at the given path and returns them as a string.",
"parameters": [{"label": "path"}]
}]
}
}`)
}
35 changes: 35 additions & 0 deletions internal/lsp/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package lsp

import (
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/terraform-ls/internal/mdplain"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
)

func ToSignatureHelp(signature *lang.FunctionSignature) *lsp.SignatureHelp {
if signature == nil {
return nil
}

parameters := make([]lsp.ParameterInformation, 0, len(signature.Parameters))
for _, p := range signature.Parameters {
parameters = append(parameters, lsp.ParameterInformation{
Label: p.Name,
// TODO: Support markdown per https://github.com/hashicorp/terraform-ls/issues/1212
Documentation: mdplain.Clean(p.Description.Value),
})
}

return &lsp.SignatureHelp{
Signatures: []lsp.SignatureInformation{
{
Label: signature.Name,
// TODO: Support markdown per https://github.com/hashicorp/terraform-ls/issues/1212
Documentation: mdplain.Clean(signature.Description.Value),
Parameters: parameters,
},
},
ActiveParameter: signature.ActiveParameter,
ActiveSignature: 0,
}
}
Loading

0 comments on commit 68b265f

Please sign in to comment.