Skip to content

Commit 9f2f18e

Browse files
authored
Merge pull request #178 from docker/path-resolution-build-stage-completions
Add Dockerfile path resolution for build stage completions
2 parents ccc1152 + a178cf1 commit 9f2f18e

File tree

3 files changed

+477
-269
lines changed

3 files changed

+477
-269
lines changed

internal/compose/completion.go

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import (
1515
"github.com/santhosh-tekuri/jsonschema/v6"
1616
)
1717

18+
type completionItemText struct {
19+
label string
20+
newText string
21+
documentation string
22+
}
23+
1824
func prefix(line string, character int) string {
1925
sb := strings.Builder{}
2026
for i := range character {
@@ -101,7 +107,7 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
101107
items = namedDependencyCompletionItems(file, path, "secrets", "secrets", params, protocol.UInteger(len(wordPrefix)))
102108
}
103109
isArray := array(lines[lspLine], character-1)
104-
nodeProps, arrayAttributes := nodeProperties(path, line, character)
110+
path, nodeProps, arrayAttributes := nodeProperties(path, line, character)
105111
if isArray != arrayAttributes {
106112
return nil, nil
107113
}
@@ -184,6 +190,7 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
184190
},
185191
}
186192
}
193+
item.TextEdit = modifyTextEdit(manager, u, item.TextEdit.(protocol.TextEdit), attributeName, path)
187194
items = append(items, item)
188195
}
189196
}
@@ -196,6 +203,54 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
196203
return &protocol.CompletionList{Items: items}, nil
197204
}
198205

