Skip to content

Commit 7642d50

Browse files
authored
Merge pull request #208 from docker/compose-service-hover-other-file
Support hovering over Compose services in another file
2 parents c80d677 + 04df764 commit 7642d50

File tree

4 files changed

+120
-48
lines changed

4 files changed

+120
-48
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ All notable changes to the Docker Language Server will be documented in this fil
1717
- support navigating to a dependency that is defined in another file ([#190](https://github.com/docker/docker-language-server/issues/190))
1818
- textDocument/hover
1919
- improve hover result by linking to the schema and the online documentation ([#199](https://github.com/docker/docker-language-server/issues/199))
20+
- add support for hovering over service names that are defined in a different file ([#207](https://github.com/docker/docker-language-server/issues/207))
2021
- Bake
2122
- textDocument/publishDiagnostics
2223
- support filtering vulnerability diagnostics with an experimental setting ([#192](https://github.com/docker/docker-language-server/issues/192))

internal/compose/definition.go

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,10 @@ func Definition(ctx context.Context, definitionLinkSupport bool, doc document.Co
3333
}
3434

3535
if definitionRange == nil {
36-
files, _ := doc.IncludedFiles()
37-
fileSearch:
38-
for u, file := range files {
39-
for _, doc := range file.Docs {
40-
if mappingNode, ok := doc.Body.(*ast.MappingNode); ok {
41-
for _, node := range mappingNode.Values {
42-
if s, ok := node.Key.(*ast.StringNode); ok && s.Value == dependency.dependencyType {
43-
for _, service := range node.Value.(*ast.MappingNode).Values {
44-
if s, ok := service.Key.(*ast.StringNode); ok && s.Value == name {
45-
definitionRange = rangeFromToken(s.Token)
46-
targetURI = u
47-
break fileSearch
48-
}
49-
}
50-
}
51-
}
52-
}
53-
}
36+
node, u := dependencyLookup(doc, dependency.dependencyType, name)
37+
if node != nil {
38+
definitionRange = rangeFromToken(node.Key.GetToken())
39+
targetURI = u
5440
}
5541
}
5642

@@ -73,3 +59,23 @@ func Definition(ctx context.Context, definitionLinkSupport bool, doc document.Co
7359
}
7460
return nil, nil
7561
}
62+
63+
func dependencyLookup(doc document.ComposeDocument, dependencyType, name string) (*ast.MappingValueNode, string) {
64+
files, _ := doc.IncludedFiles()
65+
for u, file := range files {
66+
for _, doc := range file.Docs {
67+
if mappingNode, ok := doc.Body.(*ast.MappingNode); ok {
68+
for _, node := range mappingNode.Values {
69+
if s, ok := node.Key.(*ast.StringNode); ok && s.Value == dependencyType {
70+
for _, service := range node.Value.(*ast.MappingNode).Values {
71+
if s, ok := service.Key.(*ast.StringNode); ok && s.Value == name {
72+
return service, u
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}
80+
return nil, ""
81+
}

internal/compose/hover.go

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func Hover(ctx context.Context, params *protocol.HoverParams, doc document.Compo
2727
for _, documentNode := range file.Docs {
2828
if mappingNode, ok := documentNode.Body.(*ast.MappingNode); ok {
2929
nodePath := constructNodePath([]ast.Node{}, mappingNode, int(params.Position.Line+1), int(params.Position.Character+1))
30-
result := serviceHover(mappingNode, nodePath)
30+
result := serviceHover(doc, mappingNode, nodePath)
3131
if result != nil {
3232
return result, nil
3333
}
@@ -40,41 +40,50 @@ func Hover(ctx context.Context, params *protocol.HoverParams, doc document.Compo
4040
return nil, nil
4141
}
4242

43-
func createServiceHover(mappingNode *ast.MappingNode, serviceName string) *protocol.Hover {
43+
func createYamlHover(node *ast.MappingValueNode) *protocol.Hover {
44+
split := strings.Split(node.String(), "\n")
45+
skip := -1
46+
for i := range len(split) {
47+
if skip == -1 {
48+
for j := range len(split[i]) {
49+
if !unicode.IsSpace(rune(split[i][j])) {
50+
skip = j
51+
break
52+
}
53+
}
54+
}
55+
// extra check in case there are empty lines
56+
if len(split[i]) > skip {
57+
split[i] = split[i][skip:]
58+
}
59+
}
60+
return &protocol.Hover{
61+
Contents: protocol.MarkupContent{
62+
Kind: protocol.MarkupKindMarkdown,
63+
Value: fmt.Sprintf("```YAML\n%v\n```", strings.Join(split, "\n")),
64+
},
65+
}
66+
}
67+
68+
func createServiceHover(doc document.ComposeDocument, mappingNode *ast.MappingNode, serviceName string) *protocol.Hover {
4469
for _, node := range mappingNode.Values {
4570
if s, ok := node.Key.(*ast.StringNode); ok && s.Value == "services" {
4671
for _, service := range node.Value.(*ast.MappingNode).Values {
4772
if service.Key.GetToken().Value == serviceName {
48-
split := strings.Split(service.String(), "\n")
49-
skip := -1
50-
for i := range len(split) {
51-
if skip == -1 {
52-
for j := 0; j < len(split[i]); j++ {
53-
if !unicode.IsSpace(rune(split[i][j])) {
54-
skip = j
55-
break
56-
}
57-
}
58-
}
59-
// extra check in case there are empty lines
60-
if len(split[i]) > skip {
61-
split[i] = split[i][skip:]
62-
}
63-
}
64-
return &protocol.Hover{
65-
Contents: protocol.MarkupContent{
66-
Kind: protocol.MarkupKindMarkdown,
67-
Value: fmt.Sprintf("```YAML\n%v\n```", strings.Join(split, "\n")),
68-
},
69-
}
73+
return createYamlHover(service)
7074
}
7175
}
7276
}
7377
}
78+
79+
node, _ := dependencyLookup(doc, "services", serviceName)
80+
if node != nil {
81+
return createYamlHover(node)
82+
}
7483
return nil
7584
}
7685

77-
func serviceHover(mappingNode *ast.MappingNode, nodePath []ast.Node) *protocol.Hover {
86+
func serviceHover(doc document.ComposeDocument, mappingNode *ast.MappingNode, nodePath []ast.Node) *protocol.Hover {
7887
if (len(nodePath) == 4 || len(nodePath) == 5) && nodePath[0].GetToken().Value == "services" {
7988
if nodePath[2].GetToken().Value == "extends" {
8089
serviceName := nodePath[3].GetToken().Value
@@ -89,7 +98,7 @@ func serviceHover(mappingNode *ast.MappingNode, nodePath []ast.Node) *protocol.H
8998
} else if nodePath[3].GetToken().Next != nil && nodePath[3].GetToken().Next.Type == token.MappingValueType {
9099
return nil
91100
}
92-
result := createServiceHover(mappingNode, serviceName)
101+
result := createServiceHover(doc, mappingNode, serviceName)
93102
if result != nil {
94103
return result
95104
}
@@ -102,7 +111,7 @@ func serviceHover(mappingNode *ast.MappingNode, nodePath []ast.Node) *protocol.H
102111
return nil
103112
}
104113
serviceName := nodePath[3].GetToken().Value
105-
result := createServiceHover(mappingNode, serviceName)
114+
result := createServiceHover(doc, mappingNode, serviceName)
106115
if result != nil {
107116
return result
108117
}

internal/compose/hover_test.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -492,13 +492,69 @@ services:
492492
},
493493
}
494494

495-
temporaryBakeFile := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/"))
495+
composeFile := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/"))
496496
for _, tc := range testCases {
497497
t.Run(tc.name, func(t *testing.T) {
498-
doc := document.NewComposeDocument(document.NewDocumentManager(), uri.URI(temporaryBakeFile), 1, []byte(tc.content))
498+
doc := document.NewComposeDocument(document.NewDocumentManager(), uri.URI(composeFile), 1, []byte(tc.content))
499499
result, err := Hover(context.Background(), &protocol.HoverParams{
500500
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
501-
TextDocument: protocol.TextDocumentIdentifier{URI: temporaryBakeFile},
501+
TextDocument: protocol.TextDocumentIdentifier{URI: composeFile},
502+
Position: protocol.Position{Line: tc.line, Character: tc.character},
503+
},
504+
}, doc)
505+
require.NoError(t, err)
506+
require.Equal(t, tc.result, result)
507+
})
508+
}
509+
}
510+
511+
func TestHover_InterFileSupport(t *testing.T) {
512+
testCases := []struct {
513+
name string
514+
content string
515+
otherContent string
516+
line uint32
517+
character uint32
518+
result *protocol.Hover
519+
}{
520+
{
521+
name: "hovering over an extends service as a string",
522+
content: `
523+
include:
524+
- compose.other.yaml
525+
services:
526+
test2:
527+
image: alpine:3.21
528+
extends: test`,
529+
otherContent: `
530+
services:
531+
test:
532+
image: alpine:3.20`,
533+
line: 6,
534+
character: 15,
535+
result: &protocol.Hover{
536+
Contents: protocol.MarkupContent{
537+
Kind: protocol.MarkupKindMarkdown,
538+
Value: "```YAML\n" + `test:
539+
image: alpine:3.20` +
540+
"\n```",
541+
},
542+
},
543+
},
544+
}
545+
546+
composeFile := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/"))
547+
composeOtherFile := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.other.yaml")), "/"))
548+
for _, tc := range testCases {
549+
t.Run(tc.name, func(t *testing.T) {
550+
mgr := document.NewDocumentManager()
551+
changed, err := mgr.Write(context.Background(), uri.URI(composeOtherFile), protocol.DockerComposeLanguage, 1, []byte(tc.otherContent))
552+
require.NoError(t, err)
553+
require.True(t, changed)
554+
doc := document.NewComposeDocument(mgr, uri.URI(composeFile), 1, []byte(tc.content))
555+
result, err := Hover(context.Background(), &protocol.HoverParams{
556+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
557+
TextDocument: protocol.TextDocumentIdentifier{URI: composeFile},
502558
Position: protocol.Position{Line: tc.line, Character: tc.character},
503559
},
504560
}, doc)

0 commit comments

Comments
 (0)