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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to the Docker Language Server will be documented in this fil
- add anchor resolution for all supported document links ([#348](https://github.com/docker/docker-language-server/issues/348))
- return document links for the `file` attribute of a service object's `extends` attribute object ([#172](https://github.com/docker/docker-language-server/issues/172))
- provide document links for models on Docker Hub and Hugging Face ([#356](https://github.com/docker/docker-language-server/issues/356))
- return document links for the `label_file` attribute of a service object ([#360](https://github.com/docker/docker-language-server/issues/360))
- textDocument/hover
- support hovering over referenced models ([#343](https://github.com/docker/docker-language-server/issues/343))

Expand Down
23 changes: 23 additions & 0 deletions internal/compose/documentLink.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ func createImageLink(serviceNode *ast.MappingValueNode) *protocol.DocumentLink {
return nil
}

func createLabelFileLink(u *url.URL, serviceNode *ast.MappingValueNode) []protocol.DocumentLink {
if resolveAnchor(serviceNode.Key).GetToken().Value == "label_file" {
if sequence, ok := resolveAnchor(serviceNode.Value).(*ast.SequenceNode); ok {
links := []protocol.DocumentLink{}
for _, node := range sequence.Values {
if s, ok := resolveAnchor(node).(*ast.StringNode); ok {
links = append(links, *createLink(u, s.GetToken()))
}
}
return links
}

link := createFileLink(u, serviceNode)
if link != nil {
return []protocol.DocumentLink{*link}
}
}
return nil
}

func createObjectFileLink(u *url.URL, serviceNode *ast.MappingValueNode) *protocol.DocumentLink {
if resolveAnchor(serviceNode.Key).GetToken().Value == "file" {
return createFileLink(u, serviceNode)
Expand Down Expand Up @@ -188,6 +208,9 @@ func scanForLinks(u *url.URL, n *ast.MappingValueNode) []protocol.DocumentLink {
if link != nil {
links = append(links, *link)
}

labelFileLinks := createLabelFileLink(u, serviceAttribute)
links = append(links, labelFileLinks...)
}
}
}
Expand Down
213 changes: 213 additions & 0 deletions internal/compose/documentLink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,219 @@ services:
})
}
}
func TestDocumentLink_ServiceLabelFileLinks(t *testing.T) {
testsFolder := filepath.Join(os.TempDir(), t.Name())
composeStringURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(testsFolder, "compose.yaml")), "/"))

testCases := []struct {
name string
content string
path string
linkRange protocol.Range
}{
{
name: "string value app.labels",
content: `
services:
test:
label_file: app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 16},
End: protocol.Position{Line: 3, Character: 26},
},
},
{
name: "string value ./app.labels",
content: `
services:
test:
label_file: ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 16},
End: protocol.Position{Line: 3, Character: 28},
},
},
{
name: "quoted string value \"./app.labels\"",
content: `
services:
test:
label_file: "./app.labels"`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 17},
End: protocol.Position{Line: 3, Character: 29},
},
},
{
name: "attribute value is null",
content: `
services:
test:
label_file: null`,
},
{
name: "array items",
content: `
services:
test:
label_file:
- ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 8},
End: protocol.Position{Line: 4, Character: 20},
},
},
{
name: "array item is null",
content: `
services:
test:
label_file:
- null`,
},
{
name: "anchors and aliases to nothing",
content: `
services:
test:
label_file: &anchor
test2:
label_file: *anchor`,
},
{
name: "anchor on the services object itself",
content: `
&anchor services:
test:
label_file: ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 16},
End: protocol.Position{Line: 3, Character: 28},
},
},
{
name: "anchor on the services object's value",
content: `
services: &anchor
test:
label_file: ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 16},
End: protocol.Position{Line: 3, Character: 28},
},
},
{
name: "anchor on the service object itself",
content: `
services:
&anchor test:
label_file: ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 16},
End: protocol.Position{Line: 3, Character: 28},
},
},
{
name: "anchor on the service object's value",
content: `
services:
test: &anchor
label_file: ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 16},
End: protocol.Position{Line: 3, Character: 28},
},
},
{
name: "anchor on the service object's value as JSON",
content: `
services:
test: &anchor { label_file: ./app.labels }`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 2, Character: 30},
End: protocol.Position{Line: 2, Character: 42},
},
},
{
name: "anchor on the label_file string attribute itself",
content: `
services:
test: &anchor
&anchor label_file: ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 24},
End: protocol.Position{Line: 3, Character: 36},
},
},
{
name: "anchor on the label_file string attribute's value",
content: `
services:
test: &anchor
label_file: &anchor ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 3, Character: 24},
End: protocol.Position{Line: 3, Character: 36},
},
},
{
name: "anchor on the label_file array attribute's value",
content: `
services:
test: &anchor
label_file: &anchor
- ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 8},
End: protocol.Position{Line: 4, Character: 20},
},
},
{
name: "anchor on the label_file array item's value",
content: `
services:
test: &anchor
label_file:
- &anchor ./app.labels`,
path: filepath.Join(testsFolder, "./app.labels"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 16},
End: protocol.Position{Line: 4, Character: 28},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mgr := document.NewDocumentManager()
doc := document.NewComposeDocument(mgr, "compose.yaml", 1, []byte(tc.content))
links, err := DocumentLink(context.Background(), composeStringURI, doc)
require.NoError(t, err)
if tc.path == "" {
require.Equal(t, []protocol.DocumentLink{}, links)
} else {
link := protocol.DocumentLink{
Range: tc.linkRange,
Target: types.CreateStringPointer(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(tc.path), "/"))),
Tooltip: types.CreateStringPointer(tc.path),
}
require.Equal(t, []protocol.DocumentLink{link}, links)
}
})
}
}

func TestDocumentLink_ConfigFileLinks(t *testing.T) {
testsFolder := filepath.Join(os.TempDir(), t.Name())
Expand Down
Loading