Skip to content

Commit eb56b14

Browse files
authored
Merge pull request #201 from docker/compose-code-completion-panic-fix
Fix code completion panic in an empty Compose file
2 parents 86ceb70 + 4bdaa61 commit eb56b14

File tree

3 files changed

+170
-56
lines changed

3 files changed

+170
-56
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ All notable changes to the Docker Language Server will be documented in this fil
2323

2424
### Fixed
2525

26+
- Compose
27+
- textDocument/completion
28+
- fix panic in code completion in an empty file ([#196](https://github.com/docker/docker-language-server/issues/196))
2629
- Bake
2730
- textDocument/publishDiagnostics
2831
- stop flagging `BUILDKIT_SYNTAX` as an unrecognized `ARG` ([#187](https://github.com/docker/docker-language-server/issues/187))

internal/compose/completion.go

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -108,42 +108,58 @@ func array(line string, character int) bool {
108108
return isArray
109109
}
110110

111+
func createTopLevelItems() []protocol.CompletionItem {
112+
items := []protocol.CompletionItem{}
113+
for attributeName, schema := range schemaProperties() {
114+
item := protocol.CompletionItem{Label: attributeName}
115+
if schema.Description != "" {
116+
item.Documentation = schema.Description
117+
}
118+
items = append(items, item)
119+
}
120+
slices.SortFunc(items, func(a, b protocol.CompletionItem) int {
121+
return strings.Compare(a.Label, b.Label)
122+
})
123+
return items
124+
}
125+
126+
func calculateTopLevelNodeOffset(file *ast.File) int {
127+
if len(file.Docs) == 1 {
128+
if m, ok := file.Docs[0].Body.(*ast.MappingNode); ok {
129+
return m.Values[0].Key.GetToken().Position.Column - 1
130+
}
131+
}
132+
return -1
133+
}
134+
111135
func Completion(ctx context.Context, params *protocol.CompletionParams, manager *document.Manager, doc document.ComposeDocument) (*protocol.CompletionList, error) {
112136
u, err := url.Parse(params.TextDocument.URI)
113137
if err != nil {
114138
return nil, fmt.Errorf("LSP client sent invalid URI: %v", params.TextDocument.URI)
115139
}
116140

117-
if params.Position.Character == 0 {
118-
items := []protocol.CompletionItem{}
119-
for attributeName, schema := range schemaProperties() {
120-
item := protocol.CompletionItem{Label: attributeName}
121-
if schema.Description != "" {
122-
item.Documentation = schema.Description
123-
}
124-
items = append(items, item)
125-
}
126-
slices.SortFunc(items, func(a, b protocol.CompletionItem) int {
127-
return strings.Compare(a.Label, b.Label)
128-
})
129-
return &protocol.CompletionList{Items: items}, nil
141+
file := doc.File()
142+
if file == nil || len(file.Docs) == 0 {
143+
return nil, nil
130144
}
131145

132-
lines := strings.Split(string(doc.Input()), "\n")
133146
lspLine := int(params.Position.Line)
134-
if strings.HasPrefix(strings.TrimSpace(lines[lspLine]), "#") {
135-
return nil, nil
147+
topLevelNodeOffset := calculateTopLevelNodeOffset(file)
148+
if topLevelNodeOffset != -1 && params.Position.Character == uint32(topLevelNodeOffset) {
149+
return &protocol.CompletionList{Items: createTopLevelItems()}, nil
136150
}
137151

138-
file := doc.File()
139-
if file == nil || len(file.Docs) == 0 {
152+
lines := strings.Split(string(doc.Input()), "\n")
153+
if strings.HasPrefix(strings.TrimSpace(lines[lspLine]), "#") {
140154
return nil, nil
141155
}
142156

143157
line := int(lspLine) + 1
144158
character := int(params.Position.Character) + 1
145159
path := constructCompletionNodePath(file, line)
146-
if len(path) == 1 {
160+
if len(path) == 0 {
161+
return &protocol.CompletionList{Items: createTopLevelItems()}, nil
162+
} else if len(path) == 1 {
147163
return nil, nil
148164
} else if path[1].Key.GetToken().Position.Column >= character {
149165
return nil, nil
@@ -452,9 +468,19 @@ func namedDependencyCompletionItems(file *ast.File, path []*ast.MappingValueNode
452468
}
453469

454470
func constructCompletionNodePath(file *ast.File, line int) []*ast.MappingValueNode {
455-
for _, documentNode := range file.Docs {
456-
if mappingNode, ok := documentNode.Body.(*ast.MappingNode); ok {
457-
return NodeStructure(line, mappingNode.Values)
471+
for i := range len(file.Docs) {
472+
if i+1 == len(file.Docs) {
473+
if mappingNode, ok := file.Docs[i].Body.(*ast.MappingNode); ok {
474+
return NodeStructure(line, mappingNode.Values)
475+
}
476+
}
477+
478+
if m, ok := file.Docs[i].Body.(*ast.MappingNode); ok {
479+
if n, ok := file.Docs[i+1].Body.(*ast.MappingNode); ok {
480+
if m.Values[0].Key.GetToken().Position.Line <= line && line <= n.Values[0].Key.GetToken().Position.Line {
481+
return NodeStructure(line, m.Values)
482+
}
483+
}
458484
}
459485
}
460486
return nil

internal/compose/completion_test.go

Lines changed: 119 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,41 @@ import (
1515
"go.lsp.dev/uri"
1616
)
1717

18+
var topLevelNodes = []protocol.CompletionItem{
19+
{
20+
Label: "configs",
21+
Documentation: "Configurations that are shared among multiple services.",
22+
},
23+
{
24+
Label: "include",
25+
Documentation: "compose sub-projects to be included.",
26+
},
27+
{
28+
Label: "name",
29+
Documentation: "define the Compose project name, until user defines one explicitly.",
30+
},
31+
{
32+
Label: "networks",
33+
Documentation: "Networks that are shared among multiple services.",
34+
},
35+
{
36+
Label: "secrets",
37+
Documentation: "Secrets that are shared among multiple services.",
38+
},
39+
{
40+
Label: "services",
41+
Documentation: "The services that will be used by your application.",
42+
},
43+
{
44+
Label: "version",
45+
Documentation: "declared for backward compatibility, ignored. Please remove it.",
46+
},
47+
{
48+
Label: "volumes",
49+
Documentation: "Named volumes that are shared among multiple services.",
50+
},
51+
}
52+
1853
func serviceProperties(line, character, prefixLength protocol.UInteger) []protocol.CompletionItem {
1954
return []protocol.CompletionItem{
2055
{
@@ -934,40 +969,90 @@ configs:
934969
line: 3,
935970
character: 0,
936971
list: &protocol.CompletionList{
937-
Items: []protocol.CompletionItem{
938-
{
939-
Label: "configs",
940-
Documentation: "Configurations that are shared among multiple services.",
941-
},
942-
{
943-
Label: "include",
944-
Documentation: "compose sub-projects to be included.",
945-
},
946-
{
947-
Label: "name",
948-
Documentation: "define the Compose project name, until user defines one explicitly.",
949-
},
950-
{
951-
Label: "networks",
952-
Documentation: "Networks that are shared among multiple services.",
953-
},
954-
{
955-
Label: "secrets",
956-
Documentation: "Secrets that are shared among multiple services.",
957-
},
958-
{
959-
Label: "services",
960-
Documentation: "The services that will be used by your application.",
961-
},
962-
{
963-
Label: "version",
964-
Documentation: "declared for backward compatibility, ignored. Please remove it.",
965-
},
966-
{
967-
Label: "volumes",
968-
Documentation: "Named volumes that are shared among multiple services.",
969-
},
970-
},
972+
Items: topLevelNodes,
973+
},
974+
},
975+
{
976+
name: "top level node suggestions with a space in the front",
977+
content: ` `,
978+
line: 0,
979+
character: 1,
980+
list: &protocol.CompletionList{
981+
Items: topLevelNodes,
982+
},
983+
},
984+
{
985+
name: "top level node suggestions with indented content but code completion is unindented",
986+
content: `
987+
configs:
988+
test:
989+
`,
990+
line: 3,
991+
character: 0,
992+
list: nil,
993+
},
994+
{
995+
name: "top level node suggestions with indented content and code completion is aligned correctly",
996+
content: `
997+
configs:
998+
test:
999+
`,
1000+
line: 3,
1001+
character: 1,
1002+
list: &protocol.CompletionList{
1003+
Items: topLevelNodes,
1004+
},
1005+
},
1006+
{
1007+
name: "alignment correct with multiple documents",
1008+
content: `
1009+
---
1010+
---
1011+
configs:
1012+
test:
1013+
`,
1014+
line: 5,
1015+
character: 1,
1016+
list: &protocol.CompletionList{
1017+
Items: topLevelNodes,
1018+
},
1019+
},
1020+
{
1021+
name: "alignment incorrect with multiple documents",
1022+
content: `
1023+
---
1024+
configs:
1025+
test:
1026+
---
1027+
configs:
1028+
test2:
1029+
`,
1030+
line: 7,
1031+
character: 0,
1032+
list: nil,
1033+
},
1034+
{
1035+
name: "top level node suggestions with indented content and code completion is aligned correctly but in a comment",
1036+
content: `
1037+
configs:
1038+
test:
1039+
#`,
1040+
line: 3,
1041+
character: 1,
1042+
list: nil,
1043+
},
1044+
{
1045+
name: "top level node suggestions with multiple files",
1046+
content: `
1047+
---
1048+
configs:
1049+
test:
1050+
---
1051+
`,
1052+
line: 5,
1053+
character: 0,
1054+
list: &protocol.CompletionList{
1055+
Items: topLevelNodes,
9711056
},
9721057
},
9731058
{

0 commit comments

Comments
 (0)