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
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]

### Fixed

- Compose
- textDocument/documentLink
- return document links for files referenced in the short-form `volumes` attribute of a service object ([#460](https://github.com/docker/docker-language-server/issues/460))

## [0.18.0] - 2025-08-25

### Added
Expand Down
41 changes: 41 additions & 0 deletions internal/compose/documentLink.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package compose
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/compose-spec/compose-go/v2/format"
composeTypes "github.com/compose-spec/compose-go/v2/types"
"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 @@ -104,6 +108,40 @@ func createFileLinks(folderAbsolutePath string, wslDollarSign bool, serviceNode
return nil
}

func createVolumeFileLinks(folderAbsolutePath string, wslDollarSign bool, serviceNode *ast.MappingValueNode) []protocol.DocumentLink {
if resolveAnchor(serviceNode.Key).GetToken().Value == "volumes" {
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 {
t := s.GetToken()
config, err := format.ParseVolume(t.Value)
if err == nil && config.Type == composeTypes.VolumeTypeBind {
uri, path := createLocalFileLink(folderAbsolutePath, config.Source, wslDollarSign)
info, err := os.Stat(path)
if err == nil && !info.IsDir() {
links = append(links, protocol.DocumentLink{
Range: createRange(t, len(config.Source)),
Target: types.CreateStringPointer(uri),
Tooltip: types.CreateStringPointer(path),
})
}
}
}
}
return links
}
}
return nil
}

func createLocalFileLink(folderAbsolutePath, fsPath string, wslDollarSign bool) (uri, path string) {
if filepath.IsAbs(fsPath) {
return fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(fsPath), "/")), fsPath
}
return types.Concatenate(folderAbsolutePath, fsPath, wslDollarSign)
}

func createObjectFileLink(folderAbsolutePath string, wslDollarSign bool, serviceNode *ast.MappingValueNode) *protocol.DocumentLink {
if resolveAnchor(serviceNode.Key).GetToken().Value == "file" {
return createFileLink(folderAbsolutePath, wslDollarSign, serviceNode)
Expand Down Expand Up @@ -216,6 +254,9 @@ func scanForLinks(folderAbsolutePath string, wslDollarSign bool, n *ast.MappingV

labelFileLinks := createFileLinks(folderAbsolutePath, wslDollarSign, serviceAttribute, "label_file")
links = append(links, labelFileLinks...)

volumeFileLinks := createVolumeFileLinks(folderAbsolutePath, wslDollarSign, serviceAttribute)
links = append(links, volumeFileLinks...)
}
}
}
Expand Down
112 changes: 112 additions & 0 deletions internal/compose/documentLink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2035,6 +2035,118 @@ services:
}
}

func TestDocumentLink_VolumeFileLinks(t *testing.T) {
tempDir := os.TempDir()
tempFile := filepath.Join(tempDir, "tempFile.txt")
f, err := os.Create(tempFile)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, f.Close())
})
testsFolder := createFileStructure(t)
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: "mount local file",
content: `
services:
test:
volumes:
- ./a.txt:/mount/a.txt`,
path: filepath.Join(testsFolder, "./a.txt"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 8},
End: protocol.Position{Line: 4, Character: 15},
},
},
{
name: "mount local file (string is anchored)",
content: `
services:
test:
volumes:
- &anchor ./a.txt:/mount/a.txt`,
path: filepath.Join(testsFolder, "./a.txt"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 16},
End: protocol.Position{Line: 4, Character: 23},
},
},
{
name: "mount local file (volumes attribute name is anchored)",
content: `
services:
test:
&anchor volumes:
- ./a.txt:/mount/a.txt`,
path: filepath.Join(testsFolder, "./a.txt"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 8},
End: protocol.Position{Line: 4, Character: 15},
},
},
{
name: "mount local file (volumes attribute value is anchored)",
content: `
services:
test:
volumes: &anchor
- ./a.txt:/mount/a.txt`,
path: filepath.Join(testsFolder, "./a.txt"),
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 8},
End: protocol.Position{Line: 4, Character: 15},
},
},
{
name: "mount local folder",
content: `
services:
test:
volumes:
- ./folder:/mount/folder`,
},
{
name: "absolute path",
content: fmt.Sprintf(`
services:
test:
volumes:
- %v:/mount/file.txt`, tempFile),
path: tempFile,
linkRange: protocol.Range{
Start: protocol.Position{Line: 4, Character: 8},
End: protocol.Position{Line: 4, Character: protocol.UInteger(8 + len(tempFile))},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mgr := document.NewDocumentManager()
doc := document.NewComposeDocument(mgr, uri.URI(composeStringURI), 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(filepath.FromSlash(tc.path)),
}
require.Equal(t, []protocol.DocumentLink{link}, links)
}
})
}
}

func TestDocumentLink_ConfigFileLinks(t *testing.T) {
testsFolder := filepath.Join(os.TempDir(), t.Name())
composeStringURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(testsFolder, "compose.yaml")), "/"))
Expand Down
Loading