Skip to content

Commit cd666e3

Browse files
committed
Add support for formatting Compose YAML files
This feature will not work if the file has clear, syntactical errors that prevents goccy/go-yaml from parsing the file. Signed-off-by: Remy Suen <[email protected]>
1 parent b53eaad commit cd666e3

File tree

6 files changed

+577
-5
lines changed

6 files changed

+577
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ All notable changes to the Docker Language Server will be documented in this fil
1212
- improve code completion by automatically including required attributes in completion items ([#155](https://github.com/docker/docker-language-server/issues/155))
1313
- textDocument/inlayHint
1414
- show the parent service's value if it is being overridden and they are not object attributes ([#156](https://github.com/docker/docker-language-server/issues/156))
15+
- textDocument/formatting
16+
- add support to format YAML files that do not have clear syntactical errors ([#165](https://github.com/docker/docker-language-server/issues/165))
1517

1618
### Fixed
1719

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ The Docker Language Server relies on some features that are dependent on [Buildx
1616
- code completion
1717
- code navigation
1818
- document outline support
19+
- formatting
1920
- highlight named references of services, networks, volumes, configs, and secrets
2021
- hover tooltips
22+
- inlay hints for overridden attribute values
2123
- open links to images
2224
- rename preparation
2325
- rename named references
2426
- Bake files
2527
- code completion
26-
- inferring variable values
27-
- formatting
2828
- code navigation
29+
- document outline support
30+
- formatting
2931
- hover tooltips
32+
- inferring variable values
3033

3134
## Installing
3235

internal/compose/formatting.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package compose
2+
3+
import (
4+
"strings"
5+
6+
"github.com/docker/docker-language-server/internal/pkg/document"
7+
"github.com/docker/docker-language-server/internal/tliron/glsp/protocol"
8+
"github.com/sourcegraph/jsonrpc2"
9+
)
10+
11+
type indentation struct {
12+
original int
13+
desired int
14+
}
15+
16+
type comment struct {
17+
line int
18+
whitespace int
19+
}
20+
21+
func formattingOptionTabSize(options protocol.FormattingOptions) (int, error) {
22+
if tabSize, ok := options[protocol.FormattingOptionTabSize].(float64); ok && tabSize > 0 {
23+
return int(tabSize), nil
24+
}
25+
return -1, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams, Message: "tabSize is not a positive integer"}
26+
}
27+
28+
func indent(indentation int) string {
29+
sb := strings.Builder{}
30+
for range indentation {
31+
sb.WriteString(" ")
32+
}
33+
return sb.String()
34+
}
35+
36+
func Formatting(doc document.ComposeDocument, options protocol.FormattingOptions) ([]protocol.TextEdit, error) {
37+
file := doc.File()
38+
if file == nil || len(file.Docs) == 0 {
39+
return nil, nil
40+
}
41+
tabSize, err := formattingOptionTabSize(options)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
edits := []protocol.TextEdit{}
47+
indentations := []indentation{}
48+
comments := []comment{}
49+
topLevelNodeDetected := false
50+
lines := strings.Split(string(doc.Input()), "\n")
51+
lineCheck:
52+
for lineNumber, line := range lines {
53+
lineIndentation := 0
54+
stop := 0
55+
isComment := false
56+
empty := true
57+
for i := range len(line) {
58+
if line[i] == 32 {
59+
lineIndentation++
60+
} else if line[i] == '#' {
61+
empty = false
62+
isComment = true
63+
comments = append(comments, comment{line: lineNumber, whitespace: i})
64+
break
65+
} else {
66+
empty = false
67+
if strings.HasPrefix(lines[lineNumber], "---") {
68+
edits = append(edits, formatComments(comments, 0)...)
69+
comments = nil
70+
indentations = nil
71+
topLevelNodeDetected = false
72+
continue lineCheck
73+
}
74+
75+
if !topLevelNodeDetected {
76+
topLevelNodeDetected = true
77+
if lineIndentation > 0 {
78+
newIndentation, _ := updateIndentation(indentations, lineIndentation, 0)
79+
indentations = append(indentations, newIndentation)
80+
}
81+
}
82+
break
83+
}
84+
stop++
85+
}
86+
87+
if isComment {
88+
continue
89+
}
90+
91+
if lineIndentation != 0 {
92+
newIndentation, resetIndex := updateIndentation(indentations, lineIndentation, tabSize)
93+
if resetIndex == -1 {
94+
indentations = append(indentations, newIndentation)
95+
} else {
96+
indentations = indentations[:resetIndex+1]
97+
}
98+
edits = append(edits, formatComments(comments, newIndentation.desired)...)
99+
comments = nil
100+
if lineIndentation != newIndentation.desired {
101+
edits = append(edits, protocol.TextEdit{
102+
NewText: indent(newIndentation.desired),
103+
Range: protocol.Range{
104+
Start: protocol.Position{Line: protocol.UInteger(lineNumber), Character: 0},
105+
End: protocol.Position{Line: protocol.UInteger(lineNumber), Character: protocol.UInteger(stop)},
106+
},
107+
})
108+
}
109+
} else if !empty {
110+
edits = append(edits, formatComments(comments, 0)...)
111+
comments = nil
112+
indentations = nil
113+
}
114+
}
115+
return edits, nil
116+
}
117+
118+
// formatComments goes over the list of comments and corrects its
119+
// indentation to the desired indentation only if it differs. Any
120+
// comment that needs to have its indentation changed will have a
121+
// TextEdit created for it and included in the returned result.
122+
func formatComments(comments []comment, desired int) []protocol.TextEdit {
123+
edits := []protocol.TextEdit{}
124+
for _, c := range comments {
125+
if desired != c.whitespace {
126+
edits = append(edits, protocol.TextEdit{
127+
NewText: indent(desired),
128+
Range: protocol.Range{
129+
Start: protocol.Position{Line: protocol.UInteger(c.line), Character: 0},
130+
End: protocol.Position{Line: protocol.UInteger(c.line), Character: protocol.UInteger(c.whitespace)},
131+
},
132+
})
133+
}
134+
}
135+
return edits
136+
}
137+
138+
func updateIndentation(indentations []indentation, original, tabSpacing int) (indentation, int) {
139+
last := tabSpacing
140+
for i := range indentations {
141+
if indentations[i].original == original {
142+
return indentations[i], i
143+
}
144+
last = indentations[i].desired + tabSpacing
145+
}
146+
return indentation{
147+
original: original,
148+
desired: last,
149+
}, -1
150+
}

0 commit comments

Comments
 (0)