Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
df77cb5
feat(graph): TF-32545 TF-32558: Added: custom lsp command handler for…
anubhav-goel Dec 9, 2025
fdc0505
feat(graph): TF-32545 TF-32559: Modified: get language id and path sp…
anubhav-goel Dec 9, 2025
974a3c5
feat(graph): TF-32545 TF-32560: Added: construct nodes
anubhav-goel Dec 9, 2025
f13710c
feat(graph): TF-32545 TF-32562: Added: get reftargets for correspondi…
anubhav-goel Dec 9, 2025
1a61925
feat(graph): TF-32545 TF-32563 TF-32564: Added: construct edges
anubhav-goel Dec 9, 2025
2fb6c56
feat(graph): TF-32545 TF-32566: Added: Deduplicate edges
anubhav-goel Dec 9, 2025
d6782de
feat(graph): TF-32545 TF-32565: Added: Filter self edges
anubhav-goel Dec 9, 2025
91e082c
feat(graph): TF-32545: Added: changelog
anubhav-goel Dec 9, 2025
2aa4b00
feat(graph): TF-32545: Modified: bumped hcl-lang to wip version
anubhav-goel Dec 9, 2025
07eb04f
feat(graph): TF-32545: Refactor: reusing from edge
anubhav-goel Dec 9, 2025
2d798f0
feat(graph): TF-32545: Modified: bumped hcl-lang to wip version
anubhav-goel Dec 10, 2025
ebe4515
TF-32545: Bumped: hcl-lang version
anubhav-goel Dec 12, 2025
de0ff5f
feat(graph): TF-32545 TF-32726: Modified: Using RootBlockRange from R…
anubhav-goel Dec 12, 2025
01b289d
feat(graph): TF-32545 TF-32726: Modified: response structure
anubhav-goel Dec 17, 2025
cfa8ce9
feat(graph): TF-32545 TF-32568: Added: test cases display graph
anubhav-goel Dec 19, 2025
4745ebb
feat(graph): TF-32545: Modified: Fix cross-platform test failure in d…
anubhav-goel Dec 22, 2025
2bcbd6b
feat(graph): TF-32545: Modified: test case dir path
anubhav-goel Dec 22, 2025
544b124
feat(graph): TF-32545: Modified: added integration test display graph
anubhav-goel Dec 22, 2025
e6b02fa
feat(graph): TF-32545: Modified: go fmt
anubhav-goel Dec 22, 2025
40e97db
feat(graph): TF-32545: Modified: uri trial error test case fix
anubhav-goel Dec 22, 2025
b19c499
feat(graph): TF-32545: Modified: removed test
anubhav-goel Dec 22, 2025
73d9fbf
feat(graph): TF-32545 TF-32563: Modified: match ref origin and target…
anubhav-goel Dec 23, 2025
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
6 changes: 6 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20251209-175542.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: ENHANCEMENTS
body: Terraform Graph Visualization in VS Code Extension
time: 2025-12-09T17:55:42.954862+05:30
custom:
Issue: "2056"
Repository: terraform-ls
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.7.0
github.com/hashicorp/hc-install v0.9.2
github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead
github.com/hashicorp/hcl-lang v0.0.0-20251223113758-c9e9d0ec101a
github.com/hashicorp/hcl/v2 v2.24.0
github.com/hashicorp/terraform-exec v0.24.0
github.com/hashicorp/terraform-json v0.27.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24=
github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I=
github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead h1:Nthdz3YLW98zGxDKgkBj6lTXWKhp1tWDg8ecyLkYe94=
github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead/go.mod h1:lUY+oHKrbuViZqM+HHqmKE/18umUBPLAIoVT0WSZjKE=
github.com/hashicorp/hcl-lang v0.0.0-20251223113758-c9e9d0ec101a h1:k8sRuiYS612tDfIGZiAsAXYcjRsfnkaW2u47IoL/6JU=
github.com/hashicorp/hcl-lang v0.0.0-20251223113758-c9e9d0ec101a/go.mod h1:2SQEYnpcouuNOR8bjKyWuh82bawbZgoesfHZgqVSTjg=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE=
Expand Down
201 changes: 201 additions & 0 deletions internal/langserver/handlers/command/display_graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package command

import (
"context"
"fmt"
"path/filepath"
"sort"

"github.com/creachadair/jrpc2"
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform-ls/internal/langserver/cmd"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
"github.com/hashicorp/terraform-ls/internal/uri"
)

const displayGraphVersion = 0

type displayGraphResponse struct {
FormatVersion int `json:"v"`
Nodes []node `json:"nodes"`
Edges []edge `json:"edges"`
}

type node struct {
ID int `json:"id"`
lsp.Location
Type string `json:"type"`
Labels []string `json:"labels"`
}

type edge struct {
From int `json:"from"`
To int `json:"to"`
}

