Skip to content

Suggest build stage items for a Compose's build target #175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

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

## [Unreleased]

### Added

- Compose
- textDocument/completion
- support build stage names for the `target` attribute ([#173](https://github.com/docker/docker-language-server/issues/173))

## [0.6.0] - 2025-05-07

### Added
Expand Down
12 changes: 6 additions & 6 deletions internal/bake/hcl/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax"
)

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

hclPos := parser.ConvertToHCLPosition(string(document.Input()), int(params.Position.Line), int(params.Position.Character))
candidates, err := document.Decoder().CompletionAtPos(ctx, filename, hclPos)
hclPos := parser.ConvertToHCLPosition(string(bakeDocument.Input()), int(params.Position.Line), int(params.Position.Character))
candidates, err := bakeDocument.Decoder().CompletionAtPos(ctx, filename, hclPos)
if err != nil {
var rangeErr *decoder.PosOutOfRangeError
if errors.As(err, &rangeErr) {
Expand All @@ -30,7 +30,7 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
return nil, fmt.Errorf("textDocument/completion encountered an error: %w", err)
}
}
body, ok := document.File().Body.(*hclsyntax.Body)
body, ok := bakeDocument.File().Body.(*hclsyntax.Body)
if !ok {
return nil, errors.New("unrecognized body in HCL document")
}
Expand Down Expand Up @@ -68,12 +68,12 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
}
}

dockerfilePath, err := document.DockerfileForTarget(b)
dockerfilePath, err := bakeDocument.DockerfileForTarget(b)
if dockerfilePath == "" || err != nil {
continue
}

_, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
_, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
if nodes != nil {
if attribute, ok := attributes["target"]; ok && isInsideRange(attribute.Expr.Range(), params.Position) {
if _, ok := attributes["dockerfile-inline"]; ok {
Expand Down
35 changes: 2 additions & 33 deletions internal/bake/hcl/definition.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package hcl

import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"

Expand All @@ -15,14 +12,9 @@ import (
"github.com/docker/docker-language-server/internal/types"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"go.lsp.dev/uri"
)

func LocalDockerfile(u *url.URL) (string, error) {
return types.AbsolutePath(u, "Dockerfile")
}

func Definition(ctx context.Context, definitionLinkSupport bool, manager *document.Manager, documentURI uri.URI, doc document.BakeHCLDocument, position protocol.Position) (any, error) {
body, ok := doc.File().Body.(*hclsyntax.Body)
if !ok {
Expand Down Expand Up @@ -130,7 +122,7 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager
value, _ := literalValueExpr.Value(&hcl.EvalContext{})
target := value.AsString()

bytes, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
bytes, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
lines := strings.Split(string(bytes), "\n")
for _, child := range nodes {
if strings.EqualFold(child.Value, "FROM") {
Expand Down Expand Up @@ -191,7 +183,7 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager
end--
}
arg := string(doc.Input()[start:end])
bytes, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
bytes, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
lines := strings.Split(string(bytes), "\n")
for _, child := range nodes {
if strings.EqualFold(child.Value, "ARG") {
Expand Down Expand Up @@ -455,26 +447,3 @@ func CalculateBlockLocation(definitionLinkSupport bool, input []byte, body *hcls
}
return nil
}

func ParseDockerfile(dockerfilePath string) ([]byte, *parser.Result, error) {
dockerfileBytes, err := os.ReadFile(dockerfilePath)
if err != nil {
return nil, nil, err
}
result, err := parser.Parse(bytes.NewReader(dockerfileBytes))
return dockerfileBytes, result, err
}

func OpenDockerfile(ctx context.Context, manager *document.Manager, path string) ([]byte, []*parser.Node) {
doc := manager.Get(ctx, uri.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(path), "/"))))
if doc != nil {
if dockerfile, ok := doc.(document.DockerfileDocument); ok {
return dockerfile.Input(), dockerfile.Nodes()
}
}
dockerfileBytes, result, err := ParseDockerfile(path)
if err != nil {
return nil, nil
}
return dockerfileBytes, result.AST.Children
}
5 changes: 3 additions & 2 deletions internal/bake/hcl/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

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

u, err := url.Parse("file:///home/unix/docker-bake.hcl")
require.NoError(t, err)
path, err := LocalDockerfile(u)
path, err := types.LocalDockerfile(u)
require.NoError(t, err)
require.Equal(t, "/home/unix/Dockerfile", path)
}
Expand All @@ -37,7 +38,7 @@ func TestLocalDockerfileForWindows(t *testing.T) {

u, err := url.Parse("file:///c%3A/Users/windows/docker-bake.hcl")
require.NoError(t, err)
path, err := LocalDockerfile(u)
path, err := types.LocalDockerfile(u)
require.NoError(t, err)
require.Equal(t, "c:\\Users\\windows\\Dockerfile", path)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/bake/hcl/diagnosticsCollector.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ func (c *BakeHCLDiagnosticsCollector) CollectDiagnostics(source, workspaceFolder

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

_, nodes := OpenDockerfile(context.Background(), c.docs, dockerfilePath)
_, nodes := document.OpenDockerfile(context.Background(), c.docs, dockerfilePath)
found := false
for _, child := range nodes {
if strings.EqualFold(child.Value, "FROM") {
Expand Down
2 changes: 1 addition & 1 deletion internal/bake/hcl/inlayHint.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func InlayHint(docs *document.Manager, doc document.BakeHCLDocument, rng protoco
if expr, ok := attribute.Expr.(*hclsyntax.ObjectConsExpr); ok && len(expr.Items) > 0 {
dockerfilePath, err := doc.DockerfileForTarget(block)
if dockerfilePath != "" && err == nil {
_, nodes := OpenDockerfile(context.Background(), docs, dockerfilePath)
_, nodes := document.OpenDockerfile(context.Background(), docs, dockerfilePath)
args := map[string]string{}
for _, child := range nodes {
if strings.EqualFold(child.Value, "ARG") {
Expand Down
13 changes: 7 additions & 6 deletions internal/bake/hcl/inlineCompletion.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

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

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

body, ok := document.File().Body.(*hclsyntax.Body)
body, ok := bakeDocument.File().Body.(*hclsyntax.Body)
if !ok {
return nil, errors.New("unrecognized body in HCL document")
}

dockerfilePath, err := LocalDockerfile(url)
dockerfilePath, err := types.LocalDockerfile(url)
if err != nil {
return nil, fmt.Errorf("invalid document path: %v", url.Path)
}

if !shouldSuggest(document.Input(), body, params.Position) {
if !shouldSuggest(bakeDocument.Input(), body, params.Position) {
return nil, nil
}

Expand Down Expand Up @@ -86,7 +87,7 @@ func InlineCompletion(ctx context.Context, params *protocol.InlineCompletionPara
argNames := []string{}
args := map[string]string{}
targets := []string{}
_, nodes := OpenDockerfile(ctx, manager, dockerfilePath)
_, nodes := document.OpenDockerfile(ctx, manager, dockerfilePath)
before := true
for _, child := range nodes {
if strings.EqualFold(child.Value, "ARG") && before {
Expand All @@ -113,7 +114,7 @@ func InlineCompletion(ctx context.Context, params *protocol.InlineCompletionPara

if len(targets) > 0 {
items := []protocol.InlineCompletionItem{}
lines := strings.Split(string(document.Input()), "\n")
lines := strings.Split(string(bakeDocument.Input()), "\n")
for _, target := range targets {
sb := strings.Builder{}
sb.WriteString(fmt.Sprintf("target \"%v\" {\n", target))
Expand Down
58 changes: 56 additions & 2 deletions internal/compose/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package compose
import (
"context"
"fmt"
"net/url"
"slices"
"strings"
"unicode"
Expand Down Expand Up @@ -40,7 +41,12 @@ func array(line string, character int) bool {
return isArray
}

func Completion(ctx context.Context, params *protocol.CompletionParams, doc document.ComposeDocument) (*protocol.CompletionList, error) {
func Completion(ctx context.Context, params *protocol.CompletionParams, manager *document.Manager, doc document.ComposeDocument) (*protocol.CompletionList, error) {
u, err := url.Parse(params.TextDocument.URI)
if err != nil {
return nil, fmt.Errorf("LSP client sent invalid URI: %v", params.TextDocument.URI)
}

if params.Position.Character == 0 {
items := []protocol.CompletionItem{}
for attributeName, schema := range schemaProperties() {
Expand Down Expand Up @@ -81,8 +87,12 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, doc docu
if len(dependencies) > 0 {
return &protocol.CompletionList{Items: dependencies}, nil
}
items, stop := buildTargetCompletionItems(params, manager, path, u, protocol.UInteger(len(wordPrefix)))
if stop {
return &protocol.CompletionList{Items: items}, nil
}

items := volumeDependencyCompletionItems(file, path, params, protocol.UInteger(len(wordPrefix)))
items = volumeDependencyCompletionItems(file, path, params, protocol.UInteger(len(wordPrefix)))
if len(items) > 0 {
return &protocol.CompletionList{Items: items}, nil
}
Expand Down Expand Up @@ -201,6 +211,50 @@ func findDependencies(file *ast.File, dependencyType string) []string {
return services
}

func findBuildStages(params *protocol.CompletionParams, manager *document.Manager, dockerfilePath, prefix string, prefixLength protocol.UInteger) []protocol.CompletionItem {
_, nodes := document.OpenDockerfile(context.Background(), manager, dockerfilePath)
items := []protocol.CompletionItem{}
for _, child := range nodes {
if strings.EqualFold(child.Value, "FROM") {
if child.Next != nil && child.Next.Next != nil && strings.EqualFold(child.Next.Next.Value, "AS") && child.Next.Next.Next != nil {
buildStage := child.Next.Next.Next.Value
if strings.HasPrefix(buildStage, prefix) {
items = append(items, protocol.CompletionItem{
Label: buildStage,
Documentation: child.Next.Value,
TextEdit: protocol.TextEdit{
NewText: buildStage,
Range: protocol.Range{
Start: protocol.Position{
Line: params.Position.Line,
Character: params.Position.Character - prefixLength,
},
End: params.Position,
},
},
})
}
}
}
}
return items
}

func buildTargetCompletionItems(params *protocol.CompletionParams, manager *document.Manager, path []*ast.MappingValueNode, u *url.URL, prefixLength protocol.UInteger) ([]protocol.CompletionItem, bool) {
if len(path) == 4 && path[2].Key.GetToken().Value == "build" && path[3].Key.GetToken().Value == "target" {
dockerfilePath, err := types.LocalDockerfile(u)
if err == nil {
if _, ok := path[3].Value.(*ast.NullNode); ok {
return findBuildStages(params, manager, dockerfilePath, "", prefixLength), true
} else if prefix, ok := path[3].Value.(*ast.StringNode); ok {
offset := int(params.Position.Character) - path[3].Value.GetToken().Position.Column + 1
return findBuildStages(params, manager, dockerfilePath, prefix.Value[0:offset], prefixLength), true
}
}
}
return nil, false
}

func dependencyCompletionItems(file *ast.File, path []*ast.MappingValueNode, params *protocol.CompletionParams, prefixLength protocol.UInteger) []protocol.CompletionItem {
dependency := map[string]string{
"depends_on": "services",
Expand Down
Loading
Loading