@@ -22,6 +22,10 @@ const (
2222 // (`locals { name = ... }` and `local.name` references).
2323 RenameContextLocal = "local"
2424
25+ // RenameContextDependency is the context for renaming a dependency block
26+ // label (`dependency "name" {}` and `dependency.name.outputs.X` references).
27+ RenameContextDependency = "dependency"
28+
2529 // RenameContextNull means the cursor is not on a renameable identifier.
2630 RenameContextNull = "null"
2731)
@@ -33,7 +37,8 @@ type RenameTarget struct {
3337 // Context is one of the RenameContext* constants.
3438 Context string
3539 // IdentRange is the LSP range covering only the identifier token, suitable
36- // for use as the prepare-rename range.
40+ // for use as the prepare-rename range. For block labels this excludes the
41+ // surrounding quotes.
3742 IdentRange protocol.Range
3843}
3944
@@ -83,31 +88,37 @@ func GetRenameTarget(l logger.Logger, st store.Store, position protocol.Position
8388 }
8489
8590 for cur := inode ; cur != nil ; cur = cur .Parent {
86- attr , ok := cur .Node .(* hclsyntax. Attribute )
87- if ! ok {
88- continue
89- }
90-
91- if ! ast . IsLocalAttribute ( cur ) {
91+ switch n := cur .Node .(type ) {
92+ case * hclsyntax. Block :
93+ if t , ok := blockLabelTarget ( n , position ); ok {
94+ return t
95+ }
96+ // We hit an enclosing block but the cursor isn't on its label.
9297 return null
93- }
9498
95- if ! rangeContainsPosition (attr .NameRange , position ) {
96- return null
97- }
99+ case * hclsyntax.Attribute :
100+ if ! ast .IsLocalAttribute (cur ) {
101+ return null
102+ }
103+
104+ if ! rangeContainsPosition (n .NameRange , position ) {
105+ return null
106+ }
98107
99- return RenameTarget {
100- Name : attr .Name ,
101- Context : RenameContextLocal ,
102- IdentRange : ast .FromHCLRange (attr .NameRange ),
108+ return RenameTarget {
109+ Name : n .Name ,
110+ Context : RenameContextLocal ,
111+ IdentRange : ast .FromHCLRange (n .NameRange ),
112+ }
103113 }
104114 }
105115
106116 return null
107117}
108118
109119// traversalTarget extracts a RenameTarget from a ScopeTraversalExpr if the
110- // cursor is positioned on its first two traversal steps and the root is `local`.
120+ // cursor is positioned on its first two traversal steps and the root is a
121+ // supported kind.
111122func traversalTarget (expr * hclsyntax.ScopeTraversalExpr , position protocol.Position ) RenameTarget {
112123 null := RenameTarget {Context : RenameContextNull }
113124
@@ -125,11 +136,8 @@ func traversalTarget(expr *hclsyntax.ScopeTraversalExpr, position protocol.Posit
125136 return null
126137 }
127138
128- if rootStep .Name != "local" {
129- return null
130- }
131-
132- // Restrict cursor to the first two steps.
139+ // Restrict cursor to the first two steps so that, e.g., `dependency.vpc.outputs.x`
140+ // only triggers rename when the cursor is on `dependency` or `vpc`.
133141 firstTwo := hcl.Range {
134142 Filename : rootStep .SrcRange .Filename ,
135143 Start : rootStep .SrcRange .Start ,
@@ -139,13 +147,56 @@ func traversalTarget(expr *hclsyntax.ScopeTraversalExpr, position protocol.Posit
139147 return null
140148 }
141149
150+ context , ok := contextForRoot (rootStep .Name )
151+ if ! ok {
152+ return null
153+ }
154+
142155 return RenameTarget {
143156 Name : attrStep .Name ,
144- Context : RenameContextLocal ,
157+ Context : context ,
145158 IdentRange : ast .FromHCLRange (ast .TraverseAttrIdentRange (attrStep )),
146159 }
147160}
148161
162+ // blockLabelTarget returns a RenameTarget when the cursor is on the first
163+ // label of an `include` or `dependency` block.
164+ func blockLabelTarget (block * hclsyntax.Block , position protocol.Position ) (RenameTarget , bool ) {
165+ null := RenameTarget {Context : RenameContextNull }
166+
167+ context , ok := contextForRoot (block .Type )
168+ if ! ok || context == RenameContextLocal {
169+ return null , false
170+ }
171+
172+ if len (block .Labels ) == 0 || len (block .LabelRanges ) == 0 {
173+ return null , false
174+ }
175+
176+ if ! rangeContainsPosition (block .LabelRanges [0 ], position ) {
177+ return null , false
178+ }
179+
180+ return RenameTarget {
181+ Name : block .Labels [0 ],
182+ Context : context ,
183+ IdentRange : labelInnerRange (block .LabelRanges [0 ]),
184+ }, true
185+ }
186+
187+ // contextForRoot maps an HCL root identifier (the first traversal step or a
188+ // block type) to a rename context. Returns false for unsupported names.
189+ func contextForRoot (name string ) (string , bool ) {
190+ switch name {
191+ case "local" :
192+ return RenameContextLocal , true
193+ case "dependency" :
194+ return RenameContextDependency , true
195+ }
196+
197+ return "" , false
198+ }
199+
149200// FindAllOccurrences returns every rename occurrence of target across all
150201// sibling .hcl files in the same directory as originFile (including originFile
151202// itself). When a file has an entry in configs with a parsed AST, that AST is
@@ -175,7 +226,7 @@ func FindAllOccurrences(l logger.Logger, target RenameTarget, originFile string,
175226 continue
176227 }
177228
178- occurrences = append (occurrences , definitionOccurrences (target , file , iast )... )
229+ occurrences = append (occurrences , definitionOccurrences (target , file , iast , body )... )
179230
180231 ast .WalkReferences (body , target .Context , target .Name , func (_ * hclsyntax.ScopeTraversalExpr , r hcl.Range ) {
181232 occurrences = append (occurrences , Occurrence {
@@ -204,26 +255,50 @@ func FindAllOccurrences(l logger.Logger, target RenameTarget, originFile string,
204255}
205256
206257// definitionOccurrences finds the definition site(s) of target in the given file.
207- func definitionOccurrences (target RenameTarget , file string , iast * ast.IndexedAST ) []Occurrence {
208- if target .Context != RenameContextLocal {
209- return nil
210- }
258+ func definitionOccurrences (target RenameTarget , file string , iast * ast.IndexedAST , body * hclsyntax.Body ) []Occurrence {
259+ var occs []Occurrence
211260
212- def , ok := iast .Locals [target .Name ]
213- if ! ok {
214- return nil
215- }
261+ switch target .Context {
262+ case RenameContextLocal :
263+ def , ok := iast .Locals [target .Name ]
264+ if ! ok {
265+ return nil
266+ }
216267
217- attr , ok := def .Node .(* hclsyntax.Attribute )
218- if ! ok {
219- return nil
268+ attr , ok := def .Node .(* hclsyntax.Attribute )
269+ if ! ok {
270+ return nil
271+ }
272+
273+ occs = append (occs , Occurrence {
274+ File : file ,
275+ Range : ast .FromHCLRange (attr .NameRange ),
276+ IsDefinition : true ,
277+ })
278+
279+ case RenameContextDependency :
280+ for _ , blk := range body .Blocks {
281+ if blk .Type != target .Context {
282+ continue
283+ }
284+
285+ if len (blk .Labels ) == 0 || blk .Labels [0 ] != target .Name {
286+ continue
287+ }
288+
289+ if len (blk .LabelRanges ) == 0 {
290+ continue
291+ }
292+
293+ occs = append (occs , Occurrence {
294+ File : file ,
295+ Range : labelInnerRange (blk .LabelRanges [0 ]),
296+ IsDefinition : true ,
297+ })
298+ }
220299 }
221300
222- return []Occurrence {{
223- File : file ,
224- Range : ast .FromHCLRange (attr .NameRange ),
225- IsDefinition : true ,
226- }}
301+ return occs
227302}
228303
229304// siblingHCLFiles returns absolute paths of *.hcl files in dir, excluding
@@ -300,6 +375,17 @@ func getOrParseAST(path string, configs map[string]store.Store, l logger.Logger)
300375 return iast
301376}
302377
378+ // labelInnerRange returns the LSP range of a quoted label's contents, excluding
379+ // the surrounding double quotes.
380+ func labelInnerRange (r hcl.Range ) protocol.Range {
381+ r .Start .Column ++
382+ r .Start .Byte ++
383+ r .End .Column --
384+ r .End .Byte --
385+
386+ return ast .FromHCLRange (r )
387+ }
388+
303389// rangeContainsPosition reports whether p (LSP coordinates) is inside r (HCL
304390// coordinates). The end of an HCL range is exclusive.
305391func rangeContainsPosition (r hcl.Range , p protocol.Position ) bool {
0 commit comments