206+
func createChoiceSnippetText(itemTexts []completionItemText) string {
207+
sb := strings.Builder{}
208+
sb.WriteString("${1|")
209+
for i, stage := range itemTexts {
210+
sb.WriteString(stage.newText)
211+
if i != len(itemTexts)-1 {
212+
sb.WriteString(",")
213+
}
214+
}
215+
sb.WriteString("|}")
216+
return sb.String()
217+
}
218+
219+
func modifyTextEdit(manager *document.Manager, u *url.URL, edit protocol.TextEdit, attributeName string, path []*ast.MappingValueNode) protocol.TextEdit {
220+
if attributeName == "target" && len(path) == 3 && path[2].Key.GetToken().Value == "build" {
221+
if _, ok := path[2].Value.(*ast.NullNode); ok {
222+
dockerfilePath, err := types.LocalDockerfile(u)
223+
if err == nil {
224+
stages := findBuildStages(manager, dockerfilePath, "")
225+
if len(stages) > 0 {
226+
edit.NewText = fmt.Sprintf("%v%v", edit.NewText, createChoiceSnippetText(stages))
227+
return edit
228+
}
229+
}
230+
} else if mappingNode, ok := path[2].Value.(*ast.MappingNode); ok {
231+
dockerfileAttributePath := "Dockerfile"
232+
for _, buildAttribute := range mappingNode.Values {
233+
switch buildAttribute.Key.GetToken().Value {
234+
case "dockerfile_inline":
235+
return edit
236+
case "dockerfile":
237+
dockerfileAttributePath = buildAttribute.Value.GetToken().Value
238+
}
239+
}
240+
241+
dockerfilePath, err := types.AbsolutePath(u, dockerfileAttributePath)
242+
if err == nil {
243+
stages := findBuildStages(manager, dockerfilePath, "")
244+
if len(stages) > 0 {
245+
edit.NewText = fmt.Sprintf("%v%v", edit.NewText, createChoiceSnippetText(stages))
246+
return edit
247+
}
248+
}
249+
}
250+
}
251+
return edit
252+
}
253+
199254
func findDependencies(file *ast.File, dependencyType string) []string {
200255
services := []string{}
201256
for _, documentNode := range file.Docs {
@@ -216,27 +271,18 @@ func findDependencies(file *ast.File, dependencyType string) []string {
216271
return services
217272
}
218273

219-
func findBuildStages(params *protocol.CompletionParams, manager *document.Manager, dockerfilePath, prefix string, prefixLength protocol.UInteger) []protocol.CompletionItem {
274+
func findBuildStages(manager *document.Manager, dockerfilePath, prefix string) []completionItemText {
220275
_, nodes := document.OpenDockerfile(context.Background(), manager, dockerfilePath)
221-
items := []protocol.CompletionItem{}
276+
items := []completionItemText{}
222277
for _, child := range nodes {
223278
if strings.EqualFold(child.Value, "FROM") {
224279
if child.Next != nil && child.Next.Next != nil && strings.EqualFold(child.Next.Next.Value, "AS") && child.Next.Next.Next != nil {
225280
buildStage := child.Next.Next.Next.Value
226281
if strings.HasPrefix(buildStage, prefix) {
227-
items = append(items, protocol.CompletionItem{
228-
Label: buildStage,
229-
Documentation: child.Next.Value,
230-
TextEdit: protocol.TextEdit{
231-
NewText: buildStage,
232-
Range: protocol.Range{
233-
Start: protocol.Position{
234-
Line: params.Position.Line,
235-
Character: params.Position.Character - prefixLength,
236-
},
237-
End: params.Position,
238-
},
239-
},
282+
items = append(items, completionItemText{
283+
label: buildStage,
284+
documentation: child.Next.Value,
285+
newText: buildStage,
240286
})
241287
}
242288
}
@@ -247,19 +293,52 @@ func findBuildStages(params *protocol.CompletionParams, manager *document.Manage
247293

248294
func buildTargetCompletionItems(params *protocol.CompletionParams, manager *document.Manager, path []*ast.MappingValueNode, u *url.URL, prefixLength protocol.UInteger) ([]protocol.CompletionItem, bool) {
249295
if len(path) == 4 && path[2].Key.GetToken().Value == "build" && path[3].Key.GetToken().Value == "target" {
250-
dockerfilePath, err := types.LocalDockerfile(u)
251-
if err == nil {
252-
if _, ok := path[3].Value.(*ast.NullNode); ok {
253-
return findBuildStages(params, manager, dockerfilePath, "", prefixLength), true
254-
} else if prefix, ok := path[3].Value.(*ast.StringNode); ok {
255-
offset := int(params.Position.Character) - path[3].Value.GetToken().Position.Column + 1
256-
return findBuildStages(params, manager, dockerfilePath, prefix.Value[0:offset], prefixLength), true
296+
if mappingNode, ok := path[2].Value.(*ast.MappingNode); ok {
297+
dockerfileAttributePath := "Dockerfile"
298+
for _, buildAttribute := range mappingNode.Values {
299+
switch buildAttribute.Key.GetToken().Value {
300+
case "dockerfile_inline":
301+
return nil, true
302+
case "dockerfile":
303+
dockerfileAttributePath = buildAttribute.Value.GetToken().Value
304+
}
305+
}
306+
307+
dockerfilePath, err := types.AbsolutePath(u, dockerfileAttributePath)
308+
if err == nil {
309+
if _, ok := path[3].Value.(*ast.NullNode); ok {
310+
return createBuildStageItems(params, manager, dockerfilePath, "", prefixLength), true
311+
} else if prefix, ok := path[3].Value.(*ast.StringNode); ok {
312+
offset := int(params.Position.Character) - path[3].Value.GetToken().Position.Column + 1
313+
return createBuildStageItems(params, manager, dockerfilePath, prefix.Value[0:offset], prefixLength), true
314+
}
257315
}
258316
}
259317
}
260318
return nil, false
261319
}
262320

321+
func createBuildStageItems(params *protocol.CompletionParams, manager *document.Manager, dockerfilePath, prefix string, prefixLength protocol.UInteger) []protocol.CompletionItem {
322+
items := []protocol.CompletionItem{}
323+
for _, itemText := range findBuildStages(manager, dockerfilePath, prefix) {
324+
items = append(items, protocol.CompletionItem{
325+
Label: itemText.label,
326+
Documentation: itemText.documentation,
327+
TextEdit: protocol.TextEdit{
328+
NewText: itemText.newText,
329+
Range: protocol.Range{
330+
Start: protocol.Position{
331+
Line: params.Position.Line,
332+
Character: params.Position.Character - prefixLength,
333+
},
334+
End: params.Position,
335+
},
336+
},
337+
})
338+
}
339+
return items
340+
}
341+
263342
func dependencyCompletionItems(file *ast.File, path []*ast.MappingValueNode, params *protocol.CompletionParams, prefixLength protocol.UInteger) []protocol.CompletionItem {
264343
dependency := map[string]string{
265344
"depends_on": "services",

0 commit comments

Comments
 (0)