Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hooks for dynamic completion #1017

Merged
merged 10 commits into from
Aug 2, 2022
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hc-install v0.4.0
github.com/hashicorp/hcl-lang v0.0.0-20220719100104-053ec0de36b0
github.com/hashicorp/hcl-lang v0.0.0-20220801150536-118ac453e267
github.com/hashicorp/hcl/v2 v2.13.0
github.com/hashicorp/terraform-exec v0.17.2
github.com/hashicorp/terraform-json v0.14.0
Expand Down
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,11 @@ github.com/hashicorp/hc-install v0.4.0 h1:cZkRFr1WVa0Ty6x5fTvL1TuO1flul231rWkGH9
github.com/hashicorp/hc-install v0.4.0/go.mod h1:5d155H8EC5ewegao9A4PUTMNPZaq+TbOzkJJZ4vrXeI=
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-20220719100104-053ec0de36b0 h1:I/FnNaXCYPU5Vt09KII1iYqZiSvNnTYlRJbmNFXB3R8=
github.com/hashicorp/hcl-lang v0.0.0-20220719100104-053ec0de36b0/go.mod h1:s6PiZCSfwaQeFs28zZMESYpK+ZivG2QgtwpJG18ZLzA=
github.com/hashicorp/hcl-lang v0.0.0-20220727102408-593035f5162e h1:2domzzt/gHTQJyhly+3aveYwYCihZr2/NYyjTTJ9mho=
github.com/hashicorp/hcl-lang v0.0.0-20220727102408-593035f5162e/go.mod h1:s6PiZCSfwaQeFs28zZMESYpK+ZivG2QgtwpJG18ZLzA=
github.com/hashicorp/hcl-lang v0.0.0-20220801150536-118ac453e267 h1:8Vn+GbhtkSH0rMv4SYF/WJO1j5z1z0vvpvwHPZB3Fxs=
github.com/hashicorp/hcl-lang v0.0.0-20220801150536-118ac453e267/go.mod h1:zGFgYSTTY5D5F+diri+HFKo2JGLqJb5icJl4CSEGPHk=
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
Expand Down
17 changes: 5 additions & 12 deletions internal/decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ import (
tfschema "github.com/hashicorp/terraform-schema/schema"
)

func NewDecoder(ctx context.Context, pathReader decoder.PathReader) *decoder.Decoder {
d := decoder.NewDecoder(pathReader)
d.SetContext(decoderContext(ctx))
return d
}

