Skip to content
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to the Docker Language Server will be documented in this fil

### Added

- Compose
- textDocument/completion
- suggest image tags for images from Docker Hub ([#375](https://github.com/docker/docker-language-server/issues/375))
- Bake
- textDocument/completion
- provide local file and folder name suggestions ([#414](https://github.com/docker/docker-language-server/issues/414))
Expand Down
54 changes: 52 additions & 2 deletions internal/compose/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (
"strings"
"unicode"

"github.com/docker/docker-language-server/internal/hub"
"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/goccy/go-yaml/ast"
"github.com/goccy/go-yaml/token"
"github.com/santhosh-tekuri/jsonschema/v6"
)

Expand Down Expand Up @@ -177,7 +179,7 @@ func calculateTopLevelNodeOffset(file *ast.File) int {
return -1
}

func Completion(ctx context.Context, params *protocol.CompletionParams, manager *document.Manager, doc document.ComposeDocument) (*protocol.CompletionList, error) {
func Completion(ctx context.Context, params *protocol.CompletionParams, manager *document.Manager, hub *hub.Service, doc document.ComposeDocument) (*protocol.CompletionList, error) {
documentPath, err := doc.DocumentPath()
if err != nil {
return nil, fmt.Errorf("LSP client sent invalid URI: %v", params.TextDocument.URI)
Expand Down Expand Up @@ -241,6 +243,10 @@ func Completion(ctx context.Context, params *protocol.CompletionParams, manager
if stop {
return &protocol.CompletionList{Items: items}, nil
}
items, stop = serviceImageCompletionItems(*hub, path, prefixContent)
if stop {
return &protocol.CompletionList{Items: sortItems(items)}, nil
}
folderStructureItems := folderStructureCompletionItems(documentPath, path, removeQuote(prefixContent))
if len(folderStructureItems) > 0 {
return processItems(folderStructureItems, whitespaceLine && arrayAttributes), nil
Expand Down Expand Up @@ -357,10 +363,15 @@ func createSchemaItems(params *protocol.CompletionParams, nodeProps any, lines [
return items
}

func processItems(items []protocol.CompletionItem, arrayPrefix bool) *protocol.CompletionList {
func sortItems(items []protocol.CompletionItem) []protocol.CompletionItem {
slices.SortFunc(items, func(a, b protocol.CompletionItem) int {
return strings.Compare(a.Label, b.Label)
})
return items
}

func processItems(items []protocol.CompletionItem, arrayPrefix bool) *protocol.CompletionList {
items = sortItems(items)
if arrayPrefix {
for i := range items {
if edit, ok := items[i].TextEdit.(protocol.TextEdit); ok {
Expand Down Expand Up @@ -570,6 +581,45 @@ func buildTargetCompletionItems(params *protocol.CompletionParams, manager *docu
return nil, false
}

func hubRepositoryImage(imageValue string) (repository, image, tagPrefix string) {
idx := strings.Index(imageValue, ":")
if idx == -1 {
return "", "", ""
}
slashIndex := strings.Index(imageValue, "/")
if slashIndex != strings.LastIndex(imageValue, "/") {
return "", "", ""
}
split := strings.Split(imageValue[0:idx], "/")
if len(split) == 1 {
return "library", split[0], imageValue[idx+1:]
}
return split[0], split[1], imageValue[idx+1:]
}

func serviceImageCompletionItems(hub hub.Service, path []*ast.MappingValueNode, prefix string) ([]protocol.CompletionItem, bool) {
if len(path) == 3 && path[0].Key.GetToken().Value == "services" && path[2].Key.GetToken().Value == "image" {
if path[2].Value.GetToken().Type == token.DoubleQuoteType || path[2].Value.GetToken().Type == token.SingleQuoteType {
prefix = prefix[1:]
}
repository, image, tagPrefix := hubRepositoryImage(prefix)
if repository != "" {
tags, _ := hub.GetTags(repository, image)
items := []protocol.CompletionItem{}
for _, tag := range tags {
if strings.HasPrefix(tag, tagPrefix) {
items = append(items, protocol.CompletionItem{
Label: tag,
Kind: types.CreateCompletionItemKindPointer(protocol.CompletionItemKindModule),
})
}
}
return items, true
}
}
return nil, false
}

func createBuildStageItems(params *protocol.CompletionParams, manager *document.Manager, dockerfileURI, dockerfilePath, prefix string, prefixLength protocol.UInteger) []protocol.CompletionItem {
items := []protocol.CompletionItem{}
for _, itemText := range findBuildStages(manager, dockerfileURI, dockerfilePath, prefix) {
Expand Down
140 changes: 131 additions & 9 deletions internal/compose/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/docker/docker-language-server/internal/hub"
"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"
Expand Down Expand Up @@ -2861,13 +2862,14 @@ services:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list, list)
})
Expand Down Expand Up @@ -4027,13 +4029,14 @@ models:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, nil, doc)
}, nil, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list, list)
})
Expand Down Expand Up @@ -4397,6 +4400,7 @@ services:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
if tc.dockerfileContent != "" {
u := dockerfileURI
if tc.dockerfileURI != "" {
Expand All @@ -4412,7 +4416,7 @@ services:
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list(), list)
})
Expand Down Expand Up @@ -4525,6 +4529,7 @@ services:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
changed, err := manager.Write(context.Background(), uri.URI(dockerfileURI), protocol.DockerfileLanguage, 1, []byte(tc.dockerfileContent))
require.NoError(t, err)
require.True(t, changed)
Expand All @@ -4534,7 +4539,7 @@ services:
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list(), list)
})
Expand Down Expand Up @@ -4586,13 +4591,14 @@ services:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list, list)
})
Expand Down Expand Up @@ -4719,13 +4725,14 @@ services:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list, list)
})
Expand Down Expand Up @@ -4974,13 +4981,14 @@ services:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list, list)
})
Expand Down Expand Up @@ -5248,13 +5256,14 @@ include:
for _, setup := range setups {
t.Run(fmt.Sprintf("%v (%v)", tc.name, setup.description), func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content+setup.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character + setup.offset},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
if tc.hideFiles {
require.Equal(t, setup.folderResult, list)
Expand Down Expand Up @@ -5330,13 +5339,126 @@ include:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, doc)
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list, list)
})
}
}

