Skip to content

Commit

Permalink
Add hooks for dynamic completion (#1017)
Browse files Browse the repository at this point in the history
* Pass context to CandidatesAtPos
* Add optional data to completion item response
* Refactor `DecoderContext` creation

Making `DecoderContext` public allows us to extend the context
after creation.

* Introduce hooks package and register first completion hook
* Add `completionItem/resolve` handler
* Update hcl-lang to 118ac45
* Avoid exposing CompletionItem data as null
* Add completionItem/resolve test
  • Loading branch information
dbanck authored Aug 2, 2022
1 parent 0c443d3 commit 33d377d
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 25 deletions.
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) {
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

0 comments on commit 33d377d

Please sign in to comment.