diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf56bb..90a0f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ All notable changes to the Docker Language Server will be documented in this fil - textDocument/documentLink - improve handling of malformed image attribute values with registry prefixes ([#369](https://github.com/docker/docker-language-server/issues/369)) - convert links properly if a WSL URI with a dollar sign is used ([#366](https://github.com/docker/docker-language-server/issues/366)) +- Bake + - textDocument/documentLink + - convert links properly if a WSL URI with a dollar sign is used ([#378](https://github.com/docker/docker-language-server/issues/378)) ## [0.14.0] - 2025-07-16 diff --git a/internal/bake/hcl/documentLink.go b/internal/bake/hcl/documentLink.go index 9f7efb2..fb3023b 100644 --- a/internal/bake/hcl/documentLink.go +++ b/internal/bake/hcl/documentLink.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "net/url" - "path/filepath" "strings" "github.com/docker/docker-language-server/internal/pkg/document" @@ -19,7 +17,8 @@ func DocumentLink(ctx context.Context, documentURI protocol.URI, document docume if !ok { return nil, errors.New("unrecognized body in HCL document") } - url, err := url.Parse(string(documentURI)) + + d, err := document.DocumentPath() if err != nil { return nil, fmt.Errorf("LSP client sent invalid URI: %v", string(documentURI)) } @@ -37,17 +36,15 @@ func DocumentLink(ctx context.Context, documentURI protocol.URI, document docume dockerfilePath = strings.TrimPrefix(dockerfilePath, "\"") dockerfilePath = strings.TrimSuffix(dockerfilePath, "\"") - dockerfilePath, err = types.AbsolutePath(url, dockerfilePath) - if err == nil { - links = append(links, protocol.DocumentLink{ - Range: protocol.Range{ - Start: protocol.Position{Line: uint32(v.SrcRange.Start.Line) - 1, Character: uint32(v.Expr.Range().Start.Column)}, - End: protocol.Position{Line: uint32(v.SrcRange.Start.Line) - 1, Character: uint32(v.Expr.Range().End.Column - 2)}, - }, - Target: types.CreateStringPointer(protocol.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(dockerfilePath), "/")))), - Tooltip: types.CreateStringPointer(dockerfilePath), - }) - } + target, tooltip := types.Concatenate(d.Folder, dockerfilePath, d.WSLDollarSignHost) + links = append(links, protocol.DocumentLink{ + Range: protocol.Range{ + Start: protocol.Position{Line: uint32(v.SrcRange.Start.Line) - 1, Character: uint32(v.Expr.Range().Start.Column)}, + End: protocol.Position{Line: uint32(v.SrcRange.Start.Line) - 1, Character: uint32(v.Expr.Range().End.Column - 2)}, + }, + Target: types.CreateStringPointer(target), + Tooltip: types.CreateStringPointer(tooltip), + }) } } } diff --git a/internal/bake/hcl/documentLink_test.go b/internal/bake/hcl/documentLink_test.go index 998d5cd..de204b6 100644 --- a/internal/bake/hcl/documentLink_test.go +++ b/internal/bake/hcl/documentLink_test.go @@ -99,3 +99,61 @@ func TestDocumentLink(t *testing.T) { }) } } + +func TestDocumentLink_WSL(t *testing.T) { + testCases := []struct { + name string + content string + target string + tooltip string + linkRange protocol.Range + links func(path string) []protocol.DocumentLink + }{ + { + name: "Dockerfile", + content: "target \"api\" {\n dockerfile = \"Dockerfile\"\n}", + target: "file://wsl%24/docker-desktop/tmp/Dockerfile", + tooltip: "\\\\wsl%24\\docker-desktop\\tmp\\Dockerfile", + linkRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 16}, + End: protocol.Position{Line: 1, Character: 26}, + }, + }, + { + name: "./Dockerfile", + content: "target \"api\" {\n dockerfile = \"./Dockerfile\"\n}", + target: "file://wsl%24/docker-desktop/tmp/Dockerfile", + tooltip: "\\\\wsl%24\\docker-desktop\\tmp\\Dockerfile", + linkRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 16}, + End: protocol.Position{Line: 1, Character: 28}, + }, + }, + { + name: "../other/Dockerfile", + content: "target \"api\" {\n dockerfile = \"../other/Dockerfile\"\n}", + target: "file://wsl%24/docker-desktop/other/Dockerfile", + tooltip: "\\\\wsl%24\\docker-desktop\\other\\Dockerfile", + linkRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 16}, + End: protocol.Position{Line: 1, Character: 35}, + }, + }, + } + + documentStringURI := "file://wsl%24/docker-desktop/tmp/docker-bake.hcl" + documentURI := uri.URI(documentStringURI) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + doc := document.NewBakeHCLDocument(documentURI, 1, []byte(tc.content)) + links, err := DocumentLink(context.Background(), documentStringURI, doc) + require.NoError(t, err) + link := protocol.DocumentLink{ + Range: tc.linkRange, + Target: types.CreateStringPointer(tc.target), + Tooltip: types.CreateStringPointer(tc.tooltip), + } + require.Equal(t, []protocol.DocumentLink{link}, links) + }) + } +} diff --git a/internal/compose/documentLink.go b/internal/compose/documentLink.go index 2bc90de..baece18 100644 --- a/internal/compose/documentLink.go +++ b/internal/compose/documentLink.go @@ -3,9 +3,6 @@ package compose import ( "context" "fmt" - "net/url" - "path" - "path/filepath" "strings" "github.com/docker/docker-language-server/internal/pkg/document" @@ -34,18 +31,11 @@ func createRange(t *token.Token, length int) protocol.Range { func createLink(folderAbsolutePath string, wslDollarSign bool, node *token.Token) *protocol.DocumentLink { file := node.Value - if wslDollarSign { - return &protocol.DocumentLink{ - Range: createRange(node, len(file)), - Target: types.CreateStringPointer("file://wsl%24" + path.Join(strings.ReplaceAll(folderAbsolutePath, "\\", "/"), file)), - Tooltip: types.CreateStringPointer("\\\\wsl%24" + strings.ReplaceAll(path.Join(folderAbsolutePath, file), "/", "\\")), - } - } - abs := filepath.ToSlash(filepath.Join(folderAbsolutePath, file)) + u, path := types.Concatenate(folderAbsolutePath, file, wslDollarSign) return &protocol.DocumentLink{ Range: createRange(node, len(file)), - Target: types.CreateStringPointer(protocol.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(abs, "/")))), - Tooltip: types.CreateStringPointer(filepath.FromSlash(abs)), + Target: types.CreateStringPointer(u), + Tooltip: types.CreateStringPointer(path), } } @@ -264,22 +254,8 @@ func scanForLinks(folderAbsolutePath string, wslDollarSign bool, n *ast.MappingV return nil } -func documentFolder(documentURI protocol.URI) (string, bool, error) { - url, err := url.Parse(string(documentURI)) - if err != nil { - if strings.HasPrefix(documentURI, "file://wsl%24/") { - path := documentURI[len("file://wsl%24"):] - idx := strings.LastIndex(path, "/") - return path[0 : idx+1], true, nil - } - return "", false, fmt.Errorf("LSP client sent invalid URI: %v", string(documentURI)) - } - folder, err := types.AbsoluteFolder(url) - return folder, false, err -} - func DocumentLink(ctx context.Context, documentURI protocol.URI, doc document.ComposeDocument) ([]protocol.DocumentLink, error) { - abs, wslDollarSign, err := documentFolder(documentURI) + d, err := doc.DocumentPath() if err != nil { return nil, err } @@ -293,7 +269,7 @@ func DocumentLink(ctx context.Context, documentURI protocol.URI, doc document.Co for _, documentNode := range file.Docs { if mappingNode, ok := documentNode.Body.(*ast.MappingNode); ok { for _, node := range mappingNode.Values { - links = append(links, scanForLinks(abs, wslDollarSign, node)...) + links = append(links, scanForLinks(d.Folder, d.WSLDollarSignHost, node)...) } } } diff --git a/internal/compose/documentLink_test.go b/internal/compose/documentLink_test.go index 0e25f15..07b348c 100644 --- a/internal/compose/documentLink_test.go +++ b/internal/compose/documentLink_test.go @@ -12,6 +12,7 @@ import ( "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" ) func documentLinkTooltip(testsFolder, fileName string) *string { @@ -69,7 +70,7 @@ func TestDocumentLink_WSL(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - doc := document.NewComposeDocument(document.NewDocumentManager(), "compose.yaml", 1, []byte(tc.content)) + doc := document.NewComposeDocument(document.NewDocumentManager(), uri.URI(composeStringURI), 1, []byte(tc.content)) links, err := DocumentLink(context.Background(), composeStringURI, doc) require.NoError(t, err) require.Equal(t, tc.links, links) @@ -385,7 +386,7 @@ include: for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - doc := document.NewComposeDocument(document.NewDocumentManager(), "docker-compose.yml", 1, []byte(tc.content)) + doc := document.NewComposeDocument(document.NewDocumentManager(), uri.URI(composeStringURI), 1, []byte(tc.content)) links, err := DocumentLink(context.Background(), composeStringURI, doc) require.NoError(t, err) require.Equal(t, tc.links, links) @@ -1158,7 +1159,7 @@ services: 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)) + 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 == "" { @@ -1332,7 +1333,7 @@ services: 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)) + 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 == "" { @@ -1494,7 +1495,7 @@ services: 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)) + 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 == "" { @@ -1707,7 +1708,7 @@ services: 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)) + 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 == "" { @@ -1834,7 +1835,7 @@ configs: 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)) + 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 == "" { @@ -1961,7 +1962,7 @@ secrets: 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)) + 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 == "" { diff --git a/internal/pkg/document/bakeDocument.go b/internal/pkg/document/bakeDocument.go index 8d16fa5..a4692e3 100644 --- a/internal/pkg/document/bakeDocument.go +++ b/internal/pkg/document/bakeDocument.go @@ -180,9 +180,14 @@ func (d *bakeHCLDocument) extractBakeOutput() { } } + dd, err := d.DocumentPath() + if err != nil { + d.bakePrintOutput = nil + return + } btargets, groups, err := bake.ReadTargets( context.Background(), - []bake.File{{Name: d.uri.Filename(), Data: d.Input()}}, + []bake.File{{Name: dd.FileName, Data: d.Input()}}, targets, nil, nil, diff --git a/internal/pkg/document/document.go b/internal/pkg/document/document.go index 7373a46..14efc5c 100644 --- a/internal/pkg/document/document.go +++ b/internal/pkg/document/document.go @@ -1,12 +1,24 @@ package document import ( + "fmt" + "net/url" + "strings" + "github.com/docker/docker-language-server/internal/tliron/glsp/protocol" + "github.com/docker/docker-language-server/internal/types" "go.lsp.dev/uri" ) +type DocumentPath struct { + Folder string + FileName string + WSLDollarSignHost bool +} + type Document interface { URI() uri.URI + DocumentPath() (DocumentPath, error) Copy() Document Input() []byte Version() int32 @@ -56,6 +68,22 @@ func (d *document) URI() uri.URI { return d.uri } +func (d *document) DocumentPath() (DocumentPath, error) { + uriString := string(d.uri) + url, err := url.Parse(uriString) + if err != nil { + if strings.HasPrefix(uriString, "file://wsl%24/") { + path := uriString[len("file://wsl%24"):] + idx := strings.LastIndex(path, "/") + return DocumentPath{Folder: path[0:idx], FileName: path[idx+1:], WSLDollarSignHost: true}, nil + } + return DocumentPath{}, fmt.Errorf("Invalid URI: %v", uriString) + } + folder, err := types.AbsoluteFolder(url) + idx := strings.LastIndex(uriString, "/") + return DocumentPath{Folder: folder, FileName: uriString[idx+1:]}, err +} + func (d *document) LanguageIdentifier() protocol.LanguageIdentifier { return d.identifier } diff --git a/internal/pkg/document/document_test.go b/internal/pkg/document/document_test.go index 7e6a2fe..09d4e33 100644 --- a/internal/pkg/document/document_test.go +++ b/internal/pkg/document/document_test.go @@ -2,6 +2,7 @@ package document import ( "context" + "runtime" "testing" "github.com/docker/docker-language-server/internal/tliron/glsp/protocol" @@ -116,3 +117,103 @@ func TestGetDecoder(t *testing.T) { }) } } + +func TestDocumentPath(t *testing.T) { + testCases := []struct { + name string + u uri.URI + folder string + fileName string + wsl bool + }{ + { + name: "Windows wsl$ host URI", + u: "file://wsl%24/docker-desktop/tmp/Dockerfile", + folder: "/docker-desktop/tmp", + fileName: "Dockerfile", + wsl: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mgr := NewDocumentManager() + document := NewDocument(mgr, tc.u, protocol.DockerfileLanguage, 1, []byte{}) + path, err := document.DocumentPath() + require.NoError(t, err) + require.Equal(t, tc.folder, path.Folder) + require.Equal(t, tc.fileName, path.FileName) + require.Equal(t, tc.wsl, path.WSLDollarSignHost) + }) + } +} + +func TestDocumentPathNonWindows(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + return + } + + testCases := []struct { + name string + u uri.URI + folder string + fileName string + wsl bool + }{ + { + name: "Linux URI", + u: "file:///tmp/Dockerfile", + folder: "/tmp", + fileName: "Dockerfile", + wsl: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mgr := NewDocumentManager() + document := NewDocument(mgr, tc.u, protocol.DockerfileLanguage, 1, []byte{}) + path, err := document.DocumentPath() + require.NoError(t, err) + require.Equal(t, tc.folder, path.Folder) + require.Equal(t, tc.fileName, path.FileName) + require.Equal(t, tc.wsl, path.WSLDollarSignHost) + }) + } +} + +func TestDocumentPath_Windows(t *testing.T) { + if runtime.GOOS != "windows" { + t.SkipNow() + return + } + + testCases := []struct { + name string + u uri.URI + folder string + fileName string + wsl bool + }{ + { + name: "Windows c%3A URI", + u: "file:///c%3A/tmp/Dockerfile", + folder: "c:\\tmp", + fileName: "Dockerfile", + wsl: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mgr := NewDocumentManager() + document := NewDocument(mgr, tc.u, protocol.DockerfileLanguage, 1, []byte{}) + path, err := document.DocumentPath() + require.NoError(t, err) + require.Equal(t, tc.folder, path.Folder) + require.Equal(t, tc.fileName, path.FileName) + require.Equal(t, tc.wsl, path.WSLDollarSignHost) + }) + } +} diff --git a/internal/types/common.go b/internal/types/common.go index 4b30cae..d680642 100644 --- a/internal/types/common.go +++ b/internal/types/common.go @@ -3,6 +3,7 @@ package types import ( "fmt" "net/url" + "path" "path/filepath" "runtime" "strings" @@ -97,6 +98,14 @@ func AbsoluteFolder(documentURL *url.URL) (string, error) { return filepath.Abs(filepath.Dir(documentPath)) } +func Concatenate(folder, file string, wslDollarSign bool) (uri string, absoluteFilePath string) { + if wslDollarSign { + return "file://wsl%24" + path.Join(strings.ReplaceAll(folder, "\\", "/"), file), "\\\\wsl%24" + strings.ReplaceAll(path.Join(folder, file), "/", "\\") + } + abs := filepath.ToSlash(filepath.Join(folder, file)) + return fmt.Sprintf("file:///%v", strings.TrimPrefix(abs, "/")), filepath.FromSlash(abs) +} + func CreateDefinitionResult(definitionLinkSupport bool, targetRange protocol.Range, originSelectionRange *protocol.Range, linkURI protocol.URI) any { if !definitionLinkSupport { return []protocol.Location{