func TestCompletion_ImageTags(t *testing.T) {
testCases := []struct {
name string
content string
line uint32
character uint32
list *protocol.CompletionList
}{
{
name: "docker/lsp:g",
content: `
services:
test:
image: docker/lsp:g`,
line: 3,
character: 23,
list: &protocol.CompletionList{
Items: []protocol.CompletionItem{
{
Label: "golang",
Kind: types.CreateCompletionItemKindPointer(protocol.CompletionItemKindModule),
},
},
},
},
{
name: "'docker/lsp:g'",
content: `
services:
test:
image: 'docker/lsp:g'`,
line: 3,
character: 24,
list: &protocol.CompletionList{
Items: []protocol.CompletionItem{
{
Label: "golang",
Kind: types.CreateCompletionItemKindPointer(protocol.CompletionItemKindModule),
},
},
},
},
{
name: "\"docker/lsp:g\"",
content: `
services:
test:
image: "docker/lsp:g"`,
line: 3,
character: 24,
list: &protocol.CompletionList{
Items: []protocol.CompletionItem{
{
Label: "golang",
Kind: types.CreateCompletionItemKindPointer(protocol.CompletionItemKindModule),
},
},
},
},
{
name: "ubuntu:20.",
content: `
services:
test:
image: ubuntu:20.`,
line: 3,
character: 21,
list: &protocol.CompletionList{
Items: []protocol.CompletionItem{
{
Label: "20.04",
Kind: types.CreateCompletionItemKindPointer(protocol.CompletionItemKindModule),
},
{
Label: "20.10",
Kind: types.CreateCompletionItemKindPointer(protocol.CompletionItemKindModule),
},
},
},
},
{
name: "text is correct but cursor not in the right place",
content: `
services:
test:
image: docker/lsp:g`,
line: 3,
character: 15,
list: nil,
},
}

dir := createFileStructure(t)
composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(dir, "compose.yaml")), "/"))

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
hub := hub.NewService()
doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(tc.content))
list, err := Completion(context.Background(), &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI},
Position: protocol.Position{Line: tc.line, Character: tc.character},
},
}, manager, &hub, doc)
require.NoError(t, err)
require.Equal(t, tc.list, list)
})
Expand Down
Loading
Loading