func modulePathContext(mod *state.Module, schemaReader state.SchemaReader, modReader ModuleReader) (*decoder.PathContext, error) {
schema, err := schemaForModule(mod, schemaReader, modReader)
if err != nil {
Expand Down Expand Up @@ -79,12 +73,11 @@ func varsPathContext(mod *state.Module) (*decoder.PathContext, error) {
return pathCtx, nil
}

func decoderContext(ctx context.Context) decoder.DecoderContext {
dCtx := decoder.DecoderContext{
UtmSource: utm.UtmSource,
UtmMedium: utm.UtmMedium(ctx),
UseUtmContent: true,
}
func DecoderContext(ctx context.Context) decoder.DecoderContext {
dCtx := decoder.NewDecoderContext()
dCtx.UtmSource = utm.UtmSource
dCtx.UtmMedium = utm.UtmMedium(ctx)
dCtx.UseUtmContent = true

cc, err := ilsp.ClientCapabilities(ctx)
if err == nil {
Expand Down
10 changes: 10 additions & 0 deletions internal/hooks/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Package hooks enables the implementation of hooks for dynamic
// autocompletion. Hooks should be added to this package and
// registered via AppendCompletionHooks in completion_hooks.go.
package hooks

import "github.com/hashicorp/terraform-ls/internal/state"

type Hooks struct {
ModStore *state.ModuleStore
}
18 changes: 18 additions & 0 deletions internal/hooks/module_source_local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hooks

import (
"context"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/zclconf/go-cty/cty"
)

func (h *Hooks) LocalModuleSources(ctx context.Context, value cty.Value) ([]decoder.Candidate, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this would be eventually part of a separate PR? 👀

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this as a placeholder to showcase the registration of hooks in AppendCompletionHooks. Without any hooks we wouldn't be able to create hooks.Hooks{}.

But I can remove it if you want? And defer everything to the next PR

candidates := make([]decoder.Candidate, 0)

// Obtain indexed modules via h.modStore.List()
// TODO filter modules inside .terraform
// TODO build candidates

return candidates, nil
}
2 changes: 1 addition & 1 deletion internal/langserver/handlers/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (svc *service) TextDocumentComplete(ctx context.Context, params lsp.Complet
}

svc.logger.Printf("Looking for candidates at %q -> %#v", doc.Filename, pos)
candidates, err := d.CandidatesAtPos(doc.Filename, pos)
candidates, err := d.CandidatesAtPos(ctx, doc.Filename, pos)
svc.logger.Printf("received candidates: %#v", candidates)
return ilsp.ToCompletionList(candidates, cc.TextDocument), err
}
15 changes: 15 additions & 0 deletions internal/langserver/handlers/completion_hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package handlers

import (
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/terraform-ls/internal/hooks"
)

func (s *service) AppendCompletionHooks(ctx decoder.DecoderContext) {
h := hooks.Hooks{
ModStore: s.modStore,
}

ctx.CompletionHooks["CompleteLocalModuleSources"] = h.LocalModuleSources

}
48 changes: 48 additions & 0 deletions internal/langserver/handlers/completion_resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package handlers

import (
"context"

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

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

if params.ResolveHook == nil {
return params, nil
}

unresolvedCandidate := decoder.UnresolvedCandidate{
ResolveHook: params.ResolveHook,
}

resolvedCandidate, err := svc.decoder.ResolveCandidate(ctx, unresolvedCandidate)
if err != nil || resolvedCandidate == nil {
return params, err
}

if resolvedCandidate.Description.Value != "" {
doc := resolvedCandidate.Description.Value

// TODO: Revisit when MarkupContent is allowed as Documentation
// https://github.com/golang/tools/blob/4783bc9b/internal/lsp/protocol/tsprotocol.go#L753
doc = mdplain.Clean(doc)
params.Documentation = doc
}
if resolvedCandidate.Detail != "" {
params.Detail = resolvedCandidate.Detail
}
if len(resolvedCandidate.AdditionalTextEdits) > 0 {
snippetSupport := cc.TextDocument.Completion.CompletionItem.SnippetSupport
params.AdditionalTextEdits = ilsp.TextEdits(resolvedCandidate.AdditionalTextEdits, snippetSupport)
}

return params, nil
}
72 changes: 72 additions & 0 deletions internal/langserver/handlers/completion_resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package handlers

import (
"encoding/json"
"fmt"
"testing"

tfjson "github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-ls/internal/langserver"
"github.com/hashicorp/terraform-ls/internal/langserver/session"
)

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

ls.CallAndExpectError(t, &langserver.CallRequest{
Method: "completionItem/resolve",
ReqParams: "{}"}, session.SessionNotInitialized.Err())
}

func TestCompletionResolve_withoutHook(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)
}

ls := langserver.NewLangServerMock(t, NewMockSession(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.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "completionItem/resolve",
ReqParams: fmt.Sprintf(`{
"label": "\"test\"",
"kind": 1,
"data": {
"resolve_hook": "test",
"path": "%s/main.tf"
}
}`, TempDir(t).URI),
}, fmt.Sprintf(`{
"jsonrpc": "2.0",
"id": 2,
"result": {
"label": "\"test\"",
"labelDetails": {},
"kind": 1,
"data": {
"resolve_hook": "test",
"path": "%s/main.tf"
}
}
}`, TempDir(t).URI))
}
1 change: 1 addition & 0 deletions internal/langserver/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func initializeResponse(t *testing.T, commandPrefix string) string {
},
"completionProvider": {
"triggerCharacters": [".", "["],
"resolveProvider": true,
"completionItem":{}
},
"hoverProvider": true,
Expand Down
2 changes: 1 addition & 1 deletion internal/langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func initializeResult(ctx context.Context) lsp.InitializeResult {
Change: lsp.Incremental,
},
CompletionProvider: lsp.CompletionOptions{
ResolveProvider: false,
ResolveProvider: true,
TriggerCharacters: []string{".", "["},
},
CodeActionProvider: lsp.CodeActionOptions{
Expand Down
16 changes: 15 additions & 1 deletion internal/langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {

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

ctx = ilsp.WithClientCapabilities(ctx, cc)
ctx = lsctx.WithExperimentalFeatures(ctx, &expFeatures)

return handle(ctx, req, svc.CompletionItemResolve)
},
"textDocument/hover": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
Expand Down Expand Up @@ -484,10 +495,13 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s
svc.stateStore.JobStore, svc.tfExecFactory, svc.registryClient)
svc.indexer.SetLogger(svc.logger)

svc.decoder = idecoder.NewDecoder(ctx, &idecoder.PathReader{
svc.decoder = decoder.NewDecoder(&idecoder.PathReader{
ModuleReader: svc.modStore,
SchemaReader: svc.schemaStore,
})
decoderContext := idecoder.DecoderContext(ctx)
svc.AppendCompletionHooks(decoderContext)
svc.decoder.SetContext(decoderContext)

err = schemas.PreloadSchemasToStore(svc.stateStore.ProviderSchemas)
if err != nil {
Expand Down
6 changes: 5 additions & 1 deletion internal/lsp/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ func toCompletionItem(candidate lang.Candidate, caps lsp.CompletionClientCapabil
Documentation: doc,
TextEdit: textEdit(candidate.TextEdit, snippetSupport),
Command: cmd,
AdditionalTextEdits: textEdits(candidate.AdditionalTextEdits, snippetSupport),
AdditionalTextEdits: TextEdits(candidate.AdditionalTextEdits, snippetSupport),
}

if candidate.ResolveHook != nil {
item.Data = candidate.ResolveHook
}

if caps.CompletionItem.DeprecatedSupport {
Expand Down
2 changes: 1 addition & 1 deletion internal/lsp/text_edits.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TextEditsFromDocumentChanges(changes document.Changes) []lsp.TextEdit {
return edits
}

func textEdits(tes []lang.TextEdit, snippetSupport bool) []lsp.TextEdit {
func TextEdits(tes []lang.TextEdit, snippetSupport bool) []lsp.TextEdit {
edits := make([]lsp.TextEdit, len(tes))

for i, te := range tes {
Expand Down
9 changes: 9 additions & 0 deletions internal/protocol/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package protocol

import "github.com/hashicorp/hcl-lang/lang"

type CompletionItemWithResolveHook struct {
CompletionItem

ResolveHook *lang.ResolveHook `json:"data,omitempty"`
}
18 changes: 12 additions & 6 deletions internal/terraform/module/module_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/terraform-ls/internal/decoder"
idecoder "github.com/hashicorp/terraform-ls/internal/decoder"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
"github.com/hashicorp/terraform-ls/internal/registry"
"github.com/hashicorp/terraform-ls/internal/state"
Expand Down Expand Up @@ -276,17 +277,20 @@ func DecodeReferenceTargets(ctx context.Context, modStore *state.ModuleStore, sc
return err
}

d, err := decoder.NewDecoder(ctx, &decoder.PathReader{
d := decoder.NewDecoder(&idecoder.PathReader{
ModuleReader: modStore,
SchemaReader: schemaReader,
}).Path(lang.Path{
})
d.SetContext(idecoder.DecoderContext(ctx))

pd, err := d.Path(lang.Path{
Path: modPath,
LanguageID: ilsp.Terraform.String(),
})
if err != nil {
return err
}
targets, rErr := d.CollectReferenceTargets()
targets, rErr := pd.CollectReferenceTargets()

targets = append(targets, builtinReferences(modPath)...)

Expand All @@ -304,10 +308,11 @@ func DecodeReferenceOrigins(ctx context.Context, modStore *state.ModuleStore, sc
return err
}

d := decoder.NewDecoder(ctx, &decoder.PathReader{
d := decoder.NewDecoder(&idecoder.PathReader{
ModuleReader: modStore,
SchemaReader: schemaReader,
})
d.SetContext(idecoder.DecoderContext(ctx))

moduleDecoder, err := d.Path(lang.Path{
Path: modPath,
Expand All @@ -333,10 +338,11 @@ func DecodeVarsReferences(ctx context.Context, modStore *state.ModuleStore, sche
return err
}

d := decoder.NewDecoder(ctx, &decoder.PathReader{
d := decoder.NewDecoder(&idecoder.PathReader{
ModuleReader: modStore,
SchemaReader: schemaReader,
})
d.SetContext(idecoder.DecoderContext(ctx))

varsDecoder, err := d.Path(lang.Path{
Path: modPath,
Expand Down