diff --git a/CHANGELOG.md b/CHANGELOG.md index bd0db77..5067401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/internal/compose/documentLink.go b/internal/compose/documentLink.go index 2afd549..0bb479c 100644 --- a/internal/compose/documentLink.go +++ b/internal/compose/documentLink.go @@ -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) @@ -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...) } } } diff --git a/internal/compose/documentLink_test.go b/internal/compose/documentLink_test.go index a8cc017..f691046 100644 --- a/internal/compose/documentLink_test.go +++ b/internal/compose/documentLink_test.go @@ -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())