Skip to content

Commit 16ef1a5

Browse files
authored
Merge pull request #175 from docker/compose-build-stage-suggestions
Suggest build stage items for a Compose's build target
2 parents 8f99d4e + 8391f94 commit 16ef1a5

12 files changed

+310
-55
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to the Docker Language Server will be documented in this file.
44

5+
## [Unreleased]
6+
7+
### Added
8+
9+
- Compose
10+
- textDocument/completion
11+
- support build stage names for the `target` attribute ([#173](https://github.com/docker/docker-language-server/issues/173))
12+
513
## [0.6.0] - 2025-05-07
614

715
### Added

internal/bake/hcl/completion.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import (
1515
"github.com/hashicorp/hcl/v2/hclsyntax"
1616
)
1717

18-
func Completion(ctx context.Context, params *protocol.CompletionParams, manager *document.Manager, document document.BakeHCLDocument) (*protocol.CompletionList, error) {
18+
func Completion(ctx context.Context, params *protocol.CompletionParams, manager *document.Manager, bakeDocument document.BakeHCLDocument) (*protocol.CompletionList, error) {
1919
filename := string(params.TextDocument.URI)
2020

21-
hclPos := parser.ConvertToHCLPosition(string(document.Input()), int(params.Position.Line), int(params.Position.Character))
22-
candidates, err := document.Decoder().CompletionAtPos(ctx, filename, hclPos)
21+
hclPos := parser.ConvertToHCLPosition(string(bakeDocument.Input()), int(params.Position.Line), int(params.Position.Character))
22+
candidates, err := bakeDocument.Decoder().CompletionAtPos(ctx, filename, hclPos)
2323
if err != nil {
2424
var rangeErr *decoder.PosOutOfRangeError
2525
if errors.As(err, &rangeErr) {
@@ -30,7 +30,7 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
3030
return nil, fmt.Errorf("textDocument/completion encountered an error: %w", err)
3131
}
3232
}
33-
body, ok := document.File().Body.(*hclsyntax.Body)
33+
body, ok := bakeDocument.File().Body.(*hclsyntax.Body)
3434
if !ok {
3535
return nil, errors.New("unrecognized body in HCL document")
3636
}
@@ -68,12 +68,12 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
6868
}
6969
}
7070

71-
dockerfilePath, err := document.DockerfileForTarget(b)
71+
dockerfilePath, err := bakeDocument.DockerfileForTarget(b)
7272
if dockerfilePath == "" || err != nil {
7373
continue
7474
}
7575

76-
_, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
76+
_, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
7777
if nodes != nil {
7878
if attribute, ok := attributes["target"]; ok && isInsideRange(attribute.Expr.Range(), params.Position) {
7979
if _, ok := attributes["dockerfile-inline"]; ok {

internal/bake/hcl/definition.go

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package hcl
22

33
import (
4-
"bytes"
54
"context"
65
"errors"
76
"fmt"
8-
"net/url"
9-
"os"
107
"path/filepath"
118
"strings"
129

@@ -15,14 +12,9 @@ import (
1512
"github.com/docker/docker-language-server/internal/types"
1613
"github.com/hashicorp/hcl/v2"
1714
"github.com/hashicorp/hcl/v2/hclsyntax"
18-
"github.com/moby/buildkit/frontend/dockerfile/parser"
1915
"go.lsp.dev/uri"
2016
)
2117

22-
func LocalDockerfile(u *url.URL) (string, error) {
23-
return types.AbsolutePath(u, "Dockerfile")
24-
}
25-
2618
func Definition(ctx context.Context, definitionLinkSupport bool, manager *document.Manager, documentURI uri.URI, doc document.BakeHCLDocument, position protocol.Position) (any, error) {
2719
body, ok := doc.File().Body.(*hclsyntax.Body)
2820
if !ok {
@@ -130,7 +122,7 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager
130122
value, _ := literalValueExpr.Value(&hcl.EvalContext{})
131123
target := value.AsString()
132124

133-
bytes, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
125+
bytes, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
134126
lines := strings.Split(string(bytes), "\n")
135127
for _, child := range nodes {
136128
if strings.EqualFold(child.Value, "FROM") {
@@ -191,7 +183,7 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager
191183
end--
192184
}
193185
arg := string(doc.Input()[start:end])
194-
bytes, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
186+
bytes, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
195187
lines := strings.Split(string(bytes), "\n")
196188
for _, child := range nodes {
197189
if strings.EqualFold(child.Value, "ARG") {
@@ -455,26 +447,3 @@ func CalculateBlockLocation(definitionLinkSupport bool, input []byte, body *hcls
455447
}
456448
return nil
457449
}
458-
459-
func ParseDockerfile(dockerfilePath string) ([]byte, *parser.Result, error) {
460-
dockerfileBytes, err := os.ReadFile(dockerfilePath)
461-
if err != nil {
462-
return nil, nil, err
463-
}
464-
result, err := parser.Parse(bytes.NewReader(dockerfileBytes))
465-
return dockerfileBytes, result, err
466-
}
467-
468-
func OpenDockerfile(ctx context.Context, manager *document.Manager, path string) ([]byte, []*parser.Node) {
469-
doc := manager.Get(ctx, uri.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(path), "/"))))
470-
if doc != nil {
471-
if dockerfile, ok := doc.(document.DockerfileDocument); ok {
472-
return dockerfile.Input(), dockerfile.Nodes()
473-
}
474-
}
475-
dockerfileBytes, result, err := ParseDockerfile(path)
476-
if err != nil {
477-
return nil, nil
478-
}
479-
return dockerfileBytes, result.AST.Children
480-
}

internal/bake/hcl/definition_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/docker/docker-language-server/internal/pkg/document"
1414
"github.com/docker/docker-language-server/internal/tliron/glsp/protocol"
15+
"github.com/docker/docker-language-server/internal/types"
1516
"github.com/stretchr/testify/require"
1617
"go.lsp.dev/uri"
1718
)
@@ -24,7 +25,7 @@ func TestLocalDockerfileForNonWindows(t *testing.T) {
2425

2526
u, err := url.Parse("file:///home/unix/docker-bake.hcl")
2627
require.NoError(t, err)
27-
path, err := LocalDockerfile(u)
28+
path, err := types.LocalDockerfile(u)
2829
require.NoError(t, err)
2930
require.Equal(t, "/home/unix/Dockerfile", path)
3031
}
@@ -37,7 +38,7 @@ func TestLocalDockerfileForWindows(t *testing.T) {
3738

3839
u, err := url.Parse("file:///c%3A/Users/windows/docker-bake.hcl")
3940
require.NoError(t, err)
40-
path, err := LocalDockerfile(u)
41+
path, err := types.LocalDockerfile(u)
4142
require.NoError(t, err)
4243
require.Equal(t, "c:\\Users\\windows\\Dockerfile", path)
4344
}

internal/bake/hcl/diagnosticsCollector.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func (c *BakeHCLDiagnosticsCollector) CollectDiagnostics(source, workspaceFolder
234234

235235
// checkTargetArgs examines the args attribute of a target block.
236236
func (c *BakeHCLDiagnosticsCollector) checkTargetArgs(dockerfilePath string, input []byte, expr *hclsyntax.ObjectConsExpr, source string) []protocol.Diagnostic {
237-
_, nodes := OpenDockerfile(context.Background(), c.docs, dockerfilePath)
237+
_, nodes := document.OpenDockerfile(context.Background(), c.docs, dockerfilePath)
238238
args := []string{}
239239
for _, child := range nodes {
240240
if strings.EqualFold(child.Value, "ARG") {
@@ -283,7 +283,7 @@ func (c *BakeHCLDiagnosticsCollector) checkTargetTarget(dockerfilePath string, e
283283
value, _ := literalValueExpr.Value(&hcl.EvalContext{})
284284
target := value.AsString()
285285

286-
_, nodes := OpenDockerfile(context.Background(), c.docs, dockerfilePath)
286+
_, nodes := document.OpenDockerfile(context.Background(), c.docs, dockerfilePath)
287287
found := false
288288
for _, child := range nodes {
289289
if strings.EqualFold(child.Value, "FROM") {

internal/bake/hcl/inlayHint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func InlayHint(docs *document.Manager, doc document.BakeHCLDocument, rng protoco
2727
if expr, ok := attribute.Expr.(*hclsyntax.ObjectConsExpr); ok && len(expr.Items) > 0 {
2828
dockerfilePath, err := doc.DockerfileForTarget(block)
2929
if dockerfilePath != "" && err == nil {
30-
_, nodes := OpenDockerfile(context.Background(), docs, dockerfilePath)
30+
_, nodes := document.OpenDockerfile(context.Background(), docs, dockerfilePath)
3131
args := map[string]string{}
3232
for _, child := range nodes {
3333
if strings.EqualFold(child.Value, "ARG") {

internal/bake/hcl/inlineCompletion.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/docker/docker-language-server/internal/pkg/document"
1313
"github.com/docker/docker-language-server/internal/tliron/glsp/protocol"
14+
"github.com/docker/docker-language-server/internal/types"
1415
"github.com/hashicorp/hcl/v2"
1516
"github.com/hashicorp/hcl/v2/hclsyntax"
1617
"github.com/zclconf/go-cty/cty"
@@ -42,23 +43,23 @@ func shouldSuggest(content []byte, body *hclsyntax.Body, position protocol.Posit
4243
return strings.TrimSpace(lines[position.Line]) != "}"
4344
}
4445

45-
func InlineCompletion(ctx context.Context, params *protocol.InlineCompletionParams, manager *document.Manager, document document.BakeHCLDocument) ([]protocol.InlineCompletionItem, error) {
46+
func InlineCompletion(ctx context.Context, params *protocol.InlineCompletionParams, manager *document.Manager, bakeDocument document.BakeHCLDocument) ([]protocol.InlineCompletionItem, error) {
4647
url, err := url.Parse(params.TextDocument.URI)
4748
if err != nil {
4849
return nil, fmt.Errorf("LSP client sent invalid URI: %v", params.TextDocument.URI)
4950
}
5051

51-
body, ok := document.File().Body.(*hclsyntax.Body)
52+
body, ok := bakeDocument.File().Body.(*hclsyntax.Body)
5253
if !ok {
5354
return nil, errors.New("unrecognized body in HCL document")
5455
}
5556

56-
dockerfilePath, err := LocalDockerfile(url)
57+
dockerfilePath, err := types.LocalDockerfile(url)
5758
if err != nil {
5859
return nil, fmt.Errorf("invalid document path: %v", url.Path)
5960
}
6061

61-
if !shouldSuggest(document.Input(), body, params.Position) {
62+
if !shouldSuggest(bakeDocument.Input(), body, params.Position) {
6263
return nil, nil
6364
}
6465

@@ -86,7 +87,7 @@ func InlineCompletion(ctx context.Context, params *protocol.InlineCompletionPara
8687
argNames := []string{}
8788
args := map[string]string{}
8889
targets := []string{}
89-
_, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
90+
_, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
9091
before := true
9192
for _, child := range nodes {
9293
if strings.EqualFold(child.Value, "ARG") && before {
@@ -113,7 +114,7 @@ func InlineCompletion(ctx context.Context, params *protocol.InlineCompletionPara
113114

114115
if len(targets) > 0 {
115116
items := []protocol.InlineCompletionItem{}
116-
lines := strings.Split(string(document.Input()), "\n")
117+
lines := strings.Split(string(bakeDocument.Input()), "\n")
117118
for _, target := range targets {
118119
sb := strings.Builder{}
119120
sb.WriteString(fmt.Sprintf("target \"%v\" {\n", target))

internal/compose/completion.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package compose
33
import (
44
"context"
55
"fmt"
6+
"net/url"
67
"slices"
78
"strings"
89
"unicode"
@@ -40,7 +41,12 @@ func array(line string, character int) bool {
4041
return isArray
4142
}
4243

43-
func Completion(ctx context.Context, params *protocol.CompletionParams, doc document.ComposeDocument) (*protocol.CompletionList, error) {
44+
func Completion(ctx context.Context, params *protocol.CompletionParams, manager *document.Manager, doc document.ComposeDocument) (*protocol.CompletionList, error) {
45+
u, err := url.Parse(params.TextDocument.URI)
46+
if err != nil {
47+
return nil, fmt.Errorf("LSP client sent invalid URI: %v", params.TextDocument.URI)
48+
}
49+
4450
if params.Position.Character == 0 {
4551
items := []protocol.CompletionItem{}
4652
for attributeName, schema := range schemaProperties() {
@@ -81,8 +87,12 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, doc docu
8187
if len(dependencies) > 0 {
8288
return &protocol.CompletionList{Items: dependencies}, nil
8389
}
90+
items, stop := buildTargetCompletionItems(params, manager, path, u, protocol.UInteger(len(wordPrefix)))
91+
if stop {
92+
return &protocol.CompletionList{Items: items}, nil
93+
}
8494

85-
items := volumeDependencyCompletionItems(file, path, params, protocol.UInteger(len(wordPrefix)))
95+
items = volumeDependencyCompletionItems(file, path, params, protocol.UInteger(len(wordPrefix)))
8696
if len(items) > 0 {
8797
return &protocol.CompletionList{Items: items}, nil
8898
}
@@ -201,6 +211,50 @@ func findDependencies(file *ast.File, dependencyType string) []string {
201211
return services
202212
}
203213

214+
func findBuildStages(params *protocol.CompletionParams, manager *document.Manager, dockerfilePath, prefix string, prefixLength protocol.UInteger) []protocol.CompletionItem {
215+
_, nodes := document.OpenDockerfile(context.Background(), manager, dockerfilePath)
216+
items := []protocol.CompletionItem{}
217+
for _, child := range nodes {
218+
if strings.EqualFold(child.Value, "FROM") {
219+
if child.Next != nil && child.Next.Next != nil && strings.EqualFold(child.Next.Next.Value, "AS") && child.Next.Next.Next != nil {
220+
buildStage := child.Next.Next.Next.Value
221+
if strings.HasPrefix(buildStage, prefix) {
222+
items = append(items, protocol.CompletionItem{
223+
Label: buildStage,
224+
Documentation: child.Next.Value,
225+
TextEdit: protocol.TextEdit{
226+
NewText: buildStage,
227+
Range: protocol.Range{
228+
Start: protocol.Position{
229+
Line: params.Position.Line,
230+
Character: params.Position.Character - prefixLength,
231+
},
232+
End: params.Position,
233+
},
234+
},
235+
})
236+
}
237+
}
238+
}
239+
}
240+
return items
241+
}
242+
243+
func buildTargetCompletionItems(params *protocol.CompletionParams, manager *document.Manager, path []*ast.MappingValueNode, u *url.URL, prefixLength protocol.UInteger) ([]protocol.CompletionItem, bool) {
244+
if len(path) == 4 && path[2].Key.GetToken().Value == "build" && path[3].Key.GetToken().Value == "target" {
245+
dockerfilePath, err := types.LocalDockerfile(u)
246+
if err == nil {
247+
if _, ok := path[3].Value.(*ast.NullNode); ok {
248+
return findBuildStages(params, manager, dockerfilePath, "", prefixLength), true
249+
} else if prefix, ok := path[3].Value.(*ast.StringNode); ok {
250+
offset := int(params.Position.Character) - path[3].Value.GetToken().Position.Column + 1
251+
return findBuildStages(params, manager, dockerfilePath, prefix.Value[0:offset], prefixLength), true
252+
}
253+
}
254+
}
255+
return nil, false
256+
}
257+
204258
func dependencyCompletionItems(file *ast.File, path []*ast.MappingValueNode, params *protocol.CompletionParams, prefixLength protocol.UInteger) []protocol.CompletionItem {
205259
dependency := map[string]string{
206260
"depends_on": "services",

0 commit comments

Comments
 (0)