Repository: docker/docker-language-server (language: go)

Issue:
Link the short form syntax of `depends_on` to the defined service for `textDocument/documentHighlight` requests
We should highlight the two instances of `db` and `redis` correctly if we receive a `textDocument/documentHighlight` request. We should also return a single element if the cursor is on `web`.
```YAML
services:
  web:
    build: .
    depends_on:
      - db
      - redis
  redis:
    image: redis
  db:
    image: postgres
```


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cce041e..6a78e5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ All notable changes to the Docker Language Server will be documented in this fil
     - suggest dependent volume names for the `volumes` attribute ([#133](https://github.com/docker/docker-language-server/issues/133))
     - suggest dependent config names for the `configs` attribute ([#134](https://github.com/docker/docker-language-server/issues/134))
     - suggest dependent secret names for the `secrets` attribute ([#135](https://github.com/docker/docker-language-server/issues/135))
+  - textDocument/documentHighlight
+    - support highlighting the short form `depends_on` syntax for services ([#70](https://github.com/docker/docker-language-server/issues/70))
 
 ### Fixed
 
diff --git a/internal/compose/documentHighlight.go b/internal/compose/documentHighlight.go
new file mode 100644
index 0000000..b5f0a0d
--- /dev/null
+++ b/internal/compose/documentHighlight.go
@@ -0,0 +1,134 @@
+package compose
+
+import (
+	"github.com/docker/docker-language-server/internal/pkg/document"
+	"github.com/docker/docker-language-server/internal/tliron/glsp/protocol"
+	"github.com/goccy/go-yaml/ast"
+	"github.com/goccy/go-yaml/token"
+)
+
+func serviceReferences(node *ast.MappingValueNode, dependencyAttributeName string) []*token.Token {
+	if servicesNode, ok := node.Value.(*ast.MappingNode); ok {
+		tokens := []*token.Token{}
+		for _, serviceNode := range servicesNode.Values {
+			if serviceAttributes, ok := serviceNode.Value.(*ast.MappingNode); ok {
+				for _, attributeNode := range serviceAttributes.Values {
+					if attributeNode.Key.GetToken().Value == dependencyAttributeName {
+						if sequenceNode, ok := attributeNode.Value.(*ast.SequenceNode); ok {
+							for _, service := range sequenceNode.Values {
+								tokens = append(tokens, service.GetToken())
+							}
+						}
+					}
+				}
+			}
+		}
+		return tokens
+	}
+	return nil
+}
+
+func declarations(node *ast.MappingValueNode, dependencyType string) []*token.Token {
+	if s, ok := node.Key.(*ast.StringNode); ok && s.Value == dependencyType {
+		if servicesNode, ok := node.Value.(*ast.MappingNode); ok {
+			tokens := []*token.Token{}
+			for _, serviceNode := range servicesNode.Values {
+				tokens = append(tokens, serviceNode.Key.GetToken())
+			}
+			return tokens
+		}
+	}
+	return nil
+}
+
+func DocumentHighlight(doc document.ComposeDocument, position protocol.Position) ([]protocol.DocumentHighlight, error) {
+	file := doc.File()
+	if file == nil || len(file.Docs) == 0 {
+		return nil, nil
+	}
+
+	line := int(position.Line) + 1
+	character := int(position.Character) + 1
+	if mappingNode, ok := file.Docs[0].Body.(*ast.MappingNode); ok {
+		for _, node := range mappingNode.Values {
+			if s, ok := node.Key.(*ast.StringNode); ok {
+				switch s.Value {
+				case "services":
+					refs := serviceReferences(node, "depends_on")
+					decls := declarations(node, "services")
+					highlights := highlightServiceReferences(refs, decls, line, character)
+					if len(highlights) > 0 {
+						return highlights, nil
+					}
+				}
+			}
+		}
+	}
+	return nil, nil
+}
+
+func highlightServiceReferences(refs, decls []*token.Token, line, character int) []protocol.DocumentHighlight {
+	var match *token.Token
+	for _, reference := range refs {
+		if inToken(reference, line, character) {
+			match = reference
+			break
+		}
+	}
+
+	if match == nil {
+		for _, declaration := range decls {
+			if inToken(declaration, line, character) {
+				match = declaration
+				break
+			}
+		}
+	}
+
+	if match != nil {
+		highlights := []protocol.DocumentHighlight{}
+		for _, reference := range refs {
+			if reference.Value == match.Value {
+				highlights = append(highlights, documentHighlightFromToken(reference, protocol.DocumentHighlightKindRead))
+			}
+		}
+
+		for _, declaration := range decls {
+			if declaration.Value == match.Value {
+				highlights = append(highlights, documentHighlightFromToken(declaration, protocol.DocumentHighlightKindWrite))
+			}
+		}
+		return highlights
+	}
+	return nil
+}
+
+func documentHighlightFromToken(t *token.Token, kind protocol.DocumentHighlightKind) protocol.DocumentHighlight {
+	return documentHighlight(
+		protocol.UInteger(t.Position.Line)-1,
+		protocol.UInteger(t.Position.Column)-1,
+		protocol.UInteger(t.Position.Line)-1,
+		protocol.UInteger(t.Position.Column+len(t.Value))-1,
+		kind,
+	)
+}
+
+func documentHighlight(startLine, startCharacter, endLine, endCharacter protocol.UInteger, kind protocol.DocumentHighlightKind) protocol.DocumentHighlight {
+	return protocol.DocumentHighlight{
+		Kind: &kind,
+		Range: protocol.Range{
+			Start: protocol.Position{
+				Line:      startLine,
+				Character: startCharacter,
+			},
+			End: protocol.Position{
+				Line:      endLine,
+				Character: endCharacter,
+			},
+		},
+	}
+}
+
+func inToken(t *token.Token, line, character int) bool {
+	return t.Position.Line == line && t.Position.Column <= character && character <= t.Position.Column+len(t.Value)
+}
diff --git a/internal/pkg/server/documentHighlight.go b/internal/pkg/server/documentHighlight.go
index c85881a..27007fd 100644
--- a/internal/pkg/server/documentHighlight.go
+++ b/internal/pkg/server/documentHighlight.go
@@ -2,6 +2,7 @@ package server
 
 import (
 	"github.com/docker/docker-language-server/internal/bake/hcl"
+	"github.com/docker/docker-language-server/internal/compose"
 	"github.com/docker/docker-language-server/internal/pkg/document"
 	"github.com/docker/docker-language-server/internal/tliron/glsp"
 	"github.com/docker/docker-language-server/internal/tliron/glsp/protocol"
@@ -16,6 +17,8 @@ func (s *Server) TextDocumentDocumentHighlight(ctx *glsp.Context, params *protoc
 	defer doc.Close()
 	if doc.LanguageIdentifier() == protocol.DockerBakeLanguage {
 		return hcl.DocumentHighlight(doc.(document.BakeHCLDocument), params.Position)
+	} else if doc.LanguageIdentifier() == protocol.DockerComposeLanguage {
+		return compose.DocumentHighlight(doc.(document.ComposeDocument), params.Position)
 	}
 	return nil, nil
 }


Test patch:
diff --git a/internal/compose/documentHighlight_test.go b/internal/compose/documentHighlight_test.go
new file mode 100644
index 0000000..bffa672
--- /dev/null
+++ b/internal/compose/documentHighlight_test.go
@@ -0,0 +1,102 @@
+package compose
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker-language-server/internal/pkg/document"
+	"github.com/docker/docker-language-server/internal/tliron/glsp/protocol"
+	"github.com/stretchr/testify/require"
+	"go.lsp.dev/uri"
+)
+
+func TestDocumentHighlight_Services(t *testing.T) {
+	testCases := []struct {
+		name      string
+		content   string
+		line      protocol.UInteger
+		character protocol.UInteger
+		ranges    []protocol.DocumentHighlight
+	}{
+		{
+			name: "write highlight on a service",
+			content: `
+services:
+  test:`,
+			line:      2,
+			character: 4,
+			ranges: []protocol.DocumentHighlight{
+				documentHighlight(2, 2, 2, 6, protocol.DocumentHighlightKindWrite),
+			},
+		},
+		{
+			name: "read highlight on an undefined service array item",
+			content: `
+services:
+  test:
+    depends_on:
+      - test2`,
+			line:      4,
+			character: 10,
+			ranges: []protocol.DocumentHighlight{
+				documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead),
+			},
+		},
+		{
+			name: "cursor not on anything meaningful",
+			content: `
+services:
+  test:
+    depends_on:
+      - test2
+      - test2`,
+			line:      3,
+			character: 9,
+			ranges:    nil,
+		},
+		{
+			name: "read highlight on an undefined service array item, duplicated",
+			content: `
+services:
+  test:
+    depends_on:
+      - test2
+      - test2`,
+			line:      4,
+			character: 10,
+			ranges: []protocol.DocumentHighlight{
+				documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead),
+				documentHighlight(5, 8, 5, 13, protocol.DocumentHighlightKindRead),
+			},
+		},
+		{
+			name: "read/write highlight on a service array item",
+			content: `
+services:
+  test:
+    depends_on:
+      - test2
+  test2:`,
+			line:      5,
+			character: 5,
+			ranges: []protocol.DocumentHighlight{
+				documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead),
+				documentHighlight(5, 2, 5, 7, protocol.DocumentHighlightKindWrite),
+			},
+		},
+	}
+
+	composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/"))
+	u := uri.URI(composeFileURI)
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			doc := document.NewComposeDocument(u, 1, []byte(tc.content))
+			ranges, err := DocumentHighlight(doc, protocol.Position{Line: tc.line, Character: tc.character})
+			require.NoError(t, err)
+			require.Equal(t, tc.ranges, ranges)
+		})
+	}
+}