type blockInfo struct {
filename string
block *hclsyntax.Block
}

func (h *CmdHandler) DisplayGraphHandler(ctx context.Context, args cmd.CommandArgs) (interface{}, error) {
response := newDisplayGraphResponse()

docUri, ok := args.GetString("uri")
if !ok || docUri == "" {
return response, fmt.Errorf("%w: expected uri argument to be set", jrpc2.InvalidParams.Err())
}

dh := ilsp.HandleFromDocumentURI(lsp.DocumentURI(uri.FromPath(docUri)))
doc, err := h.StateStore.DocumentStore.GetDocument(dh)
if err != nil {
return response, err
}

// Wait for incomplete jobs to ensure references are computed
jobIds, err := h.StateStore.JobStore.ListIncompleteJobsForDir(dh.Dir)
if err != nil {
return response, err
}
h.StateStore.JobStore.WaitForJobs(ctx, jobIds...)

path := lang.Path{
Path: dh.Dir.Path(),
LanguageID: doc.LanguageID,
}

pathDecoder, err := h.Decoder.Path(path)
if err != nil {
return response, err
}

nodes, nodeMap, err := getNodes(pathDecoder, path)
if err != nil {
return response, err
}

edges, err := getEdges(pathDecoder, path, h.Decoder, nodeMap)
if err != nil {
return response, err
}

response.Nodes = nodes
response.Edges = edges
return response, nil
}

func newDisplayGraphResponse() displayGraphResponse {
return displayGraphResponse{
FormatVersion: displayGraphVersion,
Nodes: make([]node, 0),
Edges: make([]edge, 0),
}
}

func getNodes(pathDecoder *decoder.PathDecoder, path lang.Path) ([]node, map[string]int, error) {
var blocks []blockInfo
for _, file := range pathDecoder.Files() {
body := file.Body.(*hclsyntax.Body)
for _, block := range body.Blocks {
blocks = append(blocks, blockInfo{
filename: block.DefRange().Filename,
block: block,
})
}
}

sort.Slice(blocks, func(i, j int) bool {
if blocks[i].filename != blocks[j].filename {
return blocks[i].filename < blocks[j].filename
}
return blocks[i].block.DefRange().Start.Line < blocks[j].block.DefRange().Start.Line
})

nodes := make([]node, 0)
nodeMap := make(map[string]int)
idCounter := 0
for _, bi := range blocks {
loc := pathRangetoLocation(path, bi.block.DefRange())
key := locationKey(loc)
nodeMap[key] = idCounter
nodes = append(nodes,
node{
ID: idCounter,
Location: loc,
Type: bi.block.Type,
Labels: bi.block.Labels,
})
idCounter++
}
return nodes, nodeMap, nil
}

func getEdges(pathDecoder *decoder.PathDecoder, path lang.Path, decoder *decoder.Decoder, nodeMap map[string]int) ([]edge, error) {
edges := make([]edge, 0)
refTargets := pathDecoder.RefTargets()
seen := make(map[string]bool)

for _, refTarget := range refTargets {
if refTarget.RootBlockRange != nil {
fromLoc := pathRangetoLocation(path, *refTarget.RootBlockRange)
fromKey := locationKey(fromLoc)
fromID, fromExists := nodeMap[fromKey]
if !fromExists {
continue
}
origins := decoder.ReferenceOriginsByTargetInPath(context.Background(), refTarget, path, path)
for _, refOrigin := range origins {
toLoc := pathRangetoLocation(path, refOrigin.RootBlockRange)
toKey := locationKey(toLoc)
toID, toExists := nodeMap[toKey]
if !toExists {
continue
}
edge := edge{
From: fromID,
To: toID,
}
edgeKey := edgeKey(edge)
if isASelfEdge(edge) || isSeenEdge(&seen, edgeKey) {
continue
}

edges = append(edges, edge)
seen[edgeKey] = true
}

}
}
return edges, nil
}

func edgeKey(e edge) string {
return fmt.Sprintf("%d->%d", e.From, e.To)
}

func locationKey(loc lsp.Location) string {
return fmt.Sprintf("%s#%d:%d#%d:%d", loc.URI, loc.Range.Start.Line, loc.Range.Start.Character, loc.Range.End.Line, loc.Range.End.Character)
}

func isSeenEdge(seen *map[string]bool, edgeKey string) bool {
_, ok := (*seen)[edgeKey]
return ok
}

func isASelfEdge(edge edge) bool {
return edge.From == edge.To
}

func pathRangetoLocation(path lang.Path, rng hcl.Range) lsp.Location {
return lsp.Location{
URI: lsp.DocumentURI(uri.FromPath(filepath.Join(path.Path, rng.Filename))),
Range: ilsp.HCLRangeToLSP(rng),
}
}
Loading
Loading