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

Symbols Support #265

Merged
merged 5 commits into from
Sep 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions internal/hcl/hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ func (pb *parsedBlock) Tokens() hclsyntax.Tokens {
return pb.tokens
}

func (pb *parsedBlock) Range() hcl.Range {
return hcl.Range{
Filename: pb.tokens[0].Range.Filename,
Start: pb.tokens[0].Range.Start,
End: pb.tokens[len(pb.tokens)-1].Range.End,
}
}

func (pb *parsedBlock) TokenAtPosition(pos hcl.Pos) (hclsyntax.Token, error) {
for _, t := range pb.tokens {
if rangeContainsOffset(t.Range, pos.Byte) {
Expand Down Expand Up @@ -120,6 +128,27 @@ func (f *file) BlockAtPosition(pos hcl.Pos) (TokenizedBlock, error) {
return nil, &NoBlockFoundErr{pos}
}

func (f *file) Blocks() ([]TokenizedBlock, error) {
var blocks []TokenizedBlock

pf, err := f.parse()
if err != nil {
return blocks, err
}

body, ok := pf.Body.(*hclsyntax.Body)
if !ok {
return blocks, fmt.Errorf("unexpected body type (%T)", body)
}

for _, block := range body.Blocks {
dt := definitionTokens(tokensInRange(pf.Tokens, block.Range()))
blocks = append(blocks, &parsedBlock{dt})
}

return blocks, nil
}

func (f *file) TokenAtPosition(pos hcl.Pos) (hclsyntax.Token, error) {
pf, _ := f.parse()

Expand Down
2 changes: 2 additions & 0 deletions internal/hcl/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ type TokenizedFile interface {
BlockAtPosition(hcl.Pos) (TokenizedBlock, error)
TokenAtPosition(hcl.Pos) (hclsyntax.Token, error)
PosInBlock(hcl.Pos) bool
Blocks() ([]TokenizedBlock, error)
}

type TokenizedBlock interface {
TokenAtPosition(hcl.Pos) (hclsyntax.Token, error)
Tokens() hclsyntax.Tokens
Range() hcl.Range
}
2 changes: 1 addition & 1 deletion internal/lsp/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ func textEdit(te lang.TextEdit, pos hcl.Pos) *lsp.TextEdit {

return &lsp.TextEdit{
NewText: te.NewText(),
Range: hclRangeToLSP(*rng),
Range: HCLRangeToLSP(*rng),
}
}
4 changes: 2 additions & 2 deletions internal/lsp/file_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ func TextEdits(changes filesystem.DocumentChanges) []lsp.TextEdit {

for i, change := range changes {
edits[i] = lsp.TextEdit{
Range: hclRangeToLSP(change.Range()),
Range: HCLRangeToLSP(change.Range()),
NewText: change.Text(),
}
}

return edits
}

func hclRangeToLSP(hclRng hcl.Range) lsp.Range {
func HCLRangeToLSP(hclRng hcl.Range) lsp.Range {
return lsp.Range{
Start: lsp.Position{
Character: hclRng.Start.Column - 1,
Expand Down
56 changes: 30 additions & 26 deletions internal/terraform/lang/datasource_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ type datasourceBlock struct {
providerRefs addrs.ProviderReferences
}

func (r *datasourceBlock) Type() string {
return r.Labels()[0].Value
func (d *datasourceBlock) Type() string {
return d.Labels()[0].Value
Copy link
Member

Choose a reason for hiding this comment

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

Thank you for fixing this! 👍

}

func (r *datasourceBlock) Name() string {
firstLabel := r.Labels()[0].Value
secondLabel := r.Labels()[1].Value
func (d *datasourceBlock) Name() string {
firstLabel := d.Labels()[0].Value
secondLabel := d.Labels()[1].Value

if firstLabel == "" && secondLabel == "" {
return "<unknown>"
Expand All @@ -82,39 +82,43 @@ func (r *datasourceBlock) Name() string {
return fmt.Sprintf("%s.%s", firstLabel, secondLabel)
}

func (r *datasourceBlock) Labels() []*ParsedLabel {
if r.labels != nil {
return r.labels
func (d *datasourceBlock) Labels() []*ParsedLabel {
if d.labels != nil {
return d.labels
}

labels := ParseLabels(r.tBlock, r.labelSchema)
r.labels = labels
labels := ParseLabels(d.tBlock, d.labelSchema)
d.labels = labels

return r.labels
return d.labels
}

func (r *datasourceBlock) BlockType() string {
func (d *datasourceBlock) BlockType() string {
return "data"
}

func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) {
if r.sr == nil {
return nil, &noSchemaReaderErr{r.BlockType()}
func (d *datasourceBlock) Range() hcl.Range {
return d.tBlock.Range()
}

func (d *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) {
if d.sr == nil {
return nil, &noSchemaReaderErr{d.BlockType()}
}

// We ignore diags as we assume incomplete (invalid) configuration
hclBlock, _ := hclsyntax.ParseBlockFromTokens(r.tBlock.Tokens())
hclBlock, _ := hclsyntax.ParseBlockFromTokens(d.tBlock.Tokens())

if PosInLabels(hclBlock, pos) {
dataSources, err := r.sr.DataSources()
dataSources, err := d.sr.DataSources()
if err != nil {
return nil, err
}

cl := &completableLabels{
logger: r.logger,
parsedLabels: r.Labels(),
tBlock: r.tBlock,
logger: d.logger,
parsedLabels: d.Labels(),
tBlock: d.tBlock,
labels: labelCandidates{
"type": dataSourceCandidates(dataSources),
},
Expand All @@ -123,25 +127,25 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand
return cl.completionCandidatesAtPos(pos)
}

lRef, err := parseProviderRef(hclBlock.Body.Attributes, r.Type())
lRef, err := parseProviderRef(hclBlock.Body.Attributes, d.Type())
if err != nil {
return nil, err
}

pAddr, err := lookupProviderAddress(r.providerRefs, lRef)
pAddr, err := lookupProviderAddress(d.providerRefs, lRef)
if err != nil {
return nil, err
}

rSchema, err := r.sr.DataSourceSchema(pAddr, r.Type())
rSchema, err := d.sr.DataSourceSchema(pAddr, d.Type())
if err != nil {
return nil, err
}
cb := &completableBlock{
logger: r.logger,
parsedLabels: r.Labels(),
logger: d.logger,
parsedLabels: d.Labels(),
schema: rSchema.Block,
tBlock: r.tBlock,
tBlock: d.tBlock,
}
return cb.completionCandidatesAtPos(pos)
}
Expand Down
20 changes: 20 additions & 0 deletions internal/terraform/lang/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ func (p *parser) BlockTypeCandidates(file ihcl.TokenizedFile, pos hcl.Pos) Compl
return list
}

func (p *parser) Blocks(file ihcl.TokenizedFile) ([]ConfigBlock, error) {
blocks, err := file.Blocks()
if err != nil {
return nil, err
}

var cfgBlocks []ConfigBlock

for _, block := range blocks {
cfgBlock, err := p.ParseBlockFromTokens(block)
if err != nil {
p.logger.Printf("parsing config block failed: %s", err)
continue
Copy link
Member

Choose a reason for hiding this comment

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

Just curious what's the motivation here behind logging and not erroring out here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not want a file with blocks we don't support yet var, local, etc to return an error to the client, I rather just log and move on

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha, that makes sense - can you just document this reasoning in the form of an in-line comment above the log line?

}
cfgBlocks = append(cfgBlocks, cfgBlock)
}

return cfgBlocks, nil
}

type completableBlockType struct {
TypeName string
LabelSchema LabelSchema
Expand Down
4 changes: 4 additions & 0 deletions internal/terraform/lang/provider_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ func (p *providerBlock) BlockType() string {
return "provider"
}

func (p *providerBlock) Range() hcl.Range {
return p.tBlock.Range()
}

func (p *providerBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) {
if p.sr == nil {
return nil, &noSchemaReaderErr{p.BlockType()}
Expand Down
4 changes: 4 additions & 0 deletions internal/terraform/lang/resource_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func (r *resourceBlock) BlockType() string {
return "resource"
}

func (r *resourceBlock) Range() hcl.Range {
return r.tBlock.Range()
}

func (r *resourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) {
if r.sr == nil {
return nil, &noSchemaReaderErr{r.BlockType()}
Expand Down
2 changes: 2 additions & 0 deletions internal/terraform/lang/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Parser interface {
SetProviderReferences(addrs.ProviderReferences)
BlockTypeCandidates(ihcl.TokenizedFile, hcl.Pos) CompletionCandidates
CompletionCandidatesAtPos(ihcl.TokenizedFile, hcl.Pos) (CompletionCandidates, error)
Blocks(ihcl.TokenizedFile) ([]ConfigBlock, error)
}

// ConfigBlock implements an abstraction above HCL block
Expand All @@ -28,6 +29,7 @@ type ConfigBlock interface {
Name() string
BlockType() string
Labels() []*ParsedLabel
Range() hcl.Range
}

// Block represents a decoded HCL block (by a Parser)
Expand Down
2 changes: 2 additions & 0 deletions langserver/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestInitalizeAndShutdown(t *testing.T) {
"change": 2
},
"completionProvider": {},
"documentSymbolProvider":true,
"documentFormattingProvider":true
}
}
Expand Down Expand Up @@ -75,6 +76,7 @@ func TestEOF(t *testing.T) {
"change": 2
},
"completionProvider": {},
"documentSymbolProvider":true,
"documentFormattingProvider":true
}
}
Expand Down
1 change: 1 addition & 0 deletions langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam
ResolveProvider: false,
},
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
},
}

Expand Down
11 changes: 11 additions & 0 deletions langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {
ctx = lsctx.WithDocumentStorage(ctx, fs)
return handle(ctx, req, TextDocumentDidClose)
},
"textDocument/documentSymbol": 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.WithParserFinder(ctx, svc.modMgr)

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

import (
"context"
"fmt"
"time"

lsctx "github.com/hashicorp/terraform-ls/internal/context"
ihcl "github.com/hashicorp/terraform-ls/internal/hcl"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
"github.com/hashicorp/terraform-ls/internal/terraform/lang"
"github.com/sourcegraph/go-lsp"
)

func (h *logHandler) TextDocumentSymbol(ctx context.Context, params lsp.DocumentSymbolParams) ([]lsp.SymbolInformation, error) {
var symbols []lsp.SymbolInformation

fs, err := lsctx.DocumentStorage(ctx)
if err != nil {
return symbols, err
}

pf, err := lsctx.ParserFinder(ctx)
if err != nil {
return symbols, err
}

file, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI))
if err != nil {
return symbols, err
}

text, err := file.Text()
if err != nil {
return symbols, err
}

hclFile := ihcl.NewFile(file, text)

// TODO: block until it's available <-pf.ParserLoadingDone()
// requires https://github.com/hashicorp/terraform-ls/issues/8
// TODO: replace crude wait/timeout loop
Copy link
Member

Choose a reason for hiding this comment

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

🤔 I first thought this is "dirty", but then I realized that it may represent just the right balance between waiting for too long or erroring prematurely. So with that in mind I don't think it's crude, I actually think that functionally this is a sensible and pragmatic solution we could replicate in other handlers too eventually.

The only things I'd be careful about is

(a) the timeout - 3 seconds feels quite long to me, I'd reduce it down to 2 seconds max

(b) how this positions us towards/against complying with LSP. As far as I understand this waiting mechanism greatly increases the chance of us providing outdated data, and so that may put more pressure on resolving #8 otherwise we're diverging from the LSP, which we shouldn't be. This is why I leaned more towards erroring than waiting in the completion handler.

Copy link
Contributor Author

@appilon appilon Sep 1, 2020

Choose a reason for hiding this comment

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

I can adjust this to max 2s. The reason this is needed is because the documentSymbol request happens right away once a file is opened/changed, in my local testing the parser was never ready right away and it errored everytime. I have never hit a max timeout either fwiw, I suspect the parser just needed a few hundred milliseconds.

Copy link
Member

Choose a reason for hiding this comment

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

The reason this is needed is because the documentSymbol request happens right away once a file is opened

I see, I wasn't aware of that. In that case I assume it's actually desirable to even wait longer before responding? i.e. we can and should tolerate longer timeout.

I was just applying caution because I saw some clients setting hard timeout for responses for some methods - e.g. formatting in particular is expected to be done within a second or two in some clients's implementations, otherwise the request is just cancelled. I assume the caution is not applicable to all methods then.

So feel free to keep it at 3 seconds.

Copy link
Member

Choose a reason for hiding this comment

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

Regardless though we really need to resolve #8

var isParserLoaded bool
var elapsed time.Duration
sleepFor := 10 * time.Millisecond
maxWait := 3 * time.Second
for !isParserLoaded {
time.Sleep(sleepFor)
elapsed += sleepFor
if elapsed >= maxWait {
return symbols, fmt.Errorf("parser is not available yet for %s", file.Dir())
}
var err error
isParserLoaded, err = pf.IsParserLoaded(file.Dir())
if err != nil {
return symbols, err
}
}

p, err := pf.ParserForDir(file.Dir())
if err != nil {
return symbols, fmt.Errorf("finding compatible parser failed: %w", err)
}

blocks, err := p.Blocks(hclFile)
if err != nil {
return symbols, err
}

for _, block := range blocks {
symbols = append(symbols, lsp.SymbolInformation{
Name: symbolName(block),
Kind: lsp.SKStruct, // We don't have a great SymbolKind match for blocks
Location: lsp.Location{
Range: ilsp.HCLRangeToLSP(block.Range()),
URI: params.TextDocument.URI,
},
})
}

return symbols, nil

}

func symbolName(b lang.ConfigBlock) string {
name := b.BlockType()
for _, l := range b.Labels() {
name += "." + l.Value
}
return name
}