wip - comments

This commit is contained in:
Mike Farah 2025-12-08 10:33:06 +11:00
parent 491ab8e70f
commit 5c166e3038
4 changed files with 100 additions and 51 deletions

View File

@ -1,9 +1,8 @@
service "cat" { # Arithmetic with literals and application-provided variables
process "main" { sum = 1 + addend
command = ["/usr/local/bin/awesome-app", "server"]
}
process "mgmt" { # String interpolation and templates
command = ["/usr/local/bin/awesome-app", "mgmt"] message = "Hello, ${name}!"
}
} # Application-provided functions
shouty_message = upper(message)

View File

@ -64,8 +64,9 @@ func extractLineComment(src []byte, endPos int) string {
return "" return ""
} }
// extractLeadingComments extracts comments from the very beginning of the file // extractLeadingComments extracts comments from the very beginning of the file.
func extractLeadingComments(src []byte) string { // It returns the comment text and the byte position of the last character in that leading block.
func extractLeadingComments(src []byte) (string, int) {
var comments []string var comments []string
i := 0 i := 0
@ -74,6 +75,8 @@ func extractLeadingComments(src []byte) string {
i++ i++
} }
lastPos := -1
// Extract comment lines from the start // Extract comment lines from the start
for i < len(src) && src[i] == '#' { for i < len(src) && src[i] == '#' {
lineStart := i lineStart := i
@ -81,6 +84,7 @@ func extractLeadingComments(src []byte) string {
for i < len(src) && src[i] != '\n' { for i < len(src) && src[i] != '\n' {
i++ i++
} }
lastPos = i - 1
comments = append(comments, strings.TrimSpace(string(src[lineStart:i]))) comments = append(comments, strings.TrimSpace(string(src[lineStart:i])))
// Skip newline // Skip newline
if i < len(src) && src[i] == '\n' { if i < len(src) && src[i] == '\n' {
@ -93,51 +97,46 @@ func extractLeadingComments(src []byte) string {
} }
if len(comments) > 0 { if len(comments) > 0 {
return strings.Join(comments, "\n") return strings.Join(comments, "\n"), lastPos
} }
return "" return "", -1
} }
// extractHeadComment extracts comments before a given start position // extractHeadComment extracts comments before a given start position
func extractHeadComment(src []byte, startPos int) string { func extractHeadComment(src []byte, startPos int) string {
var comments []string var comments []string
// Look backwards from startPos for comment lines // Start just before the token and skip trailing whitespace
i := startPos - 1 i := startPos - 1
// Skip whitespace backwards to find comment
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') { for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i-- i--
} }
// If we found a #, extract the comment for i >= 0 {
if i >= 0 && src[i] == '#' { // Find line boundaries
// Find the start of this line lineEnd := i
lineStart := i for i >= 0 && src[i] != '\n' {
for lineStart > 0 && src[lineStart-1] != '\n' {
lineStart--
}
// Extract from line start to comment end
comments = append(comments, strings.TrimSpace(string(src[lineStart:i+1])))
// Look for more comment lines above this one
i = lineStart - 1
for i >= 0 && (src[i] == '\n' || src[i] == '\r') {
i-- i--
} }
lineStart := i + 1
for i >= 0 && src[i] == '#' { line := strings.TrimRight(string(src[lineStart:lineEnd+1]), " \t\r")
lineStart = i trimmed := strings.TrimSpace(line)
for lineStart > 0 && src[lineStart-1] != '\n' {
lineStart--
}
comments = append([]string{strings.TrimSpace(string(src[lineStart : i+1]))}, comments...)
i = lineStart - 1 if trimmed == "" {
for i >= 0 && (src[i] == '\n' || src[i] == '\r') { break
i-- }
}
if !strings.HasPrefix(trimmed, "#") {
break
}
comments = append([]string{trimmed}, comments...)
// Move to previous line (skip any whitespace/newlines)
i = lineStart - 1
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i--
} }
} }
@ -176,7 +175,9 @@ func (dec *hclDecoder) Decode() (*CandidateNode, error) {
root := &CandidateNode{Kind: MappingNode} root := &CandidateNode{Kind: MappingNode}
// Extract file-level head comments (comments at the very beginning of the file) // Extract file-level head comments (comments at the very beginning of the file)
if leadingComment := extractLeadingComments(dec.fileBytes); leadingComment != "" { leadingComment, _ := extractLeadingComments(dec.fileBytes)
leadingUsed := false
if leadingComment != "" {
root.HeadComment = leadingComment root.HeadComment = leadingComment
} }
@ -188,7 +189,18 @@ func (dec *hclDecoder) Decode() (*CandidateNode, error) {
// Attach comments if any // Attach comments if any
attrRange := attrWithName.Attr.Range() attrRange := attrWithName.Attr.Range()
if headComment := extractHeadComment(dec.fileBytes, attrRange.Start.Byte); headComment != "" { headComment := extractHeadComment(dec.fileBytes, attrRange.Start.Byte)
if !leadingUsed && leadingComment != "" {
// Avoid double-applying the leading file comment to the first attribute
switch headComment {
case leadingComment:
headComment = ""
case "":
headComment = leadingComment
}
leadingUsed = true
}
if headComment != "" {
valNode.HeadComment = headComment valNode.HeadComment = headComment
} }
if lineComment := extractLineComment(dec.fileBytes, attrRange.End.Byte); lineComment != "" { if lineComment := extractLineComment(dec.fileBytes, attrRange.End.Byte); lineComment != "" {

View File

@ -114,22 +114,37 @@ func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string
// Convert output to string for easier manipulation // Convert output to string for easier manipulation
result := string(output) result := string(output)
// Look for head comments at the root level // Root-level head comment (stored as ".head")
// These are typically comments before the first attribute for path, comment := range commentMap {
if path == ".head" {
trimmed := strings.TrimSpace(comment)
if trimmed != "" && !strings.HasPrefix(result, trimmed) {
result = trimmed + "\n" + result
}
}
}
// Attribute head comments: insert above matching assignment
for path, comment := range commentMap { for path, comment := range commentMap {
parts := strings.Split(path, ".") parts := strings.Split(path, ".")
if len(parts) < 2 { if len(parts) != 2 {
continue continue
} }
commentType := parts[len(parts)-1] // "head", "line", or "foot" key := parts[0]
commentType := parts[1]
if commentType != "head" || key == "" {
continue
}
if commentType == "head" && len(parts) == 2 { trimmed := strings.TrimSpace(comment)
// Root-level head comment - inject at the beginning if trimmed == "" {
// Check if comment not already there continue
if !strings.HasPrefix(result, strings.TrimSpace(comment)) { }
result = comment + "\n" + result
} re := regexp.MustCompile(`(?m)^(\s*)` + regexp.QuoteMeta(key) + `\s*=`)
if re.MatchString(result) {
result = re.ReplaceAllString(result, "$1"+trimmed+"\n$0")
} }
} }

View File

@ -45,6 +45,23 @@ var multipleBlockLabelKeysExpectedYaml = `service:
- "management" - "management"
` `
var roundtripSample = `# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)`
var roundtripSampleExpected = `# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
`
var hclFormatScenarios = []formatScenario{ var hclFormatScenarios = []formatScenario{
{ {
description: "Simple decode", description: "Simple decode",
@ -250,6 +267,12 @@ var hclFormatScenarios = []formatScenario{
expected: "# Configuration\nport = 8080\n", expected: "# Configuration\nport = 8080\n",
scenarioType: "roundtrip", scenarioType: "roundtrip",
}, },
{
description: "roundtrip example",
input: roundtripSample,
expected: roundtripSampleExpected,
scenarioType: "roundtrip",
},
} }
func testHclScenario(t *testing.T, s formatScenario) { func testHclScenario(t *testing.T, s formatScenario) {