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
+}