diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 2efc50f7..5169ed20 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -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) { @@ -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() diff --git a/internal/hcl/types.go b/internal/hcl/types.go index 22f539b5..dde107df 100644 --- a/internal/hcl/types.go +++ b/internal/hcl/types.go @@ -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 } diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 377ae8e1..3402f009 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -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), } } diff --git a/internal/lsp/file_change.go b/internal/lsp/file_change.go index fe3cd557..34e60a0a 100644 --- a/internal/lsp/file_change.go +++ b/internal/lsp/file_change.go @@ -46,7 +46,7 @@ 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(), } } @@ -54,7 +54,7 @@ func TextEdits(changes filesystem.DocumentChanges) []lsp.TextEdit { 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, diff --git a/internal/terraform/lang/datasource_block.go b/internal/terraform/lang/datasource_block.go index 30e76414..20505e16 100644 --- a/internal/terraform/lang/datasource_block.go +++ b/internal/terraform/lang/datasource_block.go @@ -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 } -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>" @@ -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), }, @@ -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) } diff --git a/internal/terraform/lang/parser.go b/internal/terraform/lang/parser.go index 89a250b2..fa3a61c0 100644 --- a/internal/terraform/lang/parser.go +++ b/internal/terraform/lang/parser.go @@ -167,6 +167,28 @@ 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 { + // do not error out if parsing failed, continue to parse + // blocks that are supported + p.logger.Printf("parsing config block failed: %s", err) + continue + } + cfgBlocks = append(cfgBlocks, cfgBlock) + } + + return cfgBlocks, nil +} + type completableBlockType struct { TypeName string LabelSchema LabelSchema diff --git a/internal/terraform/lang/provider_block.go b/internal/terraform/lang/provider_block.go index 9924a22a..ee667afb 100644 --- a/internal/terraform/lang/provider_block.go +++ b/internal/terraform/lang/provider_block.go @@ -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()} diff --git a/internal/terraform/lang/resource_block.go b/internal/terraform/lang/resource_block.go index 20b57d24..0bf8dd31 100644 --- a/internal/terraform/lang/resource_block.go +++ b/internal/terraform/lang/resource_block.go @@ -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()} diff --git a/internal/terraform/lang/types.go b/internal/terraform/lang/types.go index 56899ff9..14b6b5c1 100644 --- a/internal/terraform/lang/types.go +++ b/internal/terraform/lang/types.go @@ -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 @@ -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) diff --git a/langserver/handlers/handlers_test.go b/langserver/handlers/handlers_test.go index b94cc129..3b9da661 100644 --- a/langserver/handlers/handlers_test.go +++ b/langserver/handlers/handlers_test.go @@ -37,6 +37,7 @@ func TestInitalizeAndShutdown(t *testing.T) { "change": 2 }, "completionProvider": {}, + "documentSymbolProvider":true, "documentFormattingProvider":true } } @@ -75,6 +76,7 @@ func TestEOF(t *testing.T) { "change": 2 }, "completionProvider": {}, + "documentSymbolProvider":true, "documentFormattingProvider":true } } diff --git a/langserver/handlers/initialize.go b/langserver/handlers/initialize.go index c0d7764f..84fa8923 100644 --- a/langserver/handlers/initialize.go +++ b/langserver/handlers/initialize.go @@ -27,6 +27,7 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam ResolveProvider: false, }, DocumentFormattingProvider: true, + DocumentSymbolProvider: true, }, } diff --git a/langserver/handlers/service.go b/langserver/handlers/service.go index e5d7640e..f5c7e213 100644 --- a/langserver/handlers/service.go +++ b/langserver/handlers/service.go @@ -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 { diff --git a/langserver/handlers/symbol.go b/langserver/handlers/symbol.go new file mode 100644 index 00000000..e6406619 --- /dev/null +++ b/langserver/handlers/symbol.go @@ -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 + 